PageSift は Web ページや PDF・Office 文書などを Markdown と構造化データに変換する、Rust 製のコンテンツ抽出エンジンです。1.2 では新しい crate pagesift-itna を追加しました。itna(名前は "anti" の逆さ読みです)は、アクセス障壁 —— レート制限、ログイン壁、ブロック、JavaScript チャレンジ —— に遭遇したときの「回復経路」を担います。
最初に立場をはっきりさせておきます。itna はデフォルトでオフです。有効化は設定ファイルに [itna] enabled = true と明示した場合だけ。そしてこの記事で書くのは「どうやって壁を破るか」ではなく、むしろ逆の話です。いつ諦めるか、いつ一歩だけ進むか、その規律をどうコードに固定したか —— それが itna の本体です。
1. いきなりリトライしない。まず分類する
障壁に当たったときの最悪の初手は、反射的にブラウザを起動して再試行することです。相手が「来るな」と言っているのか、「少し待て」と言っているのか、「本人確認が必要」と言っているのかで、正しい対応はまったく違います。
itna はまず HTTP 実行の結果を分類します(pagesift-itna の classify)。
| 結果 | 分類 |
|---|---|
| 429 / 503 + Retry-After | Throttled(制限中) |
| ログイン壁 / 401 | Login |
| 403 / ブロック検知 | Blocked |
| チャレンジ / CAPTCHA 検知 | Challenge |
そして分類ごとに、次に取れる行動を型で縛ります。エスカレーション(ブラウザ実行への移行)を許可するのは Blocked と Challenge のときだけです。Throttled と Login は、コード構造の時点でブラウザに進めません。テストにも throttling_and_login_never_escalate という、そのままの名前の断言があります。
これは設計として当然の判断です。429 は「ペースを落とせ」という合図であって、ブラウザで押し切る理由にはなりません。ログイン壁の向こう側は認証情報の問題で、レンダリングの問題ではありません。
2. エスカレーションの規律:最大で一度だけ、同じ出口で
Blocked / Challenge の場合でも、無制限に粘ることはしません。試行管理(Attempts)の上限は生成時に固定されています。
Attempts::new(timeout, 2, 1)
総試行は最大 2 回、そのうちブラウザ試行は最大 1 回。しかも 2 回の試行は**同じ egress(出口)**を使います。IP を変えて何度も叩く、という挙動は構造的に存在しません。
さらに、分類が Blocked / Challenge でもエスカレーションが許可されない条件があります(browser_allowed の門番)。
-
engine = "http"と明示されたリクエスト - 任意のカスタムヘッダを持つリクエスト
- 認証情報を含む「センシティブ」なリクエスト(後述)
- スクリプト発見モードのリクエスト
つまりエスカレーションは「素の公開ページ取得」に対してだけ開かれた、狭い経路です。
3. ネットワーク予算は共有し、尽きたら失敗する
ブラウザに進んだ後も、好きなだけリクエストを飛ばせるわけではありません。itna は 1 リクエスト全体に 1 つのネットワーク予算(NetworkBudget)を持たせます。内訳はナビゲーション 16 回、リソース 128 回、そして HTTP 側とブラウザ側で共有する単一の deadline です。
HTTP 側はリダイレクトのたびに、ブラウザ側は CDP のリクエスト傍受層でサブリクエストのたびに、この予算を消費します。尽きたらどうなるか —— こっそり続行するのではなく、エラーとして失敗します(fail-closed)。予算外の通信が「なかったこと」になる設計にはしていません。
そして全試行の軌跡 —— どのエンジンで実行し、どういう結果だったか、ブラウザ実行が何回あったか —— は、結果ドキュメントの metadata.pagesift.itna(失敗時はエラーの詳細内)に記録されます。回復経路を通ったかどうかは、後から必ず検証できます。
4. セッションは四つ組で隔離する
回復経路でブラウザを使う以上、セッション(Cookie 等の同一性)の扱いが安全の核心になります。itna のアイデンティティプールは、セッションを以下の四つ組をキーにリースします(pagesift-policy の IdentityKey)。
owner × origin × egress × fingerprint
リース取得時には、リクエスト先 URL の origin がキー内の origin と完全一致することも検証します。owner が違えば、同じサイトでもセッションは絶対に混ざりません。owner を持たない匿名リクエストはプールに入らず、使い捨てセッションになります。
そして共有そのものが禁止されるケースがあります。
- Cookie / Authorization / Proxy-Authorization ヘッダを含む、またはサイト認証情報が設定されたリクエスト(センシティブ判定)
- 任意のカスタムヘッダを持つリクエスト
センシティブ判定については、正直に書くと、当初の想定より実装のほうが厳しくなっています。「認証情報付きは共有しない」という方針を実装したら、結果としてカスタムヘッダが 1 つでもあれば共有もエスカレーションも両方止まる形になりました。安全側に倒れた結果なので、そのまま採用しています。
5. プロキシは「検証済みトンネル」でしか通らない
フィンガープリント付きトランスポートがプロキシを経由する場合、itna は対象ホストをローカルで解決し、SSRF チェックを通した IP アドレスだけをプロキシに渡します。プロキシ側で名前解決をさせる方式(SOCKS5 のリモート解決相当)も含め、対象が検証を通る前にトンネルを開くことはありません。socks5h のような指定でも解決はローカルで行います。
TLS の SNI と HTTP の authority には元のホスト名が残ります。プロキシが受け取るのは「検証済みの IP へのトンネル」だけです。
そして重要なのは失敗時の挙動です。プロキシやトンネルの確立に失敗したら、それはエラーとして返ります。通常のリクエストや直接接続への暗黙のフォールバック(ダウングレード)は存在しません。「プロキシが落ちたから素通しで取りに行く」は、この手の機能では最も起きてほしくない事故だからです。
6. Cookie の引き渡しは、正確な origin と Strict が前提
HTTP 実行からブラウザ実行へ移る際、HTTP 側で得た Cookie をブラウザへ引き渡す機能があります。ここも条件は厳しく固定しました。
- 引き渡し先は正確な origin に限定され、ホストだけが一致する別スキーム・別ポートへの持ち出しは傍受層で遮断します(唯一の例外は標準的な http:80 → https:443 への昇格のみ)
- インポートされる Cookie は一律
SameSite=Strictで設定します(セッションストアが SameSite を保持しないため、橋渡しした Cookie がクロスサイトのサブリソースに付いて出ていかないようにする措置です) - HTTP 側フィンガープリントの Chrome メジャーバージョンと、実行中ブラウザのメジャーバージョンが一致しない場合、引き渡し自体を拒否します
「だいたい同じサイトだから」という曖昧な引き渡しはしません。
7. やらないこと
itna の設計で一番時間をかけたのは、やることより「やらないこと」の線引きです。
ハードウェア同一性は偽装しません。 ブラウザ実行時にやっているのは自動化マーカーの除去だけです。起動引数で Blink の automation マーカーを消し、ドキュメント開始スクリプトで navigator.webdriver を undefined にします。正直に書くと、そのスクリプトにはもう 1 つだけ、window.chrome が存在しない場合に空のオブジェクトを補う処理があります。これはマーカー層の補完であって、UA や GPU、CPU コア数といったハードウェア・同一性パラメータの捏造はしていません。
CAPTCHA は解きません。 対話型 CAPTCHA ウィジェットのあるページは待たずに離脱し、ACCESS_BARRIER エラーに needsHuman: true を付けて返します。ソルバーは内蔵していませんし、今後もその予定はありません。解くべきなのは機械ではなく人間だ、という線引きです。なお、対話型でない JavaScript チャレンジについては、最大 8 秒(かつリクエストの timeout 以内)の有界待機をします。
ブラウザ実行は Chrome / CDP 系だけです。 通常のブラウザ描画では Lightpanda → moli → Chrome のエンジン連鎖を使いますが、itna の経路ではこの連鎖を意図的に切り、Chrome に固定します。理由はシンプルで、マーカー除去も、フィンガープリントも、Cookie の引き渡しも、すべて Chrome の意味論の上で検証しているからです。検証していないエンジンに回復経路を流すくらいなら、Chrome がない環境ではエラーを返すほうを選びました。
8. 証拠の範囲について、正直な注記
最後に、このリリースの証拠の範囲を明記します。
itna の挙動は、すべてローカルに構築した制御可能なフィクスチャに対するリリーステストで検証しています。実在の本番サイトや WAF に対する通過率は、計測していませんし、主張もしません。「どのサイトが通るか」を謳う代わりに、分類・規律・予算・隔離といった挙動の約束がコードとテストで固定されていることを示すのが、このリリースの証拠です。
ついでに書くと、README の速度・品質の数値は 1.1.0 時点の実測のままで、1.2 では再計測していません。速くなったとは言いません。
おわりに
itna は「反・○○」ではなく、"anti" を逆に読んだだけの名前の通り、発想も逆向きです。壁を破る道具ではなく、壁の前での振る舞いに規律を持たせるための仕組み —— 分類して、上げるのは一度だけ、予算が尽きたら止まり、同一性は混ぜず、通れなかったことは記録に残す。抽出エンジンが長く使われるには、通す力より先に、この種の自制が必要だと思っています。
- GitHub: https://github.com/YOLOW92/pagesift
- crates.io / npm でも公開しています。PageSift 本体は AI / LLM パイプライン向けに、Web・PDF・Office・メール・電子書籍などを Markdown と構造化データへ変換するエンジンです。