코드 예제
같은 검색을 여러 언어로 작성한 예제입니다. 특정 스크립트를 쓰는 사이트를 찾고, 결과를 페이지별로 넘기고, 기다리라는 응답을 받으면 정중하게 물러납니다. API 키는 파일에 직접 넣지 말고 환경 변수에 저장하세요.
같은 기능을 명령줄 도구가 포함된 작은 클라이언트로도 제공합니다(표준 라이브러리만 사용): Python, JavaScript, PHP, Go, Ruby. 스프레드시트에서는 Google Sheets, 잠재 고객 발굴 테이블에서는 Clay를 참고하세요.
curl
export PUBLICWWW_KEY=...
# One page of results
curl -H "Authorization: Bearer $PUBLICWWW_KEY" \
--get --data-urlencode 'query="angular.min.js"' --data 'per_page=20' \
https://api.publicwww.com/v1/search
# Every match, as a plain list of urls
curl -H "Authorization: Bearer $PUBLICWWW_KEY" \
--get --data-urlencode 'query="angular.min.js"' \
--data 'format=txt&per_page=100000' \
https://api.publicwww.com/v1/search > sites.txt
# A long, multi-line query is easier as a POST
curl https://api.publicwww.com/v1/search \
-H "Authorization: Bearer $PUBLICWWW_KEY" \
-H "Content-Type: application/json" \
-d '{"query": ["\"angular.min.js\"", "\"bootstrap.min.css\""], "per_page": 50}'
Python
import os, time, requests
KEY = os.environ["PUBLICWWW_KEY"]
BASE = "https://api.publicwww.com"
def search(query, **params):
"""One page of results, waiting out the rate limit if asked to."""
while True:
r = requests.get(BASE + "/v1/search",
headers={"Authorization": "Bearer " + KEY},
params={"query": query, **params})
if r.status_code == 429:
body = r.json()["error"]
if body["code"] != "too_many_requests":
raise RuntimeError(body["message"]) # quota, not pace
time.sleep(int(r.headers.get("Retry-After", 30)))
continue
r.raise_for_status()
return r.json()
page = search('"angular.min.js"', per_page=20)
print(page["total"], "sites match")
for row in page["results"]:
print(row["rank"], row["domain"], row["url"])
대용량 결과 스트리밍
import json, os, requests
r = requests.get("https://api.publicwww.com/v1/search",
headers={"Authorization": "Bearer " + os.environ["PUBLICWWW_KEY"]},
params={"query": '"angular.min.js"',
"format": "ndjson", "per_page": 1000000},
stream=True)
r.raise_for_status()
meta = None
for line in r.iter_lines():
if not line:
continue
obj = json.loads(line)
if obj.get("object") == "meta": # always the first line
meta = obj
print("expecting", meta["returned"], "of", meta["total"])
continue
print(obj["domain"])
PHP
<?php
$key = getenv ("PUBLICWWW_KEY");
$url = "https://api.publicwww.com/v1/search?" . http_build_query (array (
"query" => '"angular.min.js"',
"per_page" => 20,
));
$ch = curl_init ($url);
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt ($ch, CURLOPT_HTTPHEADER, array ("Authorization: Bearer " . $key));
$body = curl_exec ($ch);
$status = curl_getinfo ($ch, CURLINFO_HTTP_CODE);
curl_close ($ch);
$data = json_decode ($body, true);
if ($status != 200) {
die ($data ["error"] ["code"] . ": " . $data ["error"] ["message"] . "\n");
}
echo $data ["total"] . " sites match\n";
foreach ($data ["results"] as $r) {
echo $r ["rank"] . "\t" . $r ["domain"] . "\t" . $r ["url"] . "\n";
}
JavaScript (Node)
const KEY = process.env.PUBLICWWW_KEY;
const BASE = "https://api.publicwww.com";
async function search (query, params = {}) {
const url = new URL (BASE + "/v1/search");
url.searchParams.set ("query", query);
for (const [k, v] of Object.entries (params)) url.searchParams.set (k, v);
for (;;) {
const res = await fetch (url, { headers: { Authorization: `Bearer ${KEY}` } });
const body = await res.json ();
if (res.status === 429 && body.error.code === "too_many_requests") {
await new Promise (r => setTimeout (r, body.error.retry_after * 1000));
continue; // told to wait, not refused
}
if (!res.ok) throw new Error (`${body.error.code}: ${body.error.message}`);
return body;
}
}
const page = await search ('"angular.min.js"', { per_page: 20 });
console.log (page.total, "sites match");
for (const r of page.results) console.log (r.rank, r.domain, r.url);
Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type Result struct {
Domain string `json:"domain"`
URL string `json:"url"`
Rank *int `json:"rank"` // nil when the site has no rank
}
type Page struct {
Total int `json:"total"`
Results []Result `json:"results"`
}
func main () {
q := url.Values{}
q.Set ("query", `"angular.min.js"`)
q.Set ("per_page", "20")
req, _ := http.NewRequest ("GET", "https://api.publicwww.com/v1/search?"+q.Encode (), nil)
req.Header.Set ("Authorization", "Bearer "+os.Getenv ("PUBLICWWW_KEY"))
res, err := http.DefaultClient.Do (req)
if err != nil { panic (err) }
defer res.Body.Close ()
if res.StatusCode != 200 {
fmt.Println ("http", res.StatusCode, res.Header.Get ("Retry-After"))
os.Exit (1)
}
var page Page
json.NewDecoder (res.Body).Decode (&page)
fmt.Println (page.Total, "sites match")
for _, r := range page.Results {
fmt.Println (r.Domain, r.URL)
}
}
Ruby
require "json"
require "net/http"
KEY = ENV.fetch ("PUBLICWWW_KEY")
uri = URI ("https://api.publicwww.com/v1/search")
uri.query = URI.encode_www_form (query: '"angular.min.js"', per_page: 20)
req = Net::HTTP::Get.new (uri)
req["Authorization"] = "Bearer #{KEY}"
res = Net::HTTP.start (uri.host, uri.port, use_ssl: true) { |http| http.request (req) }
body = JSON.parse (res.body)
abort "#{body['error']['code']}: #{body['error']['message']}" unless res.code == "200"
puts "#{body['total']} sites match"
body["results"].each { |r| puts [r["rank"], r["domain"], r["url"]].join ("\t") }
스프레드시트나 셸 파이프라인으로 불러오기
# A csv of domain and rank, ready for a spreadsheet
curl -H "Authorization: Bearer $PUBLICWWW_KEY" \
--get --data-urlencode 'query="angular.min.js"' \
--data 'format=csv&header=1&delimiter=,&per_page=10000' \
https://api.publicwww.com/v1/search > sites.csv
# Just the domains, one per line, into whatever comes next
curl -H "Authorization: Bearer $PUBLICWWW_KEY" \
--get --data-urlencode 'query="angular.min.js"' \
--data 'format=csv&columns=domain&per_page=10000' \
https://api.publicwww.com/v1/search | sort -u
꼭 지켜야 할 점
- 두 가지 429를 구분하세요.
too_many_requests는 속도를 늦추고 다시 보내라는 뜻입니다.quota_exceeded는 내일 다시 오라는 뜻이며, 반복해서 재시도해도 소용없고 공격처럼 보일 수 있습니다. Retry-After를 따르세요. 간격을 추측하지 마세요. 다음 요청이 가능해질 때까지 남은 정확한 초입니다.truncated를 확인하세요. 결과 집합이 완전하다고 판단하기 전에 확인해야 합니다.rank: null에 대비하세요. 인덱싱된 사이트 중 순위가 없는 곳이 많습니다. null을 허용하지 않는 정수형을 쓰는 언어라면 Go 예제처럼 포인터나 옵션 타입이 필요합니다.page로 페이지를 넘기세요. 더 많은 행을 얻으려고 쿼리를 조금씩 바꿔 다시 실행하지 마세요.