クラスター

クラスターとは、保存したウェブサイトのリストです。APIを使うと、検索や独自のリストからクラスターを作成し、クラスター同士を組み合わせ、クラスター内のすべてのサイトのページのソースから値を取り出せます。連絡先、SNSのプロフィール、アナリティクスやタグのIDなど、正規表現で一致させられるものなら何でも取り出せます。

エンドポイント

エンドポイントメソッド機能
/v1/clustersGETアカウントのクラスターを新しい順に返します。ID、名前、ドメイン数、作成日時。
/v1/clustersPOST検索(query)またはリスト(domains)からクラスターを作成します。nameは省略できます。
/v1/clusters/{id}GETクラスターと、そのドメインの1ページ分を返します。page、per_page(最大10,000)、1行に1ドメインずつ返すformat=txt。
/v1/clusters/combinePOST既存のクラスターから新しいクラスターを作ります。operation(and、or、diff)、clusters(IDのリスト)。
/v1/clusters/{id}/extractGET, POSTpresetsとregexで値を少しずつ抽出します。offset、limit(1回の呼び出しで最大1,000サイト)、format(json、xml、csv)。
/v1/clusters/presetsGET既製の式とそのパターンの一覧。
/v1/clusters/{id}/renamePOST新しいnameを設定します。
/v1/clusters/{id}/deletePOSTクラスターを完全に削除します。

クラスターを変更する操作はすべて、JSONの本文を付けたPOSTです。PUTやDELETEは使わないため、検索ができるクライアントならクラスターも管理できます。トークン、レート制限、エラーの形式は検索と同じです。/v1/accountの応答に含まれるクラスターの数値で、保有しているクラスターの数と、本日残っている抽出ポイントを確認できます。

クラスターの作成

検索から:クエリの結果が、プランの1回の検索あたりの行数を上限としてクラスターになります。/v1/searchと同じく検索を1回消費します。

curl https://api.publicwww.com/v1/clusters \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"query": "\"googletagmanager.com/gtm.js\"", "name": "GTM sites"}'

{"id": 7, "name": "GTM sites", "size": 100000, "created": "2026-10-10T20:21:30Z",
 "query": "\"googletagmanager.com/gtm.js\"", "total": 2412577, "index_complete": true}

リストから:ドメインまたはURLを、JSONの配列か、1行に1つずつの形式で渡します。インデックスにあるサイトだけが残ります。送信した数はsubmitted、クラスターに入った数はsizeでわかります。リストからの作成は何も消費しません。

curl https://api.publicwww.com/v1/clusters \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"domains": ["example.com", "https://www.example.org/about"], "name": "Prospects"}'

1つのクラスターに入れられるドメイン数はプランによって決まり、1アカウントで保存できるクラスターは最大100個です。すでに100個ある状態で新たに作成しようとすると、409 cluster_limitが返されます。こちらで代わりに削除することはないため、不要になったクラスターを先に削除してください。

クラスターの組み合わせ

curl https://api.publicwww.com/v1/clusters/combine \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"operation": "diff", "clusters": [7, 3], "name": "GTM, not yet contacted"}'

andはすべてのクラスターに含まれるドメイン、orはいずれかに含まれるドメイン、diffは1つ目のクラスターにあって2つ目にないドメインを残します(この場合、クラスターはちょうど2つ指定します)。組み合わせは何も消費しません。

データの抽出

抽出では、クラスター内のすべてのサイトのインデックス済みページを読み込み、式でキャプチャした内容を、式ごとに1列ずつ返します。いちばん簡単なのはプリセットを使う方法です。

プリセット取り出す内容
emailmailto:リンクのアドレス
phonetel:リンクの番号
whatsapp, telegram, skype各サービスへのリンクから、WhatsAppの電話番号、Telegramのユーザー名、Skypeの名前
facebook, instagram, twitter, linkedinサイトのSNSプロフィールへのリンク(twitterはx.comも、linkedinは企業ページも対象)
gtm, ga4, uaGoogle Tag ManagerのコンテナID、Google Analytics 4とUniversal AnalyticsのID
hotjarHotjarのサイトID
adsenseAdSenseのパブリッシャーID
bitcoinbitcoin:決済リンクのアドレス

独自の式を書くこともできます。スラッシュ(またはパイプ)で囲んだ200文字以内の正規表現で、i、m、s、uのフラグを任意で付けられます。最初のキャプチャグループが値になり、これはsnipexp:と同じルールです。式は、プリセットを含めて1回の呼び出しにつき最大10個までです。

curl https://api.publicwww.com/v1/clusters/7/extract \
     -H "Authorization: Bearer $PUBLICWWW_KEY" \
     -H "Content-Type: application/json" \
     -d '{"presets": ["gtm", "email"], "regex": ["/data-site-id=\"([0-9]+)\"/i"], "limit": 1000}'

{
  "cluster": 7, "name": "GTM sites", "size": 100000,
  "offset": 0, "scanned": 1000, "in_index": 1000, "with_matches": 941,
  "next_offset": 1000,
  "regex": ["/(GTM-[A-Z0-9]{4,10})\\b/", "/mailto:(...)/i", "/data-site-id=\"([0-9]+)\"/i"],
  "points_used": 1834.2, "points_left": 98165,
  "rows": [
    { "domain": "example.com", "values": [["GTM-AB12CD"], ["info@example.com"], []], "matches": 2 }
  ]
}

少しずつ取得する。1回の呼び出しでは、offsetの位置から、クラスターのサイトを最大1,000件処理します。offsetにnext_offsetの値を設定して、nullが返るまで呼び出しを繰り返してください。何も一致しなかったサイトは省かれますが、skip_empty=0を指定するとそれらも一覧に含まれます。format=csvでは、サイトごとに1行(ドメイン、続いて式ごとに1列)が返され、次のオフセットはX-Next-Offsetヘッダーで示されます。

ポイント。抽出では、プランの1日の抽出枠を消費します。インデックスで見つかった各サイトは、そのサイトで見つかった値の数だけポイントを消費し、値がなければ0.1ポイントです。途中でポイントがなくなると、呼び出しは"stopped": "extract_quota_exceeded"を付けて早めに終了し、それまでに得た結果を返します。ポイントが残っていない状態で呼び出すと429 extract_quota_exceededが返されます。式はまず小さなlimitで試してください。詳しくはクラスターの制限をご覧ください。

クラスター全体をコードで処理する

検索からクラスターを作成し、すべてのサイトから抽出した値をCSVファイルに書き出します。クライアントライブラリには、同じ処理が既製の関数とコマンドラインツールとして用意されています。

Python

import csv, os, time, requests

KEY  = os.environ["PUBLICWWW_KEY"]
BASE = "https://api.publicwww.com"
H    = {"Authorization": "Bearer " + KEY}

def call(method, path, body=None):
    while True:
        r = requests.request(method, BASE + path, headers=H, json=body)
        if r.status_code == 429 and r.json()["error"]["code"] == "too_many_requests":
            time.sleep(int(r.headers.get("Retry-After", 30)))
            continue
        r.raise_for_status()
        return r.json()

cluster = call("POST", "/v1/clusters", {"query": '"googletagmanager.com/gtm.js"'})
offset = 0
with open("extract.csv", "w", newline="") as f:
    out = csv.writer(f)
    while offset is not None:
        part = call("POST", "/v1/clusters/%d/extract" % cluster["id"],
                    {"presets": ["gtm", "email"], "offset": offset})
        for row in part["rows"]:
            out.writerow([row["domain"]] + [" ".join(v) for v in row["values"]])
        offset = part["next_offset"]

JavaScript (Node 18+)

const BASE = "https://api.publicwww.com";
const H = { Authorization: "Bearer " + process.env.PUBLICWWW_KEY,
            "Content-Type": "application/json" };

async function call(method, path, body) {
  for (;;) {
    const r = await fetch(BASE + path, { method, headers: H,
                                         body: body && JSON.stringify(body) });
    const data = await r.json();
    if (r.status === 429 && data.error.code === "too_many_requests") {
      await new Promise(ok => setTimeout(ok, 1000 * (r.headers.get("Retry-After") || 30)));
      continue;
    }
    if (!r.ok) throw new Error(data.error.message);
    return data;
  }
}

const cluster = await call("POST", "/v1/clusters", { query: '"hotjar.com"' });
for (let offset = 0; offset !== null; ) {
  const part = await call("POST", `/v1/clusters/${cluster.id}/extract`,
                          { presets: ["hotjar", "email"], offset });
  for (const row of part.rows) console.log(row.domain, row.values.map(v => v.join(" ")).join(";"));
  offset = part.next_offset;
}

PHP

<?php
function call ($method, $path, $body = null) {
    $ch = curl_init ("https://api.publicwww.com" . $path);
    curl_setopt_array ($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ["Authorization: Bearer " . getenv ("PUBLICWWW_KEY"),
                                   "Content-Type: application/json"],
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode ($body),
    ]);
    $data = json_decode (curl_exec ($ch), true);
    if (isset ($data ["error"])) throw new Exception ($data ["error"]["message"]);
    return $data;
}

$cluster = call ("POST", "/v1/clusters", ["query" => '"jquery.min.js"']);
$out = fopen ("extract.csv", "w");
for ($offset = 0; $offset !== null; ) {
    $part = call ("POST", "/v1/clusters/" . $cluster ["id"] . "/extract",
                  ["presets" => ["email", "phone"], "offset" => $offset]);
    foreach ($part ["rows"] as $row)
        fputcsv ($out, array_merge ([$row ["domain"]], array_map (fn ($v) => join (" ", $v), $row ["values"])));
    $offset = $part ["next_offset"];
}

Go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
)

func call(method, path string, body, out any) error {
	b, _ := json.Marshal(body)
	req, _ := http.NewRequest(method, "https://api.publicwww.com"+path, bytes.NewReader(b))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PUBLICWWW_KEY"))
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		return fmt.Errorf("publicwww: %s", resp.Status)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

func main() {
	var cluster struct{ ID int `json:"id"` }
	if err := call("POST", "/v1/clusters", map[string]any{"query": `"googletagmanager.com/gtm.js"`}, &cluster); err != nil {
		panic(err)
	}
	for offset := 0; ; {
		var part struct {
			Rows []struct {
				Domain string     `json:"domain"`
				Values [][]string `json:"values"`
			} `json:"rows"`
			NextOffset *int `json:"next_offset"`
		}
		path := fmt.Sprintf("/v1/clusters/%d/extract", cluster.ID)
		if err := call("POST", path, map[string]any{"presets": []string{"gtm", "ga4"}, "offset": offset}, &part); err != nil {
			panic(err)
		}
		for _, r := range part.Rows {
			cells := []string{r.Domain}
			for _, v := range r.Values {
				cells = append(cells, strings.Join(v, " "))
			}
			fmt.Println(strings.Join(cells, ";"))
		}
		if part.NextOffset == nil {
			break
		}
		offset = *part.NextOffset
	}
}

Ruby

require "json"
require "net/http"

def call(path, body)
  uri = URI("https://api.publicwww.com" + path)
  req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{ENV.fetch('PUBLICWWW_KEY')}",
                                  "Content-Type" => "application/json")
  req.body = body.to_json
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  data = JSON.parse(res.body)
  raise data["error"]["message"] if data["error"]
  data
end

cluster = call("/v1/clusters", { query: '"hotjar.com"' })
offset = 0
while offset
  part = call("/v1/clusters/#{cluster['id']}/extract", { presets: %w[hotjar email], offset: offset })
  part["rows"].each { |r| puts [r["domain"], *r["values"].map { |v| v.join(" ") }].join(";") }
  offset = part["next_offset"]
end

エラー

ステータスとコード意味
404 cluster_not_foundこのアカウントに、そのIDのクラスターはありません。
409 cluster_limitアカウントにはすでに100個のクラスターがあります。
400 invalid_regexスラッシュまたはパイプで囲んだ200文字以内のPCREになっていない式があります。
400 unknown_presetそのようなプリセットはありません。応答にプリセットの一覧が含まれます。
400 missing_source, ambiguous_source作成にはqueryとdomainsのどちらか一方が必要です。
429 extract_quota_exceeded本日の抽出ポイントを使い切りました。

エラーの全一覧は、エラーのページと、https://api.publicwww.com/にあるAPI自身の説明に掲載しています。AIアシスタントでは、同じ操作をMCPツールとして利用できます。

次へ コード例