powered by TechFeed
表示モード
ハウツー

HTMLをそのままLLMに渡すな — Python製AIスクレイパーが「クリーニング→Markdown変換→質問」の4ステップでノイズを排除する仕組み

8月14日、KDNuggetsが「How to Build a Simple AI Web Scraper with Python」と題した記事を公開した。PythonとLLMを組み合わせてWebページから必要な情報だけを抽出するAIスクレイパーの実装方法を、コード付きで解説している。

8月14日、KDNuggetsが「How to Build a Simple AI Web Scraper with Python」と題した記事を公開した。PythonとLLMを組み合わせてWebページから必要な情報だけを抽出するAIスクレイパーの実装方法を、コード付きで解説している。


ポイント:「全文を渡さない」ことがLLM活用の肝

従来のScrapyBeautifulSoupを単体で使うWebスクレイパーはHTMLの生テキストをそのまま取得するが、これをLLMに丸投げすると、ナビゲーションリンク・ボタン・フッター・スクリプト等のノイズがそのままトークンを消費する。トークン消費はAPI利用コストに直結するため、LLMに渡す前の前処理が実用上の鍵となる。

この記事が提案するアプローチは、HTML→クリーニング→Markdown変換→LLMへ質問という4ステップのパイプラインだ。FireCrawlのようにサービス側で前処理を担うツールも存在するが、本記事はPythonライブラリのみで同等の前処理を自前実装する構成をとる。ページ全体を要約させるのではなく、ユーザーの質問に対して必要な情報だけを返させる設計が核心にある。


パイプラインの構成

使用するライブラリは以下の通り:

pip install requests beautifulsoup4 markdownify openai ftfy python-dotenv
ライブラリ 役割
requests Webページの取得
BeautifulSoup HTMLのパースとノイズ除去
markdownify HTML→Markdown変換
openai LLMへの問い合わせ
ftfy 文字コード崩れの修正
python-dotenv APIキーの安全な管理

モデルにはgpt-5.4-nanoを使用する(※記事執筆時点のモデル名。実際の利用時は最新のOpenAIモデル一覧を確認されたい)。記事では「このタスクに大規模な推論モデルは不要」と明示しており、コスト最適化の観点からも小型モデルで十分と判断している。


ステップ1:HTMLのクリーニング

最も実装が面白いのがこのステップだ。BeautifulSoupでscriptstylenavheaderfooterformbuttonといったタグを一括除去した後、さらにクラス名やID属性に含まれるノイズワードでフィルタリングする。

noise_words = [
    "cursor", "modal", "popup", "floating", "signup", "login",
    "cookie", "banner", "navbar", "menu", "footer", "header",
    "subscribe", "newsletter", "loading", "wait", "success",
    "auth", "w-nav", "w-form"
]

tags_to_remove = []
for tag in soup.find_all(True):
    if tag.attrs is None:
        continue
    class_text = " ".join(tag.get("class", [])).lower()
    id_text = str(tag.get("id", "")).lower()
    if any(word in class_text or word in id_text for word in noise_words):
        tags_to_remove.append(tag)

for tag in tags_to_remove:
    tag.decompose()

popupcookieといった文字列をクラス名・IDから検出して除去する発想は、サイト固有のDOM構造に依存しないため汎用性が高い。XPathやCSSセレクタでサイトごとに抽出ロジックを書く従来手法と比べ、メンテナンスコストを抑えられる点が実用上の強みだ。


ステップ2:Markdown変換とトークン削減

クリーニング済みのHTMLをmarkdownifyでMarkdownに変換する。見出しスタイルはATX形式(###)を指定し、画像のMarkdown記法は正規表現で除去する。さらに連続する空行を2行以内に圧縮し、「Start for free」「Contact Sales」といったCTA(Call to Action)テキストをスキップリストで除外する。この段階でLLMに渡すトークン数を大幅に削減できる。

HTMLをそのままLLMに渡す場合と比べたトークン削減効果は、ページの構造やノイズの多さによって変わるが、記事ではこのクリーニング+Markdown変換の組み合わせがコスト面で有意に効くと説明している。


ステップ3:LLMへの質問

プロンプト設計が実用上の要点だ。「ページのMarkdownのみを使用して回答せよ」「存在しない情報を補完するな」「ナビゲーションリンクやCTAは無視せよ」という制約を明示することで、ハルシネーション(LLMが事実ではない情報を生成する現象)を抑制している。

def answer_query_from_page(markdown_text, user_query):
    prompt = f"""
You are an AI web scraping assistant.
...
- Use only information from the webpage Markdown.
- Do not invent missing details.
- If the page does not contain the answer, say: "The page does not contain this information."
"""
    response = client.responses.create(model=MODEL_NAME, input=prompt)
    return response.output_text

「情報がない場合はその旨を返答せよ」という指示を含めている点も重要で、LLMが不確かな情報を推測で補うリスクを明示的に排除している。


ステップ4(統合):4行で完結するパイプライン

上記3ステップを束ねる統合関数がパイプラインの全体像だ。

def ai_web_scraper(url, user_query):
    raw_html = fetch_page(url)
    cleaned_html = clean_html(raw_html)
    markdown_text = html_to_markdown(cleaned_html)
    answer = answer_query_from_page(markdown_text, user_query)
    return answer

実際の使用例として、企業サイトに「この会社は何をしているか?」と質問したり、料金ページに「価格体系を教えてくれ」と問い合わせるデモが示されている。結果はMarkdownファイルとして保存することも可能だ。


コスト面の注意と採用判断の基準

記事はポジティブな面だけでなく、LLMのAPI呼び出しコスト、サーバー運用費、スクレイパーのメンテナンスコストについても言及している。小さい専用ソリューションで十分な問題も多いが、コストの見通しを立てた上で採用を判断するよう促している点は実践的だ。

用途・規模によってはFireCrawlのようなマネージドサービスや、Scrapyと後段LLM処理を組み合わせた構成も選択肢になる。本記事のアプローチは「小規模・自前管理・コード全体の把握を優先したい」ケースに特に向いている。


詳細はHow to Build a Simple AI Web Scraper with Pythonを参照していただきたい。