Yandex 是大多数俄罗斯人首先会想到的搜索引擎,它在几个邻国也有类似的影响力。据多数估计,它占据了俄罗斯搜索市场一半以上的份额,这使得它的公开结果对任何跟踪俄语需求、地区排名,或观察一个品牌在 Google 并不领先的市场中如何呈现的人来说,都是一个有用的信号。结果页暴露了 SERP 工具在任何地方都想要的同一套结构化数据:标题、链接、摘要,以及它们出现的顺序。

本指南将向你展示如何用 Python 以可靠的方式抓取 Yandex 搜索结果。你会构建一个小型、可运行的爬虫,它通过 Crawling API 获取一个已渲染的结果页面,用 BeautifulSoup 解析每条自然结果,并把数据导出为 JSON 和 CSV。整个演练都仅限于任何人无需账户即可看到的公开搜索结果数据,而文末附近的合法性部分不是套话,请在把它指向任何真实抓取量之前阅读它。

你将构建什么

一个 Python 脚本,它接受一个公开的 Yandex 搜索 URL,通过 Crawling API 取回 HTML,并为页面上每条自然结果提取一条结构化记录。我们以查询词"Winter Jackets"作为贯穿全文的示例,并从每条结果中拉取这些字段:

  • 标题 结果的标题文本,即列表中所显示的样子。
  • URL 结果指向的目标链接。
  • 描述 标题下方显示的摘要。
  • 排位 结果在页面上的排名,从顶部往下计数。

为什么普通请求在 Yandex 上会失败

如果你从脚本对一个 Yandex 结果 URL 发出一个裸 HTTP 请求,你很少能拿到你在自己浏览器里看到的那个干净页面。Yandex 会密切留意自动化流量。看起来不像真实浏览器的请求,或者从一个地址过快到来的请求,会被一个验证页面("你是机器人吗?"插页)拦下来挑战,或者在抵达结果列表之前就被直接屏蔽。在许多查询中重复使用同一个 IP 会很快触发这些检查。

所以一个能用的 Yandex 爬虫在一次请求中需要两样东西:一个被平台读作真实访客的 IP,以及当页面依赖脚本时一个能渲染它的浏览器。你可以自己用一个无头浏览器加一个轮换住宅代理池来组装这套东西,但保持它们健康正是大部分工作量。Crawling API 把两者折叠进一次调用:你把 URL 发给它,它从一个受信任的 IP 获取并在需要时渲染,然后返回成品 HTML 供你解析。

为什么 IP 轮换在这里很重要

Yandex 的反爬虫检查在很大程度上依赖每个地址的请求速率。来自一个 IP 的少数几个快速请求就足以触发它的验证插页。Crawling API 在服务端轮换许多地址,因此请求会分散到它们之间,没有任何单一地址会触发限制。你可以从 1,000 次免费请求起步,无需信用卡。

先决条件

在写任何代码之前,你需要先准备好几样东西。它们都不费时。

基础的 Python。你应该能自如地编写和运行一个 Python 脚本,并用 pip 安装包。如果 BeautifulSoup 对你来说是新东西,我们的在 Python 中使用 BeautifulSoup 的指南涵盖了本教程所假定的解析基础。

Python 3.8 或更高版本。python --version 确认你的版本。如果你还没有它,请从 python.org 安装,或通过像 Anaconda 这样的发行版安装。

一个 Crawlbase 账户和 token。注册,打开你的仪表盘,并从账户 docs 页面复制你的请求 token。Crawlbase 发放两种 token 类型:用于静态页面的 Normal token,以及用于脚本繁重页面的 JavaScript token。Yandex 的自然结果会回到初始 HTML 中,所以 Normal token 在这里是正确的选择。你的前 1,000 次请求是免费的。把 token 当作密码对待:它为你的请求做身份验证,所以要让它远离版本控制。

设置项目

创建一个虚拟环境,让项目依赖保持隔离,然后安装爬虫所需的三个库。

bash
python --version

python -m venv yandex_env
source yandex_env/bin/activate

pip install crawlbase beautifulsoup4 pandas

在 Windows 上,请用 yandex_env\Scripts\activate 而非那行 source 命令来激活环境。三个依赖完成实际工作:crawlbase 是把你的请求发送给 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML,让你能按 CSS 选择器拉出字段,而 pandas 在最后处理导出到 CSV 的工作。

第 1 步:通过 Crawling API 获取页面

从拿到 HTML 开始。一个 Yandex 搜索 URL 是主域名加上 text 参数里的查询词,所以 https://yandex.com/search/?text=Winter%20Jackets 搜索的是"Winter Jackets"。用 urllib.parse.quote 对查询词编码,让空格和特殊字符能挺过这趟传输。写一个小小的 fetch_page_html() 函数,它把 URL 连同你的 token 交给 Crawling API,检查 Yandex 自身返回了 200 状态,并交还解码后的 HTML 主体。在解析之前检查状态能让失败大声暴露,而不是悄无声息。

python
from crawlbase import CrawlingAPI
from urllib.parse import quote

API_TOKEN = "YOUR_CRAWLBASE_TOKEN"  # replace with your Normal token
crawling_api = CrawlingAPI({"token": API_TOKEN})

def fetch_page_html(url):
    response = crawling_api.get(url)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed with Crawlbase status {response['headers']['pc_status']}")
    return None

if __name__ == "__main__":
    url = f"https://yandex.com/search/?text={quote('Winter Jackets')}"
    html = fetch_page_html(url)
    if html:
        print(html[:500])

crawling_api.get(url) 调用返回一个响应,它的 headers["pc_status"] 是 Yandex 自身返回的状态,它的 body 是原始页面字节。对 pc_status == "200" 设防意味着一个屏蔽或一个验证页面会以一次干净的失败浮现出来,而不是把垃圾喂进解析器。把主体解码为 UTF-8 正是让西里尔字母标题和描述保持可读的关键。把文件保存为 yandex_scraper.py,用 python yandex_scraper.py 运行它,你应当在前 500 个字符里看到真实的结果标记,这能在你写下任何一个选择器之前确认获取是正常的。

Crawlbase Yandex Scraper

那个 pc_status 检查之所以始终读到 200,是因为请求一开始就以真实访客的样子抵达了 Yandex,绕开了"你是机器人吗?"插页。Crawling API 从一个轮换的 IP 获取页面,在页面需要浏览器时渲染它,并把成品 HTML 交给你,于是你省去了自己运行一支无头舰队和搜罗一个住宅代理池的麻烦。先在免费套餐上把它指向一个公开结果 URL。

第 2 步:用 BeautifulSoup 解析结果

HTML 在手之后,把它加载进 BeautifulSoup,并按选择器拉出每条结果。Yandex 把每条自然结果包裹在一个 .serp-item 容器里;在它内部,标题位于 h2.organic__url-text,目标链接位于 a.organic__url,摘要位于 div.organic__content-wrapper。要在实时页面上确认这些,在浏览器中打开 Yandex 结果 URL,右键点击一条结果,选择"检查",并从元素上读出类名;下面的选择器与撰写时的布局相符。

python
from bs4 import BeautifulSoup

def scrape_yandex_search(html_content):
    soup = BeautifulSoup(html_content, "html.parser")

    search_results = []
    for position, result in enumerate(soup.select(".serp-item"), start=1):
        title_element = result.select_one("h2.organic__url-text")
        url_element = result.select_one("a.organic__url")
        description_element = result.select_one("div.organic__content-wrapper")

        if not title_element or not url_element:
            continue

        search_results.append({
            "position": position,
            "title": title_element.get_text(strip=True),
            "url": url_element["href"],
            "description": description_element.get_text(strip=True) if description_element else None,
        })

    return search_results

选择 .serp-item 让你每条结果得到一个元素,而 enumerate(..., start=1) 在你循环时顺手把排位交给你,于是排名来自页面顺序,而不是一个脆弱的属性。从 a.organic__urlhref 读取 URL 能把目标地址与标题文本分开。if not title_element or not url_element: continue 这道防护会跳过任何不是真正自然结果的东西,这能把广告块和零散标记挡在你的输出之外。当描述的容器缺失时,描述会回退到 None

选择器会漂移

Yandex 会定期修订它的前端标记,像 organic__url-text 这样的类名可能随一次重新部署而改变。把上面的选择器当作一个起始模板,而不是一份契约。当某个字段对每条结果都返回空时,在浏览器的开发者工具里重新检查一个实时页面并更新选择器。定期维护选择器对任何生产爬虫来说都很正常,并不是哪里坏了的信号。

第 3 步:把它组装起来

现在把获取和解析接成一个可运行的脚本。爬取已渲染的结果页面,把 HTML 交给解析器,并以 JSON 形式打印结构化输出。设置 ensure_ascii=False 能让西里尔字符在输出中保持可读,而不是被转义成 \u 序列。

python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup
from urllib.parse import quote
import json

API_TOKEN = "YOUR_CRAWLBASE_TOKEN"
crawling_api = CrawlingAPI({"token": API_TOKEN})

def fetch_page_html(url):
    response = crawling_api.get(url)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed with Crawlbase status {response['headers']['pc_status']}")
    return None

def scrape_yandex_search(html_content):
    soup = BeautifulSoup(html_content, "html.parser")
    search_results = []
    for position, result in enumerate(soup.select(".serp-item"), start=1):
        title_element = result.select_one("h2.organic__url-text")
        url_element = result.select_one("a.organic__url")
        description_element = result.select_one("div.organic__content-wrapper")
        if not title_element or not url_element:
            continue
        search_results.append({
            "position": position,
            "title": title_element.get_text(strip=True),
            "url": url_element["href"],
            "description": description_element.get_text(strip=True) if description_element else None,
        })
    return search_results

def main():
    search_query = "Winter Jackets"
    url = f"https://yandex.com/search/?text={quote(search_query)}"
    html_content = fetch_page_html(url)
    if html_content:
        search_results = scrape_yandex_search(html_content)
        print(json.dumps(search_results, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

python yandex_scraper.py 运行完整脚本。它获取"Winter Jackets"的结果页面,为每条自然列表提取一条记录,并把列表打印为格式化的 JSON。你需要的就是这同样的两个函数:在 main() 里换掉查询词,解析器就会处理回来的任何内容。

输出是什么样子

你会得到一个干净有序的结果对象列表,每个都带有它的排位、标题、URL 和描述,随时可以写入 JSON、CSV 或数据库。由于示例查询混合了英语和俄语的店面,你能从结果本身看出 Yandex 的地区优势。

json
[
  {
    "position": 1,
    "title": "Best Winter Jackets of 2024 | Switchback Travel",
    "url": "https://www.switchbacktravel.com/best-winter-jackets",
    "description": "Patagonia Tres 3-in-1 parka. Category: Casual. Fill: 4.2 oz. of 700-fill-power down."
  },
  {
    "position": 2,
    "title": "Winter jacket: купить по низкой цене на Яндекс Маркете",
    "url": "https://market.yandex.ru/search?text=winter%20jacket",
    "description": "Купить winter jacket: 97 предложений, низкие цены, быстрая доставка."
  },
  {
    "position": 3,
    "title": "Amazon.com: Winter Jackets",
    "url": "https://www.amazon.com/Winter-Jackets/s?k=Winter+Jackets",
    "description": "CAMEL CROWN Men's Mountain Snow Waterproof Ski Jacket, Detachable Hood, Fleece Parka."
  }
]

在真实输出里有一件事值得留意:排位 2 的西里尔字母标题,得益于 UTF-8 解码和 ensure_ascii=False,保留了它原本的字符。如果你看到的是 \u 转义序列,说明这两个步骤之一缺失了。

跨页面和查询扩展

一个查询、一个页面只是一个演示;一个真实的任务会跑过好几次搜索,并更深地探入结果。Yandex 用 p 查询参数分页,它是一个从零开始的页码索引:p=0 是第一页,p=1 是第二页,依此类推。形态保持不变:用下一个页码构建每个 URL,通过 Crawling API 获取它,并用同一个函数解析它。一处小改动能让排位跨页面保持连续,而不是每页都从 1 重新开始,而请求之间一段短暂的 sleep 能给爬取定速。

python
import time
from urllib.parse import quote

def scrape_all_pages(query, max_pages=5):
    base_url = f"https://yandex.com/search/?text={quote(query)}&p="
    all_results = []
    position = 1

    for page in range(max_pages):
        html_content = fetch_page_html(base_url + str(page))
        if not html_content:
            break
        page_results = scrape_yandex_search(html_content)
        for result in page_results:
            result["position"] = position
            position += 1
        all_results.extend(page_results)
        time.sleep(2)  # pace requests to respect the server

    return all_results

这个循环默认走五个页面,通过 Crawling API 获取每一个,并重新分配一个递增的 position,让排名从第一页到第五页保持连续。请求之间两秒的 time.sleep 能让你不会在一个紧密循环里猛击 Yandex。只在你确实需要的范围内提高 max_pages;更深的结果通常本来也不那么相关。如果你宁愿把自己的流量路由通过一个轮换池,而不是使用这个托管 API,Smart AI Proxy 以一个即插即用的代理端点给你同样的 IP 轮换。

导出到 CSV

JSON 在终端里很方便,但一个 CSV 能直接在电子表格里打开,这正是大多数分析师真正想要的。pandas 用两行就把结果字典的列表变成一个 CSV。

python
import pandas as pd

def save_to_csv(results, filename="yandex_search_results.csv"):
    df = pd.DataFrame(results)
    df.to_csv(filename, index=False, encoding="utf-8-sig")
    print(f"Saved {len(results)} results to {filename}")

if __name__ == "__main__":
    results = scrape_all_pages("Winter Jackets", max_pages=3)
    save_to_csv(results)

从字典列表构建 DataFrame 让你每个字段得到一列(position、title、url、description),每条结果得到一行。传入 index=False 会把 pandas 自己的行索引从文件里去掉,而 encoding="utf-8-sig" 会写入一个字节顺序标记,让西里尔描述在 Excel 里正确打开,而不是变成乱码。从这里开始,如果你偏好一个便于查询的存储,你可以把同样的模式扩展到写入像 SQLite 这样的数据库。

保持不被屏蔽

即便受信任的 IP 已经替你搞定,Yandex 仍然留意着具有爬虫特征的流量,而且它的反爬虫检查比许多西方目标都更严格。一些习惯能让一次运行保持健康。

  • 控制请求节奏。在一个紧密循环里猛击结果页面是招来验证插页最快的方式。把请求拉开间隔,并变换你的查询词,而不是全速翻动一个词。
  • 依靠轮换。一个 IP 地址池会分散请求,因此没有任何单一地址会触发 Yandex 的每地址速率检查。Crawling API 替你处理这件事;如果你自己搭建技术栈,这就是要做对的那部分。
  • 读懂状态码。一次开始返回挑战或非 200 状态的运行,是在告诉你当前的速率或 IP 层级已经不够了。把那当作退避的信号,而不是可以无视的噪声。
  • 字段变空时重新检查。Yandex 会定期更改它的标记。如果结果不再被解析出来,在开发者工具里打开一个实时页面并更新选择器。

关于更宏观的方法论,请看如何在不被屏蔽的情况下抓取网站,以及关于如何在网页抓取时绕过 captcha 的更深入探讨。同样的方法也适用于其他引擎:我们关于抓取 Bing 搜索结果抓取 Google 搜索页面的指南,用的是同样的先获取再解析结构,只是选择器不同。

抓取 Yandex 合法吗?

抓取 Yandex 是否被允许,取决于 Yandex 的服务条款、你所在的司法管辖区,以及你如何使用这些数据。和大多数搜索引擎一样,Yandex 在它的条款里对自动化访问设有限制,因此无论你的工具有多谨慎,抓取都可能违反这些条款。这里的代码都不会改变这一点;它只是让技术部分跑起来。阅读 Yandex 的条款及其 robots.txt,并把两者都当作你所收集内容的边界。

有几条值得坚守的原则。只收集公开的搜索结果数据:任何人无需账户即可在一个结果页面上看到的标题、链接、描述和排位。把你的请求量保持得足够低,以免给 Yandex 的服务器造成压力,并给你的爬取定速,而不是全速运行它。Yandex 确实为一些结构化数据发布了官方产品,比如 Yandex Maps 和 Yandex Market 的 API,所以凡是你所需内容有一个官方认可的端点存在,那就是比抓取 SERP 更好的路径。

本指南刻意只聚焦于公开的搜索结果页面,因为那是让这项工作保持可辩护的那条界线。它不涉及任何登录后的内容、账户或个人数据,也不涉及从链接目标拉取的受版权保护的媒体。只有公开的 SERP 数据。如果你的项目需要的不止这些,正确的路径是一份官方数据协议,而不是一个更聪明的爬虫。

回顾

核心要点

  • Yandex 在它的地区很强势。它在俄语和地区搜索上领先,因此在 Google 不占主导的地方,它的公开结果是一个有用的信号。
  • Crawling API 在一个受信任的 IP 后面获取内容。把 URL 发给它,它在服务端轮换地址并在需要时渲染,然后返回成品 HTML,绕开"你是机器人吗?"插页。
  • BeautifulSoup 完成提取。选中每个 .serp-item,然后从中读取标题、URL、描述和排位,并预期类名会随时间漂移。
  • 用 p 索引分页。每页把 p 加一,以更深地走入结果,让排位保持连续,并用页面之间的一段 sleep 给请求定速。
  • 只停留在公开数据上。尊重 Yandex 的 ToS 和 robots.txt,把抓取量保持得低,凡是合适处优先使用像 Yandex Market 这样的官方 API,并永远不要触碰账户或个人数据。

常见问题

Yandex 是什么,为什么要抓取它?

Yandex 是俄罗斯领先的搜索引擎,常被称为"俄罗斯的 Google",它还提供地图、邮件、商城和云服务。人们抓取它的公开结果,是为了跟踪俄语和地区排名、研究搜索趋势、监控品牌在那个市场中的呈现,以及为以 Google 为中心的工具会错过的研究收集数据。

我能用 Python 抓取 Yandex 搜索结果吗?

能。借助 crawlbase 客户端和 BeautifulSoup,你可以获取一个结果页面并拉出标题、URL、描述和排位。Crawling API 充当那座桥梁,把你的请求从一个受信任的 IP 送到 Yandex,于是请求能被顺畅地处理,而不是撞上验证插页。要了解更宽泛的 Python 入门,请看我们关于用 Python 抓取网站的指南。

为什么普通请求在 Yandex 上会被屏蔽?

Yandex 会标记看起来不像真实浏览器的流量,而且它密切留意每个 IP 的请求速率。来自一个地址的少数几个快速请求就会触发它的"你是机器人吗?"验证页面或一次直接屏蔽。通过 Crawling API 获取内容,它会轮换 IP 并在需要时渲染,使每个请求看起来像一个普通访客,于是你能拿到真实的结果页面。

我如何翻页查看更多 Yandex 结果?

使用 p 查询参数,它是一个从零开始的页码索引:p=0 是第一页,p=1 是第二页,依此类推。用下一个索引构建每个页面 URL,通过 Crawling API 获取它,用同一个函数解析它,重新分配一个递增的 position 让排名保持连续,并在请求之间暂停几秒,这样你是在给爬取定速,而不是猛击它。

我如何处理俄语结果和西里尔文本?

在读取响应主体时把它解码为 UTF-8,而在导出时,用 ensure_ascii=False 写 JSON,或用 encoding="utf-8-sig" 写 CSV。这两个步骤能让西里尔字母标题和描述保持可读,而不是变成 \u 转义或乱码。Yandex 的地区优势意味着许多结果会以俄语回来,所以这一点在这里比在一次 Google 抓取里更重要。

我的选择器什么都返回不了。变了什么?

几乎可以肯定是 Yandex 的标记。像 organic__url-textorganic__content-wrapper 这样的类名会在 Yandex 重新部署它的前端时改变,所以上个月还能用的选择器现在可能失效。在浏览器的开发者工具里重新检查一个实时结果页面并更新选择器。定期维护选择器对任何生产爬虫来说都很正常。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量