APIキーが要らないスクレイピングを n8n で組むとき、いちばん壊れやすいのは CSSセレクタです。
相手のマークアップが変わった瞬間に、エラーも出さずに空文字を返すようになる。
この記事は、その依存を減らすために sitemap.xml と JSON-LD だけを入口にした ワークフローの実装メモです。
題材は「公開されているプロンプト集から毎朝1件取ってSlackに流す」ですが、
構造化データを出しているサイトなら対象は何でも同じ構成で組めます。
構成
ノードは7つ、認証情報はゼロです。
Schedule Trigger
→ HTTP Request (sitemap.xml をテキストで取得)
→ Code (詳細ページだけに絞って、当日分を1件選ぶ)
→ HTTP Request (そのページをテキストで取得)
→ HTML (h1 / pre / script[type=ld+json] を抽出)
→ Code (JSON-LD から VideoObject を拾ってメッセージ組み立て)
→ HTTP Request (Slack / Discord の Incoming Webhook に POST)
1. sitemap.xml を一覧の代わりにする
一覧ページをスクレイピングすると、ページネーション・遅延読み込み・レイアウト変更の3つに付き合うことになります。
sitemap.xml はそのどれも起きません。URLの形だけで絞れます。
const xml = $input.first().json.data || '';
const urls = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)]
.map((m) => m[1].trim())
.filter((u) => u.includes('/prompt/'))
.filter((u) => !u.includes('/prompt/category/')); // 一覧ページを除外
if (urls.length === 0) {
throw new Error('No prompt pages found in the sitemap.');
}
🔴 0件のときは例外を投げる。 ここを return [] にすると、
サイトマップの構造が変わった日から静かに何も投稿されなくなります。
「毎朝来るはずのものが来ない」は誰も気づきません。落ちてくれたほうが早く直せます。
2. ランダムではなく日付で回す
const dayIndex = Math.floor(Date.now() / 86400000);
const url = urls[dayIndex % urls.length];
Math.random() だと 1週間以内に同じものが再登場しますし、
デバッグのたびに違うページを踏むので再現しません。
day % length なら「1日1件・重複なし・再実行しても同じ」が同時に満たせます。
3. レイアウトではなくスキーマを読む
抽出は HTML ノードで3つ取ります。
{
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{ "key": "title", "cssSelector": "h1" },
{ "key": "body", "cssSelector": "pre", "returnArray": true },
{ "key": "structuredData", "cssSelector": "script[type='application/ld+json']", "returnArray": true }
]
}
}
そして本文の意味は JSON-LD 側から取ります。
let video = {};
for (const raw of item.structuredData || []) {
try {
const parsed = JSON.parse(raw);
const blocks = Array.isArray(parsed) ? parsed : [parsed];
const found = blocks.find((b) => b['@type'] === 'VideoObject');
if (found) { video = found; break; }
} catch (e) {
// 壊れたブロックがあっても致命的ではない。次を見る
}
}
// video.name / video.description / video.contentUrl
div.article-body > p:nth-child(2) はデザイン変更で死にますが、
JSON-LD はSEOのために出しているので、迂闊に消えません。
構造化データはスクレイパにとって事実上の公開APIです。
⚠️ JSON-LD は1ページに複数ブロック入ります(Organization / BreadcrumbList / VideoObject …)。
[0] 決め打ちにせず @type で探すこと。配列で入っているケースもあるので Array.isArray も要ります。
4. いちばん時間を溶かしたところ: returnArray: false
最初の実行はエラーも警告も出ず、日本語の本文が英語チャンネルに流れました。
原因は、対象ページに <pre> が 2つあったこと(日本語原文と英訳)。
HTML ノードは returnArray: false だと黙って1つ目を返します。
// returnArray: true にしたうえで、明示的に選ぶ
const blocks = (item.body || []).map((t) => String(t).trim()).filter(Boolean);
const text = blocks.length > 1 ? blocks[1] : (blocks[0] || '');
教訓としては、セレクタが何件マッチするかを先に数える。
document.querySelectorAll(sel).length を1回見るだけで済みます。
1件だと思い込んだセレクタが2件マッチしているとき、n8n は何も言ってくれません。
5. Slack と Discord を1つのノードで済ませる
Discord の Webhook URL の末尾に /slack を足すと、Slack互換のペイロードを受け付けます。
// どちらでも同じ body でいい
{ text: message }
送信先を変えるのに分岐もノード追加も不要で、URLを差し替えるだけになります。
できたもの
毎朝、プロンプト本文とそれで実際に生成された動画のURLが1件流れてきます。
JSON-LD の contentUrl に成果物が入っているので、テキストだけでなく結果まで一緒に取れました。
題材にしたのは Seedance 2 のプロンプト集(150件、全件が生成結果つきで公開)です。
ついでに150件を集計したところ、本文の長さは中央値152文字、
参照画像が必要なものが32件(image-to-video 系)でした。
「長く書くほど制御できる」という前提はこの範囲では成立していない、というのが個人的にいちばん意外でした。
同じ構成は、構造化データを出しているサイトならほぼそのまま流用できます。
Recipe / Product / Article / JobPosting あたりはスキーマが安定しているので、
DOMを触らずに中身だけ取れることが多いです。
まずは対象ページで script[type="application/ld+json"] を覗いてみるのをおすすめします。