Clusters
Un cluster est une liste de sites enregistrée. Via l’API, vous pouvez en créer un à partir d’une recherche ou de votre propre liste, combiner des clusters et extraire des valeurs du code source de chaque site d’un cluster - contacts, profils sur les réseaux sociaux, identifiants d’analytics et de balises, tout ce qu’une expression régulière peut capturer.
Les points de terminaison
| Point de terminaison | Méthode | Rôle |
|---|---|---|
/v1/clusters | GET | Les clusters du compte, du plus récent au plus ancien : identifiant, nom, nombre de domaines, date de création. |
/v1/clusters | POST | Crée un cluster à partir d’une recherche (query) ou d’une liste (domains) ; name est facultatif. |
/v1/clusters/{id} | GET | Le cluster et une page de ses domaines : page, per_page (jusqu’à 10 000), format=txt pour un domaine par ligne. |
/v1/clusters/combine | POST | Un nouveau cluster à partir de clusters existants : operation and, or ou diff, clusters - une liste d’identifiants. |
/v1/clusters/{id}/extract | GET, POST | Extrait des valeurs avec presets et regex, partie par partie : offset, limit (jusqu’à 1 000 sites par appel), format json, xml ou csv. |
/v1/clusters/presets | GET | Les expressions toutes faites, avec leurs motifs. |
/v1/clusters/{id}/rename | POST | Nouveau name. |
/v1/clusters/{id}/delete | POST | Supprime définitivement le cluster. |
Tout ce qui modifie un cluster passe par un POST avec un corps JSON - ni PUT ni
DELETE : tout client capable de lancer une recherche peut donc aussi gérer des
clusters. Le jeton, la limite de débit et le format des erreurs sont les mêmes que
pour la recherche ; les chiffres des clusters dans
la réponse de /v1/account indiquent combien vous en avez et combien de
points d’extraction il vous reste aujourd’hui.
Créer un cluster
À partir d’une recherche : les résultats de la requête, dans la limite du
nombre de lignes de votre forfait, deviennent le cluster. Cela coûte une recherche,
comme /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}
À partir d’une liste : des domaines ou des URL, sous forme de tableau JSON ou
un par ligne. Seuls les sites présents dans l’index sont conservés -
submitted indique combien vous en avez envoyé, size combien
ont été retenus. Une liste ne coûte rien.
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"}'
Le forfait plafonne le nombre de domaines d’un cluster, et un compte conserve
jusqu’à 100 clusters. S’il y en a déjà 100, une nouvelle création renvoie
409 cluster_limit - rien n’est supprimé à votre place ; supprimez
d’abord ceux dont vous n’avez plus besoin.
Combiner des clusters
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 garde les domaines présents dans tous les clusters, or
ceux présents dans au moins l’un d’eux, et diff ceux du premier cluster
qui ne sont pas dans le second - exactement deux clusters dans ce cas. La
combinaison ne coûte rien.
Extraire des données
L’extraction lit les pages indexées de chaque site du cluster et renvoie ce que les expressions capturent, une colonne par expression. Le plus simple est d’utiliser un modèle prédéfini :
| Modèle prédéfini | Ce qu’il extrait |
|---|---|
email | Adresses tirées des liens mailto: |
phone | Numéros tirés des liens tel: |
whatsapp, telegram, skype | Numéros WhatsApp, noms d’utilisateur Telegram et identifiants Skype tirés de leurs liens |
facebook, instagram, twitter, linkedin | Liens vers les profils du site sur les réseaux sociaux (twitter reconnaît aussi x.com, linkedin les pages entreprise) |
gtm, ga4, ua | Identifiants de conteneur Google Tag Manager, Google Analytics 4 et Universal Analytics |
hotjar | Identifiant de site Hotjar |
adsense | Identifiant d’éditeur AdSense |
bitcoin | Adresses tirées des liens de paiement bitcoin: |
Ou écrivez la vôtre : une expression régulière entre barres obliques (ou barres
verticales) avec les options facultatives i, m,
s, u, de 200 caractères au plus. Le premier groupe
capturant est la valeur - la même règle que pour
snipexp:. Jusqu’à dix
expressions par appel, modèles prédéfinis compris.
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 }
]
}
Partie par partie. Un appel parcourt jusqu’à 1 000 sites du cluster, à partir
de offset. Rappelez ensuite l’API avec offset égal à
next_offset, jusqu’à ce que celui-ci vaille null. Les
sites sans correspondance sont omis ; skip_empty=0 les liste aussi.
format=csv donne une ligne par site - le domaine, puis une colonne par
expression - avec le décalage suivant dans l’en-tête X-Next-Offset.
Points. L’extraction consomme le quota d’extraction quotidien de votre
forfait : chaque site trouvé dans l’index coûte autant de points que de valeurs
trouvées, 0,1 s’il n’y en a aucune. Quand les points sont épuisés, l’appel
s’arrête plus tôt avec "stopped": "extract_quota_exceeded" et renvoie
ce qu’il a déjà ; un appel sans aucun point restant reçoit
429 extract_quota_exceeded. Essayez d’abord une expression avec un
petit limit - voir les limites des clusters.
Un cluster entier, en code
Créer un cluster à partir d’une recherche et écrire dans un fichier CSV les valeurs extraites de chaque site. Les bibliothèques clientes proposent la même chose sous forme de fonctions prêtes à l’emploi et d’une ligne de commande.
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
Erreurs
| Statut et code | Signification |
|---|---|
404 cluster_not_found | Aucun cluster avec cet identifiant sur ce compte. |
409 cluster_limit | Le compte conserve déjà 100 clusters. |
400 invalid_regex | Une expression n’est pas une expression PCRE entre barres obliques ou barres verticales, de 200 caractères au plus. |
400 unknown_preset | Modèle prédéfini inconnu ; la réponse donne la liste des modèles existants. |
400 missing_source, ambiguous_source | La création exige soit query, soit domains. |
429 extract_quota_exceeded | Les points d’extraction du jour sont épuisés. |
La liste complète figure sur la page des erreurs et dans la description de l’API elle-même, à l’adresse https://api.publicwww.com/. Dans un assistant IA, les mêmes opérations sont des outils MCP.
Suivant Exemples de code