Cluster

Ein Cluster ist eine gespeicherte Liste von Websites. Über die API können Sie einen Cluster aus einer Suche oder aus Ihrer eigenen Liste erstellen, Cluster kombinieren und Werte aus dem Quelltext jeder Website eines Clusters herausziehen - Kontakte, Social-Media-Profile, Analytics- und Tag-IDs, alles, was ein regulärer Ausdruck erfassen kann.

Die Endpunkte

EndpunktMethodeFunktion
/v1/clustersGETDie Cluster des Kontos, die neuesten zuerst: ID, Name, Zahl der Domains, Erstellungszeitpunkt.
/v1/clustersPOSTErstellt einen Cluster aus einer Suche (query) oder aus einer Liste (domains); name ist optional.
/v1/clusters/{id}GETDer Cluster und eine Seite seiner Domains: page, per_page (bis zu 10.000), format=txt für eine Domain pro Zeile.
/v1/clusters/combinePOSTEin neuer Cluster aus bestehenden: operation and, or oder diff, clusters - eine Liste von IDs.
/v1/clusters/{id}/extractGET, POSTExtrahiert Werte mit presets und regex, Teil für Teil: offset, limit (bis zu 1.000 Websites pro Aufruf), format json, xml oder csv.
/v1/clusters/presetsGETDie fertigen Ausdrücke mit ihren Mustern.
/v1/clusters/{id}/renamePOSTNeuer name.
/v1/clusters/{id}/deletePOSTLöscht den Cluster endgültig.

Alles, was einen Cluster verändert, ist ein POST mit JSON-Body - kein PUT oder DELETE, sodass jeder Client, der suchen kann, auch Cluster verwalten kann. Token, Ratenlimit und Fehlerformat sind dieselben wie bei der Suche; die Cluster-Angaben in der Antwort von /v1/account zeigen, wie viele Cluster Sie haben und wie viele Extraktionspunkte heute noch übrig sind.

Einen Cluster erstellen

Aus einer Suche: Die Ergebnisse der Suchanfrage werden zum Cluster, bis zur maximalen Zeilenzahl Ihres Tarifs. Das kostet eine Suche, genau wie /v1/search.

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}

Aus einer Liste: Domains oder URLs, als JSON-Array oder eine pro Zeile. Übernommen werden nur Websites, die im Index sind - submitted gibt an, wie viele Sie gesendet haben, size, wie viele übernommen wurden. Eine Liste kostet nichts.

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"}'

Der Tarif begrenzt die Zahl der Domains in einem Cluster, und ein Konto behält bis zu 100 Cluster. Sind bereits 100 vorhanden, wird das Erstellen eines weiteren mit 409 cluster_limit beantwortet - in Ihrem Namen wird nichts gelöscht; löschen Sie zuerst die Cluster, die Sie nicht mehr brauchen.

Cluster kombinieren

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 behält die Domains, die in jedem der Cluster sind, or die, die in mindestens einem sind, und diff die, die im ersten Cluster sind, aber nicht im zweiten - dafür genau zwei Cluster. Kombinieren kostet nichts.

Daten extrahieren

Die Extraktion liest die indexierten Seiten jeder Website im Cluster und liefert, was die Ausdrücke erfassen, eine Spalte pro Ausdruck. Am einfachsten geht es mit einer Vorlage:

VorlageWas sie herausholt
emailAdressen aus mailto:-Links
phoneNummern aus tel:-Links
whatsapp, telegram, skypeWhatsApp-Nummern, Telegram-Benutzernamen und Skype-Namen aus den jeweiligen Links
facebook, instagram, twitter, linkedinLinks zu den Social-Media-Profilen der Website (twitter erfasst auch x.com, linkedin auch Unternehmensseiten)
gtm, ga4, uaContainer-IDs von Google Tag Manager sowie IDs von Google Analytics 4 und Universal Analytics
hotjarHotjar-Site-ID
adsenseAdSense-Publisher-ID
bitcoinAdressen aus bitcoin:-Zahlungslinks

Oder Sie schreiben ein eigenes Muster: einen regulären Ausdruck zwischen Schrägstrichen (oder senkrechten Strichen) mit den optionalen Flags i, m, s, u, bis zu 200 Zeichen lang. Die erste Erfassungsgruppe ist der Wert - dieselbe Regel wie bei snipexp:. Bis zu zehn Ausdrücke pro Aufruf, Vorlagen eingeschlossen.

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 }
  ]
}

Teil für Teil. Ein Aufruf geht bis zu 1.000 Websites des Clusters durch, beginnend bei offset. Rufen Sie dann erneut auf und setzen Sie offset auf next_offset, bis dieser Wert null ist. Websites ohne Treffer werden weggelassen; mit skip_empty=0 werden auch sie aufgeführt. format=csv liefert eine Zeile pro Website - die Domain, dann eine Spalte pro Ausdruck - und den nächsten Offset im Header X-Next-Offset.

Punkte. Die Extraktion verbraucht das tägliche Extraktionskontingent Ihres Tarifs: Jede im Index gefundene Website kostet so viele Punkte, wie Werte auf ihr gefunden wurden, 0,1, wenn keine. Sind die Punkte aufgebraucht, bricht der Aufruf vorzeitig mit "stopped": "extract_quota_exceeded" ab und liefert, was er bis dahin hat; ein Aufruf ohne verbleibende Punkte erhält 429 extract_quota_exceeded. Probieren Sie einen Ausdruck zuerst mit einem kleinen limit aus - siehe Cluster-Limits.

Ein ganzer Cluster im Code

Einen Cluster aus einer Suche erstellen und die extrahierten Werte jeder Website in eine CSV-Datei schreiben. Die Client-Bibliotheken bieten dasselbe als fertige Funktionen und auf der Kommandozeile.

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

Fehler

Status und CodeBedeutung
404 cluster_not_foundKein Cluster mit dieser ID in diesem Konto.
409 cluster_limitDas Konto hat bereits 100 Cluster.
400 invalid_regexEin Ausdruck ist kein PCRE-Ausdruck zwischen Schrägstrichen oder senkrechten Strichen mit bis zu 200 Zeichen.
400 unknown_presetKeine solche Vorlage; die Antwort listet die vorhandenen auf.
400 missing_source, ambiguous_sourceZum Erstellen ist entweder query oder domains nötig.
429 extract_quota_exceededDie Extraktionspunkte für heute sind aufgebraucht.

Die vollständige Liste steht auf der Seite Fehler und in der Selbstbeschreibung der API unter https://api.publicwww.com/. In einem KI-Assistenten stehen dieselben Operationen als MCP-Tools bereit.

Weiter Codebeispiele