如何使用 Python 抓取必应搜索结果

评论: 0

网络分析不仅限于谷歌。必应提供了另一种 SERP 视图,对搜索引擎优化研究、链接挖掘、品牌监控、竞争分析和内容研究非常有用。Python 是实现这种自动化的理想工具:成熟的生态系统、简单明了的语法以及强大的 HTML 解析库和 JSON 库,让您可以更快、更方便地抓取必应搜索结果。

为什么关注必应而不是谷歌?

必应使用自己的排名指南和质量信号,因此结果往往与谷歌不同。这对于在有机搜索和长尾查询中发现更多机会非常有价值。在其网站管理员建议中,必应强调相关性、质量/信任度、用户参与度、新鲜度、地理因素和页面速度--这是与谷歌不同的信号平衡。这就是为什么有些网页在必应上的排名更高。

刮擦必应搜索结果时的实际用例:

  • 扩大链接建设捐赠者名单--该引擎有时会提升未进入谷歌前十名的网站。
  • 跟踪 PAA("People also ask")和必应的通用 SERP 元素(视频、旋转木马),以调整内容策略。

您能从必应搜索中提取哪些数据?

从 "经典 "SERP 中可以可靠地提取信息:

  • 标题
  • URL(文件链接);
  • 片段(描述);
  • 在结果中的位置(序数指数);
  • 一些通用结果:"相关/也有人问"、嵌入式图片/视频结果(直接包含在主 SERP 中时)。

重要提示:必应的标记会定期更改,因此下面代码中的选择器可能需要调整。

抓取必应搜索时的法律和道德考虑因素

  • 请遵守微软的使用条款:对于 "正式 "访问网络数据,微软现在提供必应搜索的 "接地 "功能,作为 Azure AI 代理的一部分。公共必应搜索 API 已于 2025 年 8 月 11 日完全日落。
  • 与必应搜索接地有自己的 TOU 和限制:它是通过 Azure 代理使用的,结果会在代理的响应中返回,而不是作为 "原始 "JSON SERP 数据返回。
  • 尊重 robots.txt,避免主机超载--遵守 robots 是基本的刮擦道德。

为抓取设置 Python 环境

安装基本设备:

pip install requests beautifulsoup4 lxml fake-useragent selenium
  • requests - HTTP 客户端(可设置标头,如 User-Agent);
  • beautifulsoup4 + lxml - HTML 解析;
  • fake-useragent - 随机 UA 生成(或建立自己的列表);
  • selenium - 在需要时渲染动态块。

方法 1 - 通过 Requests 和 BeautifulSoup 对必应进行搜索

我们将以此为基准来演示工作流程:发出 GET 请求、设置 User-Agent、解析结果卡并收集标题、URL、片段和位置。

import time
import random
from typing import List, Dict
import requests
from bs4 import BeautifulSoup

BING_URL = "https://www.bing.com/search"

HEADERS_POOL = [
    # You can add more — or use fake-useragent
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15 "
    "(KHTML, like Gecko) Version/17.0 Safari/605.1.15",
]

def fetch_serp(query: str, count: int = 10, first: int = 1,
               proxy: str | None = None) -> List[Dict]:
    """
Returns a list of results: title, url, snippet, position.
`first` — starting position (pagination), `count` — how many records to fetch.

    """
    params = {"q": query, "count": count, "first": first}
    headers = {"User-Agent": random.choice(HEADERS_POOL)}
    proxies = {"http": proxy, "https": proxy} if proxy else None

    resp = requests.get(BING_URL, params=params, headers=headers,
                        proxies=proxies, timeout=15)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "lxml")

   # Typical Bing markup: <li class="b_algo"> ... <h2><a href="">Title</a></h2>
    items = []
    for idx, li in enumerate(soup.select("li.b_algo"), start=first):
        a = li.select_one("h2 a")
        if not a:
            continue
        title = a.get_text(strip=True)
        url = a.get("href")
         # Snippet is often in .b_caption p or simply the first <p>
        sn_el = li.select_one(".b_caption p") or li.select_one("p")
        snippet = sn_el.get_text(" ", strip=True) if sn_el else ""
        items.append({
            "position": idx,
            "title": title,
            "url": url,
            "snippet": snippet
        })
    return items

if __name__ == "__main__":
    data = fetch_serp("python web scraping tutorial", count=10)
    for row in data:
        print(f"{row['position']:>2}. {row['title']} -- {row['url']}")
        print(f"   {row['snippet']}\n")

解释:

  • 使用计数/首数参数进行分页。
  • 选择器 li.b_algo h2 a 和 .b_caption p 是基线;布局可以更改(在 DevTools 中检查)。
  • 在需要时添加代理,并调节请求之间的停顿。
  • 我们将在下文中进一步改进这个例子,因为在当前条件下,这是最有效的方法。

方法 2 - 通过 API 抓取必应搜索结果(2025 年状态)

微软的公共必应搜索 API 已于 2025 年 8 月退役。微软建议将 Azure AI 代理中的必应搜索迁移到 Grounding。

实际意义

  • 大多数开发人员已无法使用带有 "原始 "JSON SERP 数据的经典 REST 端点。
  • 与必应搜索的接地连接是 Azure 代理内部的一个工具;代理可以 "查找 "网络并返回合成答案。该服务有自己的使用条款和具体规定:它不是为批量提取原始 SERP 结果而设计的。

以 JSON 格式替代原始 SERP

使用第三方 SERP API/平台(如 Apify Bing Search Scraper)返回结构化结果:标题、URL、片段、位置等。

最小 Apify 请求示例:

import requests

API_TOKEN = "apify_xxx"  # store in ENV
actor = "tri_angle/bing-search-scraper"
payload = {
    "queries": ["python web scraping tutorial"],
    "countryCode": "US",
    "includeUnfilteredResults": False
}

r = requests.post(
    f"https://api.apify.com/v2/acts/{actor}/runs?token={API_TOKEN}",
    json=payload, timeout=30
)
run = r.json()
# Retrieve dataset items using run['data']['defaultDatasetId']

Apify 文档支持有机结果、PAA、相关查询等。确保您的用例符合平台规则和您所在司法管辖区的法律。

提示:如果您在 Azure 人工智能代理堆栈中工作,并且只需要 LLM 的接地引用(而不是原始 JSON),请阅读以下指南 通过必应搜索接地.

方法 3 - 使用 Selenium 解析动态内容

当 SERP 包括旋转木马、交互式块或由 JavaScript 呈现的内容时,请切换到 Selenium(无头 Chrome/Firefox)。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options

def selenium_bing(query: str, headless: bool = True):
    opts = Options()
    if headless:
        opts.add_argument("--headless=new")
    opts.add_argument("--disable-gpu")
    opts.add_argument("--no-sandbox")
    with webdriver.Chrome(options=opts) as driver:
        driver.get("https://www.bing.com/")
        box = driver.find_element(By.NAME, "q")
        box.send_keys(query)
        box.submit()

        # Consider adding explicit waits via WebDriverWait
        cards = driver.find_elements(By.CSS_SELECTOR, "li.b_algo h2 a")
        results = []
        for i, a in enumerate(cards, start=1):
            results.append({"position": i, "title": a.text, "url": a.get_attribute("href")})
        return results

if __name__ == "__main__":
    print(selenium_bing("site:docs.python.org requests headers"))

参考官方 硒文档的驱动程序安装和 WebDriverWait 示例。

实用解决方案:解析策略和示例代码

在最终实施中,我们将直接从 HTML 中执行 Bing 搜索:

  1. 向 https://www.bing.com/search 发送 HTTP 请求。
  2. 设置用户代理。
  3. 通过 BeautifulSoup + lxml 解析 HTML,提取标题、URL 和片段。

这样,您就不需要微软账户,也不会与第三方付费 API 绑定。对于结果选择,我们使用结果卡容器 li.b_algo,它通常用于必应的有机区块。

工作示例(分页、延迟、可选代理)

from __future__ import annotations

import argparse
import csv
import dataclasses
import pathlib
import random
import sys
import time
from typing import List, Optional, Tuple

import requests
from bs4 import BeautifulSoup, FeatureNotFound

BING_URL = "https://www.bing.com/search"

# Pool of user agents
UA_POOL = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
]

@dataclasses.dataclass
class SerpItem:
    position: int
    title: str
    url: str
    snippet: str


def build_session(proxy: Optional[str] = None) -> requests.Session:
    """Create a session with baseline headers and an optional proxy."""
    s = requests.Session()
    s.headers.update(
        {
            "User-Agent": random.choice(UA_POOL),
            "Accept-Language": "uk-UA,uk;q=0.9,en;q=0.8",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        }
    )
    if proxy:
        # Requests proxy dict format: {'http': 'http://host:port', 'https': 'http://host:port'}
        s.proxies.update({"http": proxy, "https": proxy})
    return s


def _soup_with_fallback(html: str) -> BeautifulSoup:
    """Parse HTML with a forgiving fallback chain: lxml -> html.parser -> html5lib (if available)."""
    for parser in ("lxml", "html.parser", "html5lib"):
        try:
            return BeautifulSoup(html, parser)
        except FeatureNotFound:
            continue
    # If none are installed, bs4 will raise; let it propagate
    return BeautifulSoup(html, "html.parser")


def parse_serp_html(html: str, start_pos: int) -> List[SerpItem]:
    """Extract organic results from Bing SERP HTML."""
    soup = _soup_with_fallback(html)
    items: List[SerpItem] = []

    # Organic blocks typically look like <li class="b_algo"> with h2>a and a snippet under .b_caption p or the first <p>.
    for i, li in enumerate(soup.select("li.b_algo"), start=start_pos):
        a = li.select_one("h2 > a")
        if not a:
            continue
        title = (a.get_text(strip=True) or "").strip()
        url = a.get("href") or ""
        p = li.select_one(".b_caption p") or li.select_one("p")
        snippet = (p.get_text(" ", strip=True) if p else "").strip()
        items.append(SerpItem(position=i, title=title, url=url, snippet=snippet))

    return items


def fetch_bing_page(
    session: requests.Session,
    query: str,
    first: int = 1,
    count: int = 10,
    cc: str = "UA",
    setlang: str = "uk",
    timeout: int = 20,
) -> List[SerpItem]:
    """Download one results page and return parsed items."""
    params = {
        "q": query,
        "count": count,   # 10, 15, 20...
        "first": first,   # 1, 11, 21...
        "cc": cc,         # country code for results
        "setlang": setlang,  # interface/snippet language
    }
    r = session.get(BING_URL, params=params, timeout=timeout)
    r.raise_for_status()
    return parse_serp_html(r.text, start_pos=first)


def search_bing(
    query: str,
    pages: int = 1,
    count: int = 10,
    pause_range: Tuple[float, float] = (1.2, 2.7),
    proxy: Optional[str] = None,
    cc: str = "UA",
    setlang: str = "uk",
    timeout: int = 20,
) -> List[SerpItem]:
    """Iterate over pages and return an aggregated list of results."""
    session = build_session(proxy=proxy)
    all_items: List[SerpItem] = []
    first = 1
    for _ in range(pages):
        items = fetch_bing_page(
            session, query, first=first, count=count, cc=cc, setlang=setlang, timeout=timeout
        )
        all_items.extend(items)
        time.sleep(random.uniform(*pause_range))  # polite delay
        first += count
    return all_items


def _normalize_cell(s: str) -> str:
    """Optional: collapse internal whitespace so simple viewers show one‑line cells."""
    # Convert tabs/newlines/multiple spaces to a single space
    return " ".join((s or "").split())


def save_csv(
    items: List[SerpItem],
    path: str,
    excel_friendly: bool = False,
    normalize: bool = False,
    delimiter: str = ",",
) -> int:
    """
Write results to CSV.
— excel_friendly=True -> write UTF‑8 with BOM (utf‑8‑sig) so Excel auto‑detects Unicode.
— normalize=True -> collapse whitespace inside string fields.
— delimiter -> change if your consumer expects ';', etc.
Returns the number of rows written (excluding header).

    """
    p = pathlib.Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)

    encoding = "utf-8-sig" if excel_friendly else "utf-8"

    # newline='' is required so Python's csv handles line endings correctly on all platforms
    with p.open("w", newline="", encoding=encoding) as f:
        writer = csv.DictWriter(
            f,
            fieldnames=["position", "title", "url", "snippet"],
            delimiter=delimiter,
            quoting=csv.QUOTE_MINIMAL,
        )
        writer.writeheader()
        for it in items:
            row = dataclasses.asdict(it)
            if normalize:
                row = {k: _normalize_cell(v) if isinstance(v, str) else v for k, v in row.items()}
            writer.writerow(row)
    return len(items)


def main() -> int:
    ap = argparse.ArgumentParser(description="Bing SERP scraper (Requests + BS4)")
    ap.add_argument("-q", "--query", required=True, help="Search query")
    ap.add_argument("--pages", type=int, default=1, help="Number of pages (x count)")
    ap.add_argument("--count", type=int, default=10, help="Results per page")
    ap.add_argument("--cc", default="UA", help="Country code for results (cc)")
    ap.add_argument("--setlang", default="uk", help="Interface/snippet language (setlang)")
    ap.add_argument("--proxy", help="Proxy, e.g. http://user:pass@host:port")
    ap.add_argument("--csv", help="Path to CSV to save results")
    ap.add_argument(
        "--excel-friendly",
        action="store_true",
        help="Add BOM (UTF‑8‑SIG) so Excel opens the file correctly",
    )
    ap.add_argument(
        "--normalize-cells",
        action="store_true",
        help="Remove line breaks and extra spaces in cells",
    )
    ap.add_argument(
        "--delimiter",
        default=",",
        help="CSV delimiter (default ','); e.g.: ';'",
    )
    args = ap.parse_args()

    try:
        items = search_bing(
            args.query,
            pages=args.pages,
            count=args.count,
            proxy=args.proxy,
            cc=args.cc,
            setlang=args.setlang,
        )
    except requests.HTTPError as e:
        print(f"[ERROR] HTTP error: {e}", file=sys.stderr)
        return 2
    except requests.RequestException as e:
        print(f"[ERROR] Network error: {e}", file=sys.stderr)
        return 2

    if args.csv:
        try:
            n = save_csv(
                items,
                args.csv,
                excel_friendly=args.excel_friendly,
                normalize=args.normalize_cells,
                delimiter=args.delimiter,
            )
            print(f"Saved {n} rows to {args.csv}")
        except OSError as e:
            print(f"[ERROR] Could not write CSV to {args.csv}: {e}", file=sys.stderr)
            return 3
    else:
        for it in items:
            print(f"{it.position:>2}. {it.title} -- {it.url}")
            if it.snippet:
                print("   ", it.snippet[:180])

    return 0


if __name__ == "__main__":
    sys.exit(main())

使用额外参数和代理的示例:

python bing_scraper.py -q "Python web scraping" --pages 3 --csv out.csv \
  --proxy "http://username:password@proxy:port"

剧本的作用

  1. 向必应发送带有受控参数(q、count、first)和本地设置(cc、setlang)的 GET 请求。
  2. 覆盖 User-Agent,并添加 Accept-Language 以获得更稳定的片段。
  3. 通过 BeautifulSoup(..., "lxml")解析 HTML,定位结果卡 li.b_algo,并提取标题、url 和片段。BS4 中的 .select() CSS 选择器是一种标准、灵活的方法。
  4. 支持可选代理。对于请求,正确的代理格式是协议→URL 映射。

稳定性提示:

  • 添加暂停(随机调整请求之间的间隔)。
  • 旋转 User-Agent(动态或从列表中)。Requests 演示了如何正确设置标题,我们在工作示例中就是这样做的。
  • 必要时使用代理基础设施/IP 轮换,以便在平台限制范围内有效扩展。
  • 保持合理的总体请求量,并检查验证码提示的回复。
  • 对于复杂情况,可考虑使用包含反机器人基础设施的 SERP API(Apify 等)。

从何处了解有关工具的更多信息

  • 请求:标头、代理、超时 官方文件.
  • 美丽汤:.select() 和 CSS 选择器。
  • lxml修正了 BS4 的 HTML 解析器。

提示:如果您需要代理基础架构来实现更稳定的数据收集,请查看 必应的最佳代理.

如何在搜索必应时避免阻塞

确保您的铲运机不会在第一个循环中 "死亡 "的关键原则:

  • 增加延迟(随机调整请求间隔)。
  • 旋转 User-Agent(动态或从自己的列表中);在请求中设置标头的正确方法已在文档中说明,我们在工作示例中使用了相同的方法。
  • 使用代理或 IP 轮换(遵守服务使用条款)。
  • 限制验证码提示的请求总数并监控回复情况。
  • 对于复杂的任务,可考虑使用具有内置反机器人基础设施的 SERP API(Apify 等)。

结论

当您想将研究扩展到 Google 以外、收集更多捐赠域名、跟踪其他 SERP 功能并获得独立的景观视图时,使用必应搜索很有帮助。为了实现稳定的 "官方 "集成,微软提倡在 Azure AI 代理中与必应搜索接地;从服务条款的角度来看,这种方法更安全,但不能返回原始 JSON SERP 数据。如果您的任务是提取结构化结果,可选择通过 Requests/BS4 或 Selenium 直接解析 HTML,或使用专门的 SERP API。根据任务选择工具:快速 HTML 解析适用于原型,代理适用于基于 LLM 的答案,SERP API 适用于更大规模的收集。

评论:

0 评论