← すべての記事

入札モニタリング: スクレイパーが見落としている公示情報

EU基準値未満の入札モニタリングでは、多数の小規模ポータルをスクレイピングする必要があります。最もコストのかかる障害はリクエストの拒否ではなく、正常終了しながら何も保存されていない実行です。

216,000ユーロ。この金額未満の場合、欧州の市議会がサービスを発注する際に、EUの中央調達登録システムであるTEDへ入札情報を掲載する義務はありません。

つまり、中小規模の応札者が現実的に受注できる案件は別の場所にあります。発注元の自治体、地域組織、公共事業者、あるいは各省庁独自のポータルサイト上です。

課題

欧州委員会の集計によると、EU内の25万を超える公的機関が、工事、物品、サービスに年間約2兆6,000億ユーロを支出しています。EU指令が適用されるのは閾値以上の契約のみです。2026年1月1日以降、この閾値は中央政府の物品・サービス調達で140,000ユーロ、地方・地域当局で216,000ユーロ、工事請負で5,404,000ユーロに設定されています。これらを下回る案件には各国の国内規則が適用され、TEDへの掲載は任意となります。

入札監視がスクレイピング業務となる理由はここにあります。入札コンサルティング企業や建設会社向けにシステムを構築する場合、閾値以上の案件への対応は容易な部分にすぎません。残りは、機械による読み取りを想定して設計されていない膨大なロングテールのポータル群です。

北米で公共入札データを提供するBidPrimeは、2026年5月に米国における同様の状況を「数千ものワークフロー」と表現しました。自動アクセスを制限するポータル、アカウントやセッションを要求する調達システム、公示本文と分離された添付ファイル、予告なく変更されるURLなどが挙げられます。これらは米国特有の課題ではありません。

しかし、入札監視において最も深刻な障害は、アクセスを拒否するポータルではありません。それはログで検知できます。真に問題なのは、処理が正常終了(グリーン)しながらデータが取得できていないケースです。

自治体のポータルは、セッション切れに対してHTTP 200で通常ページを返すことがあります。検索結果テーブルが表示されるべき場所にメンテナンス通知がレンダリングされることもあります。昨日送信した検索クエリに対して、2ページ目がリクエスト元のcookieにしか存在しなかったために、再び1ページ目が返されることもあります。パーサーは0行を検出し、「本日の新規入札なし」と記録します。見逃した価格データとは異なり、見逃した入札案件を取り戻すことはできません。欠落に気付いた時点ですでに提出期限は過ぎており、応札者にとってその公示は何の価値も持ちません。

アプローチ: 各フェッチに取得内容の検証を義務付ける

スクレイピングが不要なものから始めます。TEDのAPIは公開通知への匿名アクセスをキー不要で提供しているため、基準額以上の案件はそこから取得します。英国では、GOV.UKによると、2025年2月24日以降、Find a Tenderがすべての発注機関が調達情報を公開する中央プラットフォームとなっています。中央レジストリがすでにカバーしている範囲を確認してから、それをミラーリングしているポータル用のパーサーを作成してください。

それ以外のすべてについては、主に3つのルールが役立ちます。

1. 空の結果も含め、正常な応答を定義する。 結果ページには列ヘッダー(ポータル独自の文言では「Closing date」など)が含まれます。正常なデータなしの日には、ポータル固有の空状態テキストが表示されます。それ以外の応答は、ゼロ件ではなく異常です。両方の文字列をrequestのvalidate.data.acceptリストに設定すると、セッション切れのページは正常な空結果として処理されず、requestが失敗扱いになります。FourAのエンジンは認識した検証ページを成功としてカウントしないようになっていますが、すべての自治体がタイムアウトページをどのように記述しているかまでは把握できません(この部分はユーザー側で指定する必要があります)。一致判定は単純な大文字・小文字を区別する部分一致であるため、再入力するのではなくページからテキストを直接コピーしてください。この仕組みについては、validateルールによる成否判定で解説しており、時系列データの観点からの同様の問題はブロックがデータポイントに見えるときで扱いました。

2. ポータルに表示される件数を確認する。 ほとんどの結果一覧には、一致する通知の件数が表示されます(「1-20 of 143」など)。その数値をパースし、保存した行数と比較してください。数値が異なる場合、status codeが何であれ、その実行は完了していません。

3. ルートを一度見つけたら、それを再利用する。 ページネーションやセッションが紐付くポータルでは、以降のページも同一アドレスおよび同一ブラウザからアクセスされることを前提としている傾向があります。FourAのようなプラットフォームでは、Autoが1ページ目に機能するルートを見つけ、そのセッション(成功した試行のproxy ID、User-Agent、cookie)を返します。それ以降のページはすべてその出口のSingleを経由して処理されます。これが本番環境のパスです。Autoはパスを探索するためだけに使用します。

import math, re, requests

API = "https://api.foura.ai/api"
H = {"X-API-Key": "pk_live_..."}
LIST = "https://tenders.example-council.gov/open?page={}"
PER_PAGE = 20

# A real answer is the results table or the portal's own empty-state line.
# An expired session or a maintenance notice served at 200 is neither.
CHECK = {"data": {"accept": ["Closing date", "No open tenders"]}}

def fetch(endpoint, body):
    r = requests.post(f"{API}/{endpoint}", headers=H, json=body).json()
    if r.get("error"):
        raise RuntimeError(r["error"])  # a gap to refill, not a quiet day
    return r

# Auto finds a route that works and hands back its session...
first = fetch("auto", {"url": LIST.format(1), "validate": CHECK})
proxy, ua = first["session"]["proxy"], first["session"]["userAgent"]
pages = [first["data"]]

# ...and Single replays it on the same exit for every other page.
m = re.search(r"\d+-\d+ of (\d+)", pages[0])
total = int(m.group(1)) if m else 0
for n in range(2, math.ceil(total / PER_PAGE) + 1):
    pages.append(fetch("single", {
        "method": "GET", "url": LIST.format(n), "proxy": proxy,
        "headers": [["User-Agent", ua]], "validate": CHECK,
    })["data"])

ポータルがサーバー側で検索状態を保持している場合は、同じheadersリストでセッションcookieを送信します。SingleはリダイレクトホップごとにすべてのSet-Cookieヘッダーを返します。入札書類は通常PDFとしてダウンロードリンクの背後に配置されていますが、Singleのリダイレクト制御とrawバッファモードにより、破損したテキストではなくバイトデータとして取得できます。もう1つのベストプラクティスは、各ポータルを独自のスケジュールでポーリングすることです。これらは小規模な自治体が運用する小さなサーバーであり、午後の時間帯に分散された20リクエストに比べ、1回のバーストで送られる20リクエストはずっと負荷が高く見えます。

結果

  • 収集された入札案件が25%増加。 スクレイピングサービス企業のGroupBWTは、手動のベースラインと比較して50以上の調達ポータルを監視しているクライアントについて、この結果を報告しています。「入札案件の総数が増えたからではなく、取りこぼしがなくなったため」とのことです。
  • 閾値以上の部分に対するスクレイパーは不要。 TEDの通知はキー不要のAPIから取得できるため、パーサーの開発工数はすべてAPIのないポータルに割り当てられます。
  • 通知が公開されている間に欠落を検出。 承認ルールに失敗したページはその日のうちにエラーを発生させ、ジョブは空のデータを保存する代わりにリトライを実行します。
  • ルート検出はページ単位ではなくポータル単位で1回。 Autoの検索は一度だけ実行され、以降のすべてのページ取得は検出された出口に対するSingleの呼び出しとなります。

最初の項目のみが測定された実績であり、残りの3つはこの構成による改善点を示しています。

主なポイント

入札監視において、保存する中で最もコストが高くつく行は「本日は新規案件なし」です。後から修正できない唯一のデータだからです。価格データなら翌日でも再取得できます。しかし、スクレイパーがセッション切れページを読み取っている間に締め切られた入札案件は、二度と戻ってきません。

そのため、ゼロ件という結果には相応の裏付けを持たせてください。テーブルやポータル自体の空状態メッセージ、一致する件数のいずれも表示できない一覧ページは、まだ何も情報を返していません。それを正常な回答として処理してしまうと、入札者は落札通知が出て初めて案件の存在を知ることになります。