Bingスクレイピング入門|PythonとRola IPで検索結果を取得
2026年10月12日 · ガイド · 約12分で読める
Bingの検索結果をスクレイピングすると、検索上位に表示されるページのタイトル、URL、スニペットを一覧にできます。PythonのRequestsとBeautifulSoupを使い、取得したHTMLから自然検索の結果を抽出してCSVとJSONに保存する方法を紹介します。SEOの定点観測や競合ページの調査など、同じ検索語の結果を比較したい場面で使える手順です。
この記事のポイント: 1件の自然検索結果を1行として扱い、広告やサイトリンクは分けます。ページ送りでは重複を確認し、検索語・取得時刻・取得元URLも保存します。地域別に比較するときは、言語設定と接続元IPの条件をそろえます。
コード例の検索語は「Python ドキュメント」、言語設定はja-JPです。掲載画面はpython documentationの英語表示を使った操作例で、検索結果の構造や確認する項目を示しています。
Bingの検索結果から取得するデータを決める
検索結果ページ(SERP)は、自然検索、広告、関連検索、Copilotの回答などから構成されます。すべてのリンクをまとめて抽出すると、検索順位を比較しにくくなります。まずは自然検索のタイトルリンクと説明文に対象を絞りましょう。
| 保存する項目 | 内容 | 分析するときの使い方 |
|---|---|---|
query |
検索語 | 同じ語句の結果を比較する |
page・position_on_page |
取得したページ番号とページ内の抽出順 | ページをまたぐ順位とは区別する |
title・url |
タイトルとリンク先URL | ページの特定、競合調査に使う |
snippet |
Bingが表示する説明文 | 空欄になる場合もある |
raw_href |
HTML内のリンク属性そのもの | リダイレクトURLの確認に使う |
source_url・fetched_at_utc |
実際の応答URLと記録時刻(UTC) | 取得条件や結果を後から確認する |
タイトルの下にある「Tutorial」などのサイトリンクは、親の結果と同列に数えません。また、検索画面に表示される概算件数は、そのままダウンロードできる件数ではありません。

「Sponsored」は広告の表示です。後述のコードではli.b_algoを対象にすることで、ページ内の見出しやリンクを無差別に収集することを避けます。

取得前にMicrosoftのサービス規約と利用条件を確認してください。コードはアクセス制限や確認画面を検出した場合に停止する構成です。
Pythonの実行環境を準備する
Python 3.10以降を使用します。macOSやLinuxでは、作業フォルダーを作って次のコマンドを実行します。
mkdir bing-scraper
cd bing-scraper
python3 -m venv .venv
source .venv/bin/activate
python -m pip install requests beautifulsoup4
WindowsのPowerShellでは、仮想環境内のPythonを直接指定できます。次の方法なら有効化スクリプトの実行ポリシーを変更する必要はありません。
mkdir bing-scraper
cd bing-scraper
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install requests beautifulsoup4
以降のpythonコマンドも、Windowsでは.\.venv\Scripts\python.exeに読み替えてください。
JupyterLabで処理を確認する場合は、同じ環境に追加でインストールします。通常のPythonファイルとして実行する場合は不要です。
python -m pip install jupyterlab
python -m jupyterlab
バージョンを調べるには、ターミナルで次のコマンドを実行します。ノートブックでは、ライブラリをインストールした仮想環境と同じカーネルを選びます。
python --version
python -m pip show requests beautifulsoup4

HTMLのどこに検索結果があるかを確認する
自然検索と広告のコンテナを区別する
ブラウザーでBingを開き、「Python ドキュメント」を検索します。Chromeではタイトルを右クリックして「検証」を選び、Elementsパネルでリンクのa、見出しのh2、外側のliを順に確認します。
今回のパーサーは、自然検索のコンテナli.b_algo内にあるh2 a[href]を読み取ります。次の画像で選択されているのは広告のタイトルで、DOMにもb_adが見えます。h2 aだけで検索すると広告も含まれるため、外側のコンテナまで確認するのがポイントです。

ブラウザーのDOMと応答HTMLを見比べる
NetworkパネルでDocを選び、ページを再読み込みします。searchリクエストのResponseを開き、b_algoや実際のタイトルを探してください。Elementsはブラウザーが構築したDOM、Responseはそのリクエストで返された内容です。詳しい操作はChrome DevToolsのNetworkパネルの説明で確認できます。
Pythonが受け取るHTMLは、ブラウザーの表示と一致するとは限りません。HTTPステータス、応答先のドメイン、自然検索の件数を確認し、セレクターが実際の応答に合っているかを調べます。


タイトル・URL・スニペットを抽出する仕組み
タイトルはh2 aのテキスト、URLはそのhrefから取得します。画面上の表示URLは省略される場合があるため、リンク先の完全なアドレスとしては使いません。
Bingのリンクには/ck/a形式のリダイレクトが含まれることがあります。コードではuパラメーターが認識できるa1形式の場合だけデコードし、元の属性値はraw_hrefに残します。解析できない場合はBingのURLを保持し、リンク先のサイトにはアクセスしません。
説明文は.b_caption pなどから抽出し、見つからなければ空文字にします。スニペットがなくても、タイトルとリンクがある結果は保存します。スニペットはBingの表示文であり、リンク先ページの全文やmeta descriptionと常に一致するものではありません。

ページ送りとCSV保存の考え方
次ページのリンクをたどる
first=11、first=21のようにURLを組み立てるより、受け取ったHTMLのa.sb_pagN[href]を読み取る方が、ページに含まれる検索条件を引き継ぎやすくなります。

次ページでも前のページと同じ結果が返ることがあります。コードはURLの並びが完全に同じページを検出して停止します。個々のURLが別のページに再登場した場合は、その出現位置を残します。サイト一覧を作る場合に限って、取得後にURL単位で重複を整理してください。

日本語を扱いやすい形式で保存する
CSVはUTF-8 BOM付きで保存し、Excelで日本語を読み込みやすくします。=や@などで始まる文字列には先頭にアポストロフィを付け、数式として扱われるのを防ぎます。元の文字列はJSONに保存します。
ページ内順位は、抽出できた自然検索の順番です。1ページ10件とは限らないため、「2ページ目の先頭=11位」とは決めつけず、pageとposition_on_pageを分けて記録します。
Bing検索結果を取得するPythonコード
次のコード全体をbing_scraper.pyという名前で保存してください。必要な関数と実行処理をすべて含んでいます。プロキシは--proxyを指定した場合だけ有効になります。
初期設定は1スレッド、最大3ページ、ページ間の待機は5秒です。この待機時間は例示値で、Bingの許可された取得頻度を示すものではありません。接続10秒・読み取り30秒のタイムアウトを設定し、403・429では自動再試行せず停止します。
import argparse
import base64
import csv
import getpass
import json
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import parse_qs, quote, urlencode, urljoin, urlsplit
import requests
from bs4 import BeautifulSoup
def bing_url(url):
p = urlsplit(url)
return (p.scheme == "https" and p.hostname in
{"www.bing.com", "cn.bing.com", "bing.com"}
and p.path == "/search" and not p.username and p.port in (None, 443))
def fetch(session, url):
# Validate every redirect before sending the next request.
for _ in range(5):
if not bing_url(url):
raise RuntimeError("Unexpected search URL; stopped.")
r = session.get(url, timeout=(10, 30), allow_redirects=False)
if r.is_redirect:
url = urljoin(r.url, r.headers["Location"])
continue
if r.status_code == 429:
retry_after = r.headers.get("Retry-After", "not provided")
raise RuntimeError(f"HTTP 429; Retry-After: {retry_after}; stopped.")
if r.status_code == 403:
raise RuntimeError(f"HTTP {r.status_code}; stopped.")
r.raise_for_status()
if "text/html" not in r.headers.get("Content-Type", ""):
raise RuntimeError("Expected HTML; stopped.")
r.encoding = "utf-8"
return r
raise RuntimeError("Too many redirects; stopped.")
def destination(href, base_url):
url = urljoin(base_url, href)
p = urlsplit(url)
if p.hostname in {"www.bing.com", "cn.bing.com", "bing.com"}:
if p.path == "/ck/a":
value = parse_qs(p.query).get("u", [""])[0]
if value.startswith("a1"):
try:
payload = value[2:]
decoded = base64.urlsafe_b64decode(
payload + "=" * (-len(payload) % 4)
).decode("utf-8")
if urlsplit(decoded).scheme in {"http", "https"}:
return decoded
except (ValueError, UnicodeError):
pass
return url if p.scheme in {"http", "https"} else ""
def parse_page(html, page_url, query, page_number):
soup = BeautifulSoup(html, "html.parser")
if soup.select_one('#b_captcha, iframe[src*="captcha"]'):
raise RuntimeError("Verification page; stopped.")
rows = []
stamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
for item in soup.select("li.b_algo"):
a = item.select_one("h2 a[href]")
if a is None or not a.get_text(strip=True):
continue
url = destination(a["href"], page_url)
if not url:
continue
snippet = item.select_one(".b_caption p")
if snippet is None:
snippet = item.select_one("p.b_lineclamp2, p.b_lineclamp3")
rows.append({
"query": query, "page": page_number,
"position_on_page": len(rows) + 1,
"title": a.get_text(" ", strip=True),
"url": url,
"snippet": snippet.get_text(" ", strip=True) if snippet else "",
"raw_href": a["href"],
"source_url": page_url, "fetched_at_utc": stamp,
})
if not rows:
raise RuntimeError("No organic rows; inspect HTML, do not assume end.")
link = soup.select_one("a.sb_pagN[href]")
next_url = urljoin(page_url, link["href"]) if link else None
if next_url and not bing_url(next_url):
raise RuntimeError("Unexpected next-page URL; stopped.")
return rows, next_url
def excel_safe(value):
if isinstance(value, str):
if value.lstrip().startswith(("=", "+", "-", "@")):
return "'" + value
if value.startswith(("\t", "\r", "\n")):
return "'" + value
return value
def save_rows(rows, folder):
if not rows:
raise RuntimeError("No data to export.")
folder = Path(folder)
folder.mkdir(parents=True, exist_ok=True)
(folder / "results.json").write_text(
json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
with (folder / "results.csv").open(
"w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows({k: excel_safe(v) for k, v in r.items()} for r in rows)
def configure_proxy(session):
scheme = input("Proxy scheme (http/https/socks5h): ").strip()
host = input("Proxy host only: ").strip()
port = int(input("Proxy port: "))
if scheme not in {"http", "https", "socks5h"}:
raise ValueError("Unsupported scheme.")
if not host or any(c.isspace() or c in "/@?#:[]" for c in host):
raise ValueError("Enter only the proxy hostname or IPv4 address.")
if not 1 <= port <= 65535:
raise ValueError("Invalid port.")
user = getpass.getpass("Proxy username (hidden): ")
password = getpass.getpass("Proxy password (hidden): ")
auth = quote(user, safe="") + ":" + quote(password, safe="")
proxy = f"{scheme}://{auth}@{host}:{port}"
session.proxies.update({"http": proxy, "https": proxy})
def scrape(query, max_pages=2, proxy=False):
if not 1 <= max_pages <= 3:
raise ValueError("This example supports 1 to 3 pages.")
folder = Path("runs") / datetime.now().strftime("%Y%m%d-%H%M%S-%f")
folder.mkdir(parents=True)
session = requests.Session()
session.trust_env = False
session.headers["Accept-Language"] = "ja-JP,ja;q=0.9"
url = "https://www.bing.com/search?" + urlencode(
{"q": query, "setlang": "ja-JP"})
all_rows, seen_pages, seen_signatures = [], set(), set()
try:
if proxy:
configure_proxy(session)
for page in range(1, max_pages + 1):
if url in seen_pages:
break
seen_pages.add(url)
r = fetch(session, url)
(folder / f"page-{page}.html").write_text(r.text, encoding="utf-8")
rows, next_url = parse_page(r.text, r.url, query, page)
signature = tuple(row["url"] for row in rows)
if signature in seen_signatures:
print("Repeated page; stopped.")
break
seen_signatures.add(signature)
all_rows.extend(rows)
print(f"Page {page}: HTTP {r.status_code}, {len(rows)} organic rows")
print("Response host:", urlsplit(r.url).hostname)
if not next_url or page == max_pages:
break
url = next_url
time.sleep(5)
finally:
session.close()
if all_rows:
save_rows(all_rows, folder)
print(f"Saved {len(all_rows)} rows to {folder}")
return all_rows, folder
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("query")
parser.add_argument("--pages", type=int, default=2)
parser.add_argument("--proxy", action="store_true")
args = parser.parse_args()
try:
scrape(args.query, args.pages, args.proxy)
except (requests.RequestException, RuntimeError, ValueError) as error:
# Never print proxy exception details, which can contain credentials.
message = str(error) if not isinstance(error, requests.RequestException) else type(error).__name__
raise SystemExit("Stopped: " + message)
実行してCSVを確認する
まず1ページで動作を確認します。
python bing_scraper.py "Python ドキュメント" --pages 1
続けて2ページまで取得する場合は、--pages 2を指定します。WindowsのPowerShellでは次のように実行できます。
.\.venv\Scripts\python.exe bing_scraper.py "Python ドキュメント" --pages 2
runsの下に日時付きのフォルダーが作られ、取得できたページのHTMLと、1件以上解析できた場合のresults.json・results.csvが保存されます。後続ページの取得でエラーが発生した場合も、それまでに抽出した結果の保存を試みます。実際に出力されたCSVの行数と、ターミナルのエラーメッセージを確認してください。
CSVの最初の3件について、タイトル、URL、スニペットが同じフォルダーのHTMLと対応しているか確認してください。JSONでは日本語がそのまま読めること、CSVでは列が崩れていないことも確認します。

Excelで文字化けする場合は「データ」→「テキストまたはCSVから」でUTF-8を指定して読み込みます。.xlsxが必要なら、その後でExcelブックとして保存します。
日本語の検索語でも、結果がすべて日本語になるとは限りません。setlang=ja-JPとAccept-Languageは言語に関する設定で、日本のIPアドレスを指定する設定ではありません。海外向けページを比較する場合も、検索語・言語・接続元の地域を組として固定すると結果を比較しやすくなります。
Rola IPを使う場合の設定
比較の目的に合わせてプロキシを選ぶ
同じ地域から定期的に確認するなら、Rola IPの静的住宅用プロキシが選択肢になります。複数地域の比較には、住宅用プロキシの利用可能な地域とセッション設定を確認します。
仕組みから知りたい場合は、住宅用プロキシとは何かを解説した記事が参考になります。

IPをリクエストごとに変えると、ページ間で検索条件が変わる可能性があります。ひとつの検索語を複数ページ取得するときは、比較したい地域とセッションをそろえましょう。プロキシは接続経路を提供するもので、検索順位の一致やアクセス成功を保証する機能ではありません。
管理画面の値を入力する
利用中の製品画面から、接続先ホスト、ポート、認証情報、必要な地域・セッション設定を取得します。ホスト欄にはホスト名またはIPv4アドレスだけを入力し、ポートは別に指定します。このコードはユーザー名・パスワード認証を使い、入力した認証情報は画面に表示しません。
地域やセッションの指定方法はプロキシの設定パラメーター、接続コードの設定はPythonでの接続方法で確認できます。
python bing_scraper.py "Python ドキュメント" --pages 1 --proxy
HTTPプロキシのポートならhttpを選びます。BingがHTTPSでも、HTTPプロキシのCONNECTトンネルを利用できるため、プロキシURLまでhttpsにする必要はありません。httpsはプロキシ自身とのTLS接続に対応したポートで使います。
SOCKS5を使う場合は、先に次をインストールし、入力時にsocks5hを選びます。末尾のhは、接続先の名前解決をプロキシ側で行う指定です。
python -m pip install "requests[socks]"
session.trust_env=Falseにより環境変数のプロキシ設定を使わず、コードで設定した接続先を使用します。環境変数のCA設定も自動では取り込まれないため、社内ネットワークで独自CAが必要な場合はsession.verifyにCAファイルを指定します。詳しくはRequestsのプロキシ設定を参照してください。
取得できない場合の確認ポイント
| 症状 | 確認する内容 | 対応 |
|---|---|---|
| HTTP 200なのに結果が0件 | 保存HTMLが検索結果か、確認画面か | HTML内のタイトルとセレクターを照合する |
| HTTP 403 | 対象サービスのアクセス条件 | 停止し、利用条件やリクエスト内容を確認する |
| HTTP 429 | 表示されたRetry-Afterとリクエスト頻度 |
自動再試行せず停止。値は秒数またはHTTP日時として確認する |
HTTP 407・ProxyError |
プロトコル、ポート、認証情報 | 管理画面の接続情報と照合する |
Timeout |
回線、接続先、プロキシの応答 | まず1ページで接続状況を切り分ける |
| 2ページ目が1ページ目と同じ | 次ページURLと結果URLの順序 | 重複ページで止め、保存HTMLを比較する |
cn.bing.comへ移動する |
応答URL、接続地域、言語設定 | 実際の応答先を記録する |
| ブラウザーとPythonで結果が異なる | cookies、言語、接続元、取得時刻 | 同じ条件の取得同士を比較する |
| Excelで文字化けする | 読み込み時の文字コード | UTF-8を指定してCSVをインポートする |
次ページリンクが見つからない場合は、その応答から次に進めないという意味です。検索結果をすべて取り終えたことまでは示しません。また、異なる並びのページや部分的な重複は、完全一致による検出の対象外です。
HTML解析とBing SERP APIはどう使い分ける?
少数の検索語で項目やページ構造を確認するなら、RequestsとBeautifulSoupによるHTML解析が扱いやすい方法です。大量のキーワード、広告枠、デバイス別結果などを定期的に扱うなら、構造化データを返すSERP APIも比較対象になります。料金だけでなく、対象地域、取得項目、タスクの失敗判定、データの利用条件も確認します。
Microsoftの従来のBing Search APIsは、公式発表によると2025年8月11日に終了しました。現在の第三者サービスとは提供主体が異なります。たとえばDataForSEOのBing SERP APIには即時取得とキューを使う方式があり、HTTPステータスに加えてタスクの成否とorganicの種別を確認する必要があります。
画像検索も扱うなら、公式APIを使ったNaver画像検索のスクレイピング手順も参考になります。検索サービスごとに、必要なデータを取得できるインターフェースを選びましょう。
まとめ
Bingの検索結果をPythonで取得するには、自然検索のコンテナを特定し、タイトル・URL・スニペットを取り出します。ページ送りではURLの重複を確認し、HTMLと取得条件を残してCSVを照合できる状態にすることが大切です。地域を固定した比較にはRola IPを組み合わせ、まず1ページのデータを確認してから対象を広げてください。