Scrapelessを使った段階的ウェブスクレイピングパイプラインの構築方法
Senior Web Scraping Engineer
TL;DR:
- 条件付きリクエストは変更されていないページを
304に変え、ボディは0バイトになります — 完全なレスポンスは50,388バイトで、10回の条件チェックが4.21秒、10回の完全フェッチが5.52秒かかりました。 - コンテンツハッシングは変更を検出しますが、帯域幅を節約しません:バリデータのないページの2回目のフェッチは、ハッシュが比較される前に11,064バイトを転送しました。
- レコードごとのフィンガープリンティングが書き込みを縮小します。1つの移動価格は20レコードのページをダウンストリームに1行だけ書き込むようにしました。
- 関連するフィールドだけをフィンガープリンティングし、無関係のフィールドが変更されるとすべてが汚れているとマークされます。
- HTTPバリデータはサーバーが送信したドキュメントを説明するため、レコードがクライアントサイドレンダリングを介して到着する瞬間に役に立たなくなります。
304は、前回の実行でバリデータが保存されている場合にのみ利用可能で、状態テーブルが最適化ではなくパイプラインの一部になります。- レンダリングされたページに対して増分クロールを行うには、Scrapelessの無料プランを利用してください。
ほとんどのスケジュールされたスクレイパーは、すべてを再ダウンロードし、すべてを再解析し、すべての行を書き換え、その後成功を報告します。それはカタログが増えるまで機能しますが、その時点で日々のジョブは、昨日のデータがまだ昨日のデータであることを確認するためにほぼすべての時間を費やしています。
増分スクレイピングは逆のアレンジです:何が変わったのかを尋ね、答えが「はい」の場所でのみ作業を行います。この質問は3つのレベルで尋ねることができ、それぞれ異なるもの — 帯域幅、解析、または書き込み — を節約します。各レイヤーは同じライブカタログページに対して測定されたため、トレードオフは形容詞ではなく数字で表示されます。
Pipeline at a Glance
| レイヤー | 質問 | メカニズム | 測定結果 |
|---|---|---|---|
| HTTP | ドキュメントは変更されましたか? | If-None-Match / If-Modified-Since |
304、0バイト 対 50,388 |
| ドキュメント | バイトは変更されましたか? | ボディのSHA-256 | 検出されたが、11,064バイトが依然として転送 |
| レコード | どの行が変更されましたか? | レコードごとのフィールドフィンガープリンティング | 書き込まれた20行のうちの1行 |
フローは チェック → 変更された場合のみフェッチ → 抽出 → フィンガープリンティングを比較 → 違いのみを書き込む です。レイヤーは積み重なります:HTTPチェックは最も安価で、最も利用可能性が低く、レコードチェックは常に利用可能で、完全なフェッチのコストがかかります。
チェックをあまり頻繁に行わないことは同じ問題の別の半分です。ロボット排除プロトコルは、サイトがクロールしてほしいものを示すところであり、増分設計はスクレイパーが遅いペースを守ることを可能にするものです — 各チェックはフル転送ではなく、ラウンドトリップのコストがかかります。
Layer 1: Let the Server Answer
HTTPバリデーターは、その表現が変更されたかどうかについてサーバー自身の意見です。2つのタイプがあります:ETag、不透明なバージョントークン、および Last-Modified、タイムスタンプです。最初のレスポンスはこれらを含んでいます。
python
import requests
URL = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
first = requests.get(URL, timeout=30)
first.raise_for_status()
etag = first.headers["ETag"]
print(f"first GET {first.status_code} {len(first.content)} bytes ETag={etag}")
second = requests.get(URL, headers={"If-None-Match": etag}, timeout=30)
print(f"second GET {second.status_code} {len(second.content)} bytes")
print(f"bytes avoided: {len(first.content) - len(second.content)}")
text
first GET 200 50388 bytes ETag=W/"63e40de8-c4d4"
second GET 304 0 bytes
bytes avoided: 50388
304 にはボディが全く含まれていません。条件付きリクエストメカニズムは、クライアントがすでに持っているコピーを保持するように定義されているため、スクレイパーはそのコピーを保持している必要があります。
Last-Modified は異なるヘッダーを通じて同じように機能します:
python
third = requests.get(URL, headers={"If-Modified-Since": first.headers["Last-Modified"]}, timeout=30)
print(first.headers["Last-Modified"], "->", third.status_code, len(third.content), "bytes")
text
Wed, 08 Feb 2023 21:02:32 GMT -> 304 0 bytes
両方が存在する場合は ETag を優先してください。HTTPのセマンティクス仕様は Last-Modified を1秒解像度のタイムスタンプにしているため、同じ秒内での2回の変更は区別できませんが、エンティティタグは任意の編集で変更することができます。
節約は現実的ですが、それが全体の実行ではありません:
text
10 conditional GETs 4.21s
10 full GETs 5.52s
条件チェックは1.31倍速く実行され、492 KBが転送されませんでした。リクエストには依然としてラウンドトリップのコストがあります — 消失するのはボディであり、接続ではありません。
Layer 2: Hash the Document When the Server Says Nothing
多数のターゲットがバリデーターを送信しません。したがって、ドキュメントが変更されたかどうかを知る唯一の方法は、それを取得して確認することです。暗号学的ダイジェストは、その比較のための標準的なツールです — SHA-256標準は、任意の1バイトの違いが無関係なダイジェストを生成する一定の幅の値を提供するため、平等性は信頼できる「何も動かなかった」という信号になります。
python
import hashlib
import requests
q1 = requests.get("https://quotes.toscrape.com/", timeout=30)
q1.raise_for_status()
print("ETag:", q1.headers.get("ETag"), " Last-Modified:", q1.headers.get("Last-Modified"))
q2 = requests.get("https://quotes.toscrape.com/", timeout=30)
h1 = hashlib.sha256(q1.content).hexdigest()
h2 = hashlib.sha256(q2.content).hexdigest()
print(f"sha256 run 1: {h1[:16]}...")
print(f"sha256 run 2: {h2[:16]}...")
print(f"unchanged: {h1 == h2} bytes still transferred: {len(q2.content)}")
text
ETag: None Last-Modified: None
sha256 run 1: efdc2605a2062dce...
sha256 run 2: efdc2605a2062dce...
unchanged: True bytes still transferred: 11064
このレイヤーが何を購入し何を購入しないかに注意してください。ハッシュは正しく変更がないことを報告し、11,064バイトがネットワークを横断しました。ドキュメントハッシングは解析、データベースの書き込み、ダウンストリームのアラート、および再埋め込みを節約しますが、決して帯域幅を節約しません。
ここには、数字が表示されないという誤検出の問題もあります。セッショントークン、回転広告、またはレンダリングされたタイムスタンプを含むページは、データは同一であっても、毎回異なるハッシュを生成します。生の本文ではなく抽出されたレコードのハッシュを計算することで、信号が安定する次のレイヤーが実現します。
レイヤー3: レコードのフィンガープリンティング
上記のレイヤーはドキュメントに関する質問に答えます。パイプラインが通常知っておくべきことは、どの行を書き込む必要があるかです。
python
import json, hashlib
def fingerprint(record):
payload = json.dumps({k: record[k] for k in ("price", "rating")}, sort_keys=True)
return hashlib.sha256(payload.encode()).hexdigest()[:16]
すべてのレコードではなく、選択したフィールドのサブセットのフィンガープリンティングを行うことが、このプロセスを機能させる決定です。自動的に変更されるフィールド(ビューカウント、"最終更新" タイムスタンプ、ランク付きリスト内の位置など)を含めると、すべてのレコードは毎回汚れているように見えます。
各レコードごとに1つのフィンガープリントを保存し、次のスクレイプと比較します:
python
known = {title: (fp, price) for title, fp, price in conn.execute("SELECT title, fp, price FROM snap")}
new, changed, same = [], [], 0
for record in second_pass:
previous = known.get(record["title"])
if previous is None:
new.append(record)
elif previous[0] != fingerprint(record):
changed.append((record["title"], previous[1], record["price"]))
else:
same += 1
1つの価格を変更して実際の動きを示すライブ20レコードページに対して:
text
parsed 20 records from the live page
stored baseline: 20 fingerprints
example fingerprint: 'Sharp Objects' -> e974692e9d935048
unchanged 19 | changed 1 | new 0
CHANGED A Murder in Time £16.64 -> £99.99
rows written downstream: 1 of 20
20ではなく1行が書き込まれます。毎日の現実がほとんど何も動かないカタログでは、その比率がフィンガープリンティングの全ての主張です:書き込み量はカタログサイズではなく、変更率を追跡します。
フィンガープリントと同時に前の値を保持することが、検出を使用可能なイベントに変えるのです。£16.64 -> £99.99は価格変更レコードです;汚れフラグは、何かが起きたことを示すヒントに過ぎません。
クライアントサイドでレンダリングされるページに対してインクリメンタルクローリングを実行していますか? Scrapeless無料プランは、基準を構築し、最初のいくつかの差分をカバーするための十分なリクエストを提供します。
HTTPレイヤーの適用が止まる場所
バリデーターは、サーバーが送信したドキュメントを説明します。レコードがクライアントサイドレンダリングを介して到着すると、そのドキュメントはアプリケーションシェルであり、そのETagはカタログではなく、シェルのデプロイを追跡します。
結果は特定のものです:ページは304を返すことができますが、その背後の価格はすべて動いています、なぜならシェルは実際には変更されていないからです。ブラウザでコンテンツが組み立てられるすべてのターゲットは、レコードレイヤーでチェックする必要があり、ユニバーサルスクレイピングAPIを使用してレンダリングしてから比較します。
これは、ページが呼び出す内部JSONエンドポイントにも当てはまります。これらはしばしばバリデーターを送信し、送信される場合、トップレイヤーは実際にレコードを持つペイロードに対して再び機能します。
ステートの保存
これらのすべては、最後の実行で学習したことを保持する場所がなければ機能しません。パイプラインが必要とするステートは小さいです:
| カラム | 目的 |
|---|---|
url |
チェックした内容 |
etag / last_modified |
条件ヘッダーとして再生された |
body_sha256 |
バリデーターが存在しない場合のドキュメントレベルの比較 |
checked_at |
最後の確認が行われた時 |
レコードごとに、テーブルにはキー、フィンガープリント、および報告したい以前の値が保持されます。両方とも同じデータベース内のスクレイピングデータと並んで配置され、ETLパイプラインの形状は変更されません — 抽出の前にチェックステップが追加されます。
見逃しがちな運用上の注意点があります:ETagは、それを発行したURLにのみ有効です。異なるクエリストリングやページネーションのバリアントに対して保存されたタグを再生すると、200と完全なボディが生成され、これは誤動作ではなく正しい動作です。
結論
インクリメンタルスクレイピングは三つの質問であり、どの質問をしているかを知ることが、何を保存するかを決定します。HTTPバリデーターはボディを保存します — 50,388バイトが0バイトの304になりました。ドキュメントハッシュは解析と書き込みを保存しますが、帯域幅は決して保存しません。なぜなら、11,064バイトは比較の前に到着するからです。レコードフィンガープリントは書き込みを保存し、20レコードのページを1行にまとめました。
サーバーがバリデーターを提供する場合はバリデーターから始め、未加工のボディではなく抽出されたレコードをハッシュ化することに戻り、実際に気にするフィールドだけをフィンガープリンティングしてください。料金は、変更されていないものが発生しなくなった後の残りのフェッチのコストを示します。
動いていないページを再スクレイピングするのをやめる準備はできましたか? Scrapeless無料プランから始める そして次の実行が比較する基準を構築してください。
FAQ
Q: インクリメンタルウェブスクレイピングとは何ですか?
前回の実行以来変更された部分のみをスクレイピングし、ターゲット全体を再収集するのではありません。これは、フェッチまたは書き込みの前に実行されるチェックとして実装されます:条件付きHTTPリクエスト、ドキュメントハッシュ比較、またはレコードごとのフィンガープリンター比較です。ここでの測定された効果は、HTTPレイヤーで50,388バイトを回避し、レコードレイヤーで20行中19行をスキップしたことでした。
Q: スクレイパーでETagをどのように使用しますか?
レスポンスからETagヘッダーを保存し、次のリクエストで同じURLにIf-None-Matchとして返します。何も変更されていないと合意したサーバーは、空のボディで304で応答し、これがパイプラインの残りをスキップする信号となります。このタグは正確なURLに結び付けられているため、異なるクエリ文字列に対して再生された保存されたタグは通常の200を返します。
Q: サイトがETagやLast-Modifiedを送信しない場合はどうなりますか?
代わりにハッシュを取得して比較します。抽出されたレコードを生のHTMLではなくハッシュ化します — セッショントークン、広告、またはレンダリングされたタイムスタンプが変化すると生ボディハッシュが変わり、データが同一であってもページが汚れていることを示します。これにより、どちらにせよ帯域幅が消費されます;節約はパース、書き込み、そしてダウンストリームの何かにあります。
Q: 全レコードをハッシュ化すべきですか、それとも特定のフィールドだけですか?
特定のフィールドです。全レコードハッシュにはページが持つ可能性のあるすべてのものが含まれるため、ランク位置や「最後に表示された」カウンターは、すべての実行で各レコードを変更されたように見せます。動きが重要なフィールドのみをフィンガープリンティングすることが、上記の安定した19-unchangedの結果を生み出しました。
Q: ページが304を返す間に実際にデータが変わることはありますか?
はい、クライアントレンダリングされたページではあります。バリデーターはサーバーが送信したHTMLドキュメントを説明し、シングルページアプリケーションの場合はレコードではなくシェルになるため、タグはシェルのデプロイを追跡します。これらのターゲットは、レンダリング後のレコードレイヤーで比較する必要があります、またはデータを持つ内部JSONエンドポイントに対してです。
Q: インクリメンタルスクレイピングは実際にどれくらいの節約をもたらしますか?
どのレイヤーが応答するかによります。上記の測定では、条件付きリクエストがレスポンスボディを完全に削除し、10回のチェックで1.31倍速く実行され、レコードレイヤーは20アイテムのうち1つが動いたページで書き込みを95%削減しました。往復自体はすべての場合において残るため、節約はページサイズと変更率に比例し、リクエスト数には比例しません。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



