サイトマップ駆動のクローリング: フェッチする前にURLを見つける
Scraping and Proxy Management Expert
TL;DR:
- ほとんどのクローラーはリンクをたどることでURLを発見しますが、これは不要なページを取得してから必要なページを見つけることになります。
- サイトマップはあらかじめ一覧を提供します:1つのHTTPリクエストで、発行者によって重複を排除されたサイトがインデックスにしたいすべてのURLが返されます。
- このガイドで実際のサイトマップをフィルタリングした結果、7,577件がターゲットセクションに一致した597件に減りましたが、ページを一つも取得する前でした。
- 何かが止まるまでクローリングするのではなく、そのリストから明示的なクロール予算を設定しましょう — 制約はコードに置くべきであり、あなたの意図にあるべきではありません。
- データが必要なときだけJavaScriptをレンダリングしてください。このウォークスルーにあるページはサーバーサイドでタイトルを提供するため、非レンダリング取得は迅速であり、同じフィールドを返します。
- Scrapelessの無料プランから開始し、クローリングを許可されているサイトマップに発見ステップを指向してください。
リンクをたどるクローラーは機能しますが、無駄が多いです。サイトのアーティクルページに到達するために、ホームページ、カテゴリリスト、ページネーション、タグページ、そしてエントリポイントとコンテンツの間にあるその他のすべてを取得し、そのほとんどを捨てます。
サイトマップはその手間を省きます。それは発行者が明示的に発見したいと望んでいるURLのリストであり、最も安価な発見メカニズムであり、誰かを苛立たせる可能性が最も低いです。
サイトマップが提供するもの
サイトマップは、サイトのURLをリストするXMLファイルであり、サイトマッププロトコルによって定義されています。2つの形式が存在し、両方を扱う必要があります:
- urlsetは、ページを指す
<url><loc>エントリを含みます。 - sitemapindexは、他のサイトマップを指す
<sitemap><loc>エントリを含み、サイトがプロトコルの50,000-URLまたは50 MBのファイル制限を超える場合に使用されます。
両方とも同じ<loc>要素を使用しますので、以下のパーサーは<loc>を読み取り、それが何であるかを決定します。サイトマップは一般にgzip圧縮されて提供されるため、フェッチャーはそれを透過的に扱う必要があります。
見つける場所:まず/sitemap.xmlをチェックし、次にrobots.txt内のSitemap:ディレクティブを確認します。これはロボット排除プロトコル標準によって宣伝場所として定義されています。
前提条件
- Python 3.9以降。発見ステップでは標準ライブラリのみを使用します。
- フェッチステップ用のScrapeless APIキー:
bash
export SCRAPELESS_API_KEY="your_api_key_here"
以下の例はScrapelessのサイトマップを指しているため、ウォークスルーはそのまま実行することが安全です。適応する際には、クローリングが許可されているサイトマップに差し替えてください。
発見とフィルタリング
一つのリクエスト、そしてメモリ内でのフィルタリング。サイトからはまだ何も取得されていません:
python
import gzip, re, urllib.request
SITEMAP = "https://www.scrapeless.com/sitemap.xml"
LOC_RE = re.compile(r"<loc>\s*([^<\s]+)\s*</loc>", re.I)
def fetch_xml(url: str) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "sitemap-demo/1.0"})
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
if url.endswith(".gz") or raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
return raw.decode("utf-8", errors="replace")
xml = fetch_xml(SITEMAP)
is_index = "<sitemapindex" in xml.lower()
urls = LOC_RE.findall(xml)
print(f"document type: {'sitemap index' if is_index else 'url set'}")
print(f"total <loc> entries: {len(urls)}")
blog = sorted({u for u in urls if "/en/blog/" in u})
print(f"filtered to /en/blog/: {len(blog)}")
print(f"first: {blog[0]}")
text
document type: url set
total <loc> entries: 7,577
filtered to /en/blog/: 597
first: https://www.scrapeless.com/ja/blog/10-best-no-code-web-scrapers
これがこのアプローチの全ての議論です:7,577のURLが列挙され、597の関連するものに絞り込まれ、コストは1つのリクエストです。リンクをたどるクローラーは同じリストに達するために数百のページをフェッチしなければならなかったでしょうし、たまたま通りかかったパスからリンクされていないものは逃してしまったでしょう。
そのコードには重要な3つの詳細があります。
gzipチェックはファイル拡張子を信用するのではなく、マジックバイトを検査します。なぜなら、多くのサーバーが.xml URLからgzip圧縮されたコンテンツを返すからです。探しているシグネチャは、GZIPファイル形式仕様書で定義されたヘッダーです。
sorted({...})はセット内包表現であるため、重複したURLはカウントする前に消えます。サイトマップには重複が含まれていることがあり、特にサイトが複数のジェネレーターから構成されている場合には特にそうです。
<sitemapindex>を検出することは重要です。なぜなら、インデックスがあると、収集した<loc>の値は他のサイトマップであり、ページではないからです。is_indexが真であれば、それらのすべてをフェッチし、ページURLとして扱う前に繰り返します。
明示的な予算を設定し、その後取得する
発見は安いが、取得は高い。取得するページの数はコード内の定数であるべきであり、ループの実行時間によって生じる性質ではありません。
python
import json, os, re, urllib.request
SITEMAP = "https://www.scrapeless.com/sitemap.xml"
API = "https://api.scrapeless.com/api/v2/unlocker/request"
LOC_RE = re.compile(r"<loc>\s*([^<\s]+)\s*</loc>", re.I)
TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.S | re.I)
def fetch_sitemap(url: str) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "sitemap-demo/1.0"})
with urllib.request.urlopen(req, timeout=60) as r:
return r.read().decode("utf-8", errors="replace")
def fetch_page(url: str) -> str:
body = json.dumps({
"actor": "unlocker.webunlocker",
"input": {"url": url, "js_render": False},
}).encode()
req = urllib.request.Request(API, data=body, headers={
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
"Content-Type": "application/json",
})
with urllib.request.urlopen(req, timeout=180) as r:
return json.loads(r.read())["data"]
urls = sorted({u for u in LOC_RE.findall(fetch_sitemap(SITEMAP)) if "/en/blog/" in u})
print(f"フィルタリングと重複排除後の候補数: {len(urls)}")
BUDGET = 3
print(f"クロール予算: {BUDGET}")
for url in urls[:BUDGET]:
html = fetch_page(url)
m = TITLE_RE.search(html)
title = re.sub(r"<[^>]+>", "", m.group(1)).strip() if m else "(タイトルなし)"
print(f" {url.rsplit('/', 1)[-1]} -> {title}")
text
フィルタリングと重複排除後の候補数: 597
クロール予算: 3
10-best-no-code-web-scrapers -> 2025年のデータ抽出のためのノーコードWebスクレイピングのベスト10
20-ways-for-web-scraping-without-getting-blocked -> ブロックされずにWebスクレイピングを行う20の方法
403-web-scraping -> Webスクレイピングにおけるエラー403: 禁止されたリクエストを修正するための10の簡単な解決策
BUDGETはスライスに適用される命名定数です。それは意図的なものです。停止条件が「リストが無くなった」となるクローラーは、数百のURLを扱う場合と数万のURLを扱う場合で非常に異なる振る舞いをし、その違いは実運用時にのみ現れます。
js_renderがFalseに設定されていることに注意してください。これらのページは最初のHTMLで<title>を提供するため、レンダリングを行うと、既にそこにあるフィールドに対してコストと遅延が追加されます。スクリプトによってDOMに必要なデータが書き込まれる場合にのみレンダリングを行い、デフォルトでは行わないでください。Scrapeless Universal Scraping APIはその両方を処理し、JSレンダリングガイドではどのケースにいるかを判断する方法を説明しています。
URLのカウントはスナップショットです。サイトマップはライブドキュメントであるため、異なる日に再実行することで異なる数が返されます。このため、リストをハードコーディングするのではなく、各実行時に読み取ることが重要です。
トラブルシューティング
/sitemap.xmlが404を返す。 robots.txtを読んで、他の場所を指しているかもしれないSitemap:行を探してください。一部のサイトはそこにのみ公開しているものがあります。または、全く公開していない場合もあり、その場合はリンク追跡が残された選択肢です。
有効に見えるファイルからパーサーがゼロのURLを返す。 応答がgzippedであり、解凍されていないか確認してください - 上記のマジックバイトチェックは一般的なケースを処理します。また、XMLを取得していることを確認し、リダイレクトによって生じる404エラーページではないことも確認してください。
カウントが異常に低く見える。 ドキュメントはおそらくサイトマップインデックスです。すべての<loc>は別のサイトマップを取得するものであり、ページURLは一段階下にあります。
エントリがもはや存在しないURLを指している。 サイトマップは生成されており、生成者は遅れがちです。リストを保証ではなく候補として扱い、いくつかのエントリが404を返すことを予想してください。
何かをクロールする前にサイトの利用規約とrobots.txtを確認し、収集を公開ページに制限し、予算をサイトが快適に提供できる範囲内に保ちましょう。サイトマップは、発行者がインデックスを持たせたいと思っているものを教えてくれます;それは、あなたができるだけ速くすべてを取得するための許可ではありません。
結論
サイトマップ駆動型クロールは、クローラーの最も無駄な部分 - 発見 - を単一のリクエストに置き換えます。このマニュアルでは、7,577のURLを列挙し、重要な597に絞り込み、3つだけを取得し、境界をコードに書き込むことによって実現しました。これは偶然に任せるのではなく、明示的に決定するものです。
サイトマップを超えた一般化の習慣は、順序付けです。最初に列挙し、まだ無料の間にフィルタリングと重複排除を行い、取得する量を明示的に決定し、それからリクエストを開始します。問題を抱えることになるほとんどのクローラーは、この手順を逆の順序で行います。
Scrapelessの無料プランで始めるを使って、クローリングが許可されているソースに対してフェッチステップを実行し、定期的なジョブのサイズを見積もる際にはScrapelessの料金プランを確認してください。
FAQ
Q: ウェブサイトのサイトマップはどうやって見つけるのですか?
まずは/sitemap.xmlを試し、その後robots.txtを読んでSitemap:ディレクティブを確認してください。これはRFC 9309で、サイトマップを広告するための正式な場所として定義されています。大規模なサイトは、単一のフラットファイルではなく、いくつかのセクションサイトマップを指し示すインデックスをそのパスに公開することがよくあります。
Q: サイトマップインデックスとurlsetの違いは何ですか?
urlsetはページのURLをリストします。サイトマップインデックスは他のサイトマップファイルをリストします。両者は<loc>要素を使用しているため、<loc>だけを見るパーサーは、ページがあると思っている間にサイトマップのURLを返すことになります。<sitemapindex>のルート要素があるかどうかを確認し、見つけたら再帰的に処理してください。この分割は、プロトコルが単一のファイルを50,000のURLまたは50MBで制限しているために存在します。
Q: サイトマップからのクローリングはリンクをたどるより良いですか?
発見のためには、通常そうです:1回のリクエストで出版社が明示的にリストしたURLを返すため、中間ページを取得する必要はありません。トレードオフはカバレッジです—サイトマップには生成者が含めることにしたものしか含まれていないため、存在するが省略されたページは見えなくなります。すべての広告されたものよりも、すべて到達可能なものが必要な場合は、リンクをたどる方が勝ります。
Q: サイトマップからクローリングする際にJavaScriptをレンダリングすべきですか?
最初のHTMLに欲しい情報がない場合にのみ実行してください。このマニュアルにあるページは<title>をサーバーサイドで提供しているため、js_renderはFalseに設定されており、フェッチが安価で迅速です。最初に1ページをチェックしてください:データがビューソースに表示される場合、レンダリングは必要ありません。
Q: 一度の実行で何ページをフェッチすべきですか?
明示的に数値を設定し、候補リストをその数にスライスしてください。適切な値はサイトの容量とあなたのニーズによりますが、重要なのは、それがリストの長さの副作用ではなく、コードで記録された決定であるということです—これが小さなセクションの仕事と全サイトの仕事を非常に異なるイベントに変える要因です。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



