跳至内容
返回博客

2026年Python网络爬虫:开发者实用指南

Raluca Penciuc最后更新于 5 min read
2026年Python网络爬虫:开发者实用指南
简而言之:这是一份实操性的 Python 网页爬取指南,首先介绍决策树,随后逐步讲解 Requests 配合 Beautiful Soup、lxml 实现的 XPath、Playwright、Selenium、Scrapy,以及针对难以爬取目标的爬取 API。 完成本指南后,您将获得可运行的代码、反封锁策略手册、支持大型语言模型(LLM)的存储模式以及生产环境检查清单。

如果你在 python web scraping ,你会得到上百个教程,它们都只安装那三个相同的库,却在真正有趣的部分之前就戛然而止。本指南则不同。 使用 Python 进行网页抓取,是指编写一个脚本,用于获取网页、解析其 HTML,并提取结构化字段,这些字段可直接导入电子表格、数据库或模型管道。其操作机制很简单。人们之所以感到困难,是因为合适的工具取决于目标网站,而选错工具会浪费数小时的时间。

本教程立场鲜明。开篇提供了一个简短的决策树,以免你为一个本可用 Requests 仅用五十行代码就能解析的页面而安装 Playwright。 随后,教程将逐层深入解析真实的网页抓取技术栈:静态 HTML、JavaScript 渲染、CSS 选择器与 XPath 的对比、字段清理、使用 Scrapy 实现扩展、代理轮换、处理 HTTP 错误、将输出存储为 CSV、SQLite 或 JSONL 格式以供下游大型语言模型(LLMs)使用,以及安排定期运行。

您将看到不同库针对同一目标的并列代码示例,从而清晰地看到权衡取舍,而非含糊其辞。您还将看到关于阻塞、维护成本以及何时使用托管 API 比其他方案更能节省时间的坦诚说明 time.sleep(random.uniform(1, 3)) 。如果您曾编写过 Python 爬虫却在生产环境中出现故障,本指南将为您揭示原因,并指导您如何避免重蹈覆辙。

为何 Python 网页爬取在 2026 年成为默认选择

Python 网页爬取之所以占据主导地位,主要有三个原因:可读性强的语法、成熟的生态系统,以及与数据栈其他部分的无缝集成。 只需十几行 Python 代码,即可下载页面、解析内容,并返回一组可直接用于 pandas 的字典列表。正是这种从“URL”到“数据框”的简短路径,使得数据工程师、分析师和机器学习团队始终青睐 Python,而非 Go 或 Node。

其库生态极其丰富。Requestshttpx 负责处理 HTTP 层;Beautiful Souplxml 则通过 CSS 选择器或 XPath 解析 HTML;PlaywrightSelenium 则用于驱动真实浏览器以处理 JavaScript 渲染的页面。 Scrapy 提供了一个开箱即用的完整爬虫框架,支持并发、重试、项目处理管道和多种导出格式。其中每一项都经过实战检验,并得到积极维护。

故事的另一半在于集成接口。抓取到的数据可直接流向 pandas、DuckDB、SQLite、Parquet 或 JSONL,以便供大型语言模型(LLM)摄取。Notebook 使迭代过程更加高效。类型提示和数据类确保数据记录的准确性。 原型开发与生产环境之间也不存在实质性差距,因为在 Jupyter 中运行的脚本无需重写,即可在 cron 任务或容器环境中运行。正是这种端到端的连续性,使得 Python 成为 2026 年网络爬虫领域的务实首选,这也是本指南后续内容完全依赖 Python 的原因。

选择合适的工具:针对目标网站的决策树

在安装任何软件之前,请先针对目标网站回答以下四个问题。每个问题都能将库的选择范围缩小一个数量级,遵循这个决策树所节省的时间,远比任何优化技巧都要多。

步骤 1:该网站是否有官方 API?如果有,请使用它。与爬取相比,API 速度更快、成本更低,且在法律上更合规。将爬取保留给那些公开但无法被机器读取的页面。

步骤 2:从 curlrequests.get获取的原始 HTML 中是否包含数据?在浏览器中打开页面,查看源代码(而非“检查元素”),并搜索你需要的某个值。如果存在,说明这是静态页面。使用 Requests 配合 Beautiful Soup 就足够了。不要使用无头浏览器。

步骤 3:内容是否在初始 HTML 加载后由 JavaScript 注入?如果是,则需要真正的浏览器环境。Playwright 是现代开发中的默认选择。如果你的团队已经在持续集成(CI)中运行 Selenium,使用它也完全没问题。两者都能等待选择器触发、执行点击、滚动操作,并从渲染后的 DOM 中提取数据。

步骤 4:你是否正在爬取数千个 URL、串联请求,或运行长期运行的任务?请转用 Scrapy。其事件循环、项目管道、并发控制以及内置重试机制,都远胜于任何自研的循环方案。

针对难以处理的目标,还有第五种方案。如果网站会积极检测浏览器指纹、屏蔽数据中心 IP,或者在首次访问时就弹出 CAPTCHA,请保留你的解析代码,但将请求层委托给一个爬虫 API。这是一种备选方案,而非默认选择。

请参考此快速指南:

目标网站上的信号

最佳工具

提供公开 API

API

初始 HTML 中的数据

请求 + Beautiful Soup

JavaScript渲染的内容

Playwright(或 Selenium)

数千个页面、重试机制、处理管道

Scrapy

验证码、IP 封禁、TLS 指纹识别

API 抓取

大多数 Python 网络爬虫项目都应从最轻量级的数据获取工具开始,仅在当前层级能力不足时才逐步升级。这与社区的指导原则一致:最佳的配置就是能够可靠获取数据的最简单方案。

环境配置:Python 3、Virtualenv 及所需库

请使用 Python 3.10 或更高版本。为每个项目创建一个隔离的虚拟环境,以防止依赖项泄漏到系统 Python 中:

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

pip install --upgrade pip
pip install requests beautifulsoup4 lxml pandas
pip install playwright
pip install scrapy
pip install selenium webdriver-manager

# Playwright ships as a Python package plus browser binaries.
# Install the Chromium binary once:
python -m playwright install chromium

lxml 既可作为最快的 Beautiful Soup 解析器,也可作为我们后续使用的 XPath 引擎。 pandas 该库虽非必需,但在数据清理和导出方面非常有用。 webdriver-manager 负责处理 Selenium 的 ChromeDriver 下载,因此您无需在每个 CI 运行器上手动匹配浏览器版本。

开始之前的一些实用提示:

  • 若您使用 Apple Silicon 平台,请选用 ARM64 版本的 Python。Playwright 的 Chromium 二进制文件在原生 ARM 环境下运行速度远快于通过 Rosetta 运行时。
  • 在 Windows 上,请使用 .venv\Scripts\activate ,并建议优先使用 PowerShell 而非 cmd,以便获得更清晰的堆栈跟踪信息。
  • 一旦您的爬虫运行正常,请锁定版本: pip freeze > requirements.txt。爬虫程序天生脆弱,锁定一组已知稳定的依赖项,是防止未来定时任务因库的静默更新而中断的最经济有效的方法。
  • 如果您计划进行容器化,请在 Dockerfile 中添加 playwright install 步骤放入 Dockerfile 中,这样浏览器二进制文件就会被封装到镜像中,而不会在每次冷启动时重新下载。

在编写任何代码之前先检查目标页面

在开发者工具上花费的每一小时,都能节省十小时的调试时间。在 Chrome 或 Firefox 中打开要抓取的页面,在触碰编辑器之前,先逐一查看以下三个面板:

“元素”面板。右键单击你关注的某个值,选择“检查”。记下标签、类名以及任何稳定的属性,例如 data-testiditemprop。看起来像是自动生成的 ID 和类名(css-1x9k2j)是不稳定的,会在下次部署时导致你的选择器失效。如果存在语义属性,请优先使用它们。

“网络”标签页。在“网络”标签页打开的状态下刷新页面,并通过 XHRFetch进行过滤。许多现代网站通过调用内部 JSON 端点来渲染列表。如果你能找到一个返回所需字段的端点,直接使用 Requests 调用该端点,比解析 HTML 更快、更可靠,且不易出错。这是大多数爬虫教程都忽略的最重要的技巧。

“复制为 cURL”。在“网络”选项卡中右键单击任何请求,选择“复制为 cURL”。将其粘贴到终端中确认是否有效,然后将其转换为 Python 代码。工具 curlconverter.com 会自动完成这一过程,但手动阅读请求头能让你了解服务器实际期望的内容:一个 User-Agent、一个 Referer、一个会话 Cookie 或一个 CSRF 令牌。

如果初始 HTML 中就包含你的数据,那么你走的是静态路径;如果数据只有在页面加载并执行 JavaScript 后才会出现,那么你走的是浏览器路径。如果存在隐藏的 JSON API,请始终优先使用它。本指南的其余部分假设你在编写代码之前已经进行了上述判断。

使用 Requests 和 Beautiful Soup 构建静态抓取工具

对于静态页面,只需三个步骤即可完成全部工作:获取、解析、提取。我们将使用 books.toscrape.com,这是一个专门为教程准备的公开演示目标,以避免对真实网站造成过载。

import requests
from bs4 import BeautifulSoup

URL = "https://books.toscrape.com/catalogue/page-1.html"
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; MyScraper/1.0)"}

resp = requests.get(URL, headers=HEADERS, timeout=15)
resp.raise_for_status()          # raises on 4xx / 5xx

A resp.status_code 返回 200 表示抓取成功。其他情况均视为异常信号,而非注脚。 raise_for_status() 将其转换为可捕获的异常。为每次请求显式设置 timeout 。默认行为是“无限期等待”,而这恰恰是你在定时任务中不希望看到的。

使用 lxml解析 HTML,其速度远快于 Python 的内置 html.parser:

soup = BeautifulSoup(resp.text, "lxml")

现在进行选择。Beautiful Soup 提供了三种查询树的方法。你日常会用到的两种是 find_all 基于标签的查询,以及 select 用于 CSS 选择器。当目标具有稳定类时,请优先使用 select 当目标具有稳定的类名时:

books = []
for card in soup.select("article.product_pod"):
    title = card.select_one("h3 a")["title"]
    price = card.select_one("p.price_color").get_text(strip=True)
    stock = card.select_one("p.instock").get_text(strip=True)
    books.append({"title": title, "price": price, "stock": stock})

print(books[:3])

养成以下几个习惯,日后会省去不少麻烦:

  • 为每个选择器添加保护措施。 select_one 若无匹配项,则返回 None ,因此 .get_text() 将在下一次迭代时导致程序崩溃。请使用 if card.select_one(...) 或海象运算符来跳过缺失的行。
  • 使用 strip=TrueHTML 中充斥着空白字符和不换行空格。在提取时进行清理比事后修复每个下游字段更高效。
  • 存储原始值,而非“最终”值。将价格字符串保存为 "£51.77" ,并在单独的步骤中进行规范化处理。当提取过程出错时,你需要查看服务器实际返回的内容。

要实现分页,请查找“下一页”链接,并循环处理直到该链接消失:

def scrape_all_pages(start_url):
    url = start_url
    results = []
    while url:
        r = requests.get(url, headers=HEADERS, timeout=15)
        r.raise_for_status()
        s = BeautifulSoup(r.text, "lxml")
        for card in s.select("article.product_pod"):
            results.append({
                "title": card.select_one("h3 a")["title"],
                "price": card.select_one("p.price_color").get_text(strip=True),
            })
        next_link = s.select_one("li.next a")
        url = requests.compat.urljoin(url, next_link["href"]) if next_link else None
    return results

这是一个完整且真实的静态 Python 网页抓取程序:使用真实的请求头进行抓取,通过 lxml进行解析,使用 CSS 选择元素,防范节点缺失,跟随下一页链接,并在分页结束时干净利落地终止。若需深入了解 Beautiful Soup 在处理表格、表单和嵌套元素方面的操作,请参阅我们的配套 Beautiful Soup 教程。

还有两个习惯值得在入门第一天就养成。首先,将请求封装在 try/except requests.RequestException 中,并记录失败的 URL。大规模抓取时网络错误在所难免,而一个在首次 ConnectionError 就崩溃的爬虫,绝不可能完成整夜的爬取任务。其次,每处理几百条记录就将结果检查点保存到磁盘,而不是将所有数据都保存在内存中:

import json

def checkpoint(records, path="checkpoint.jsonl"):
    with open(path, "a", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

这样,如果进程在 100 页中的第 47 页崩溃,你已有前 46 页的数据保存在磁盘上,可以继续处理而无需重新抓取。这两种习惯——错误处理和检查点保存——将演示用的爬虫与那些可以实际长期运行的爬虫区分开来。

CSS 选择器与 XPath:选择一种查询语言并坚持使用

Beautiful Soup 提供了 CSS 选择器。 lxml 同时支持这两种方式,但其主要优势在于 XPath。二者虽解决相同的问题,但失败模式各异,因此有意识地选择其中一种比挑选“最佳”方案更为重要。

CSS 选择器更简洁、更熟悉,读起来就像前端代码。它们非常适合基于类的选择: article.product_pod > h3 a。但对于需要沿 DOM 向上遍历、匹配文本内容或相对于同级元素进行导航的情况,它们则力不从心。

XPath 表达力更强。它支持轴导航(ancestor::, following-sibling::)、文本匹配(//a[text()="Next"])以及位置选择((//tr)[3])。使用 lxml 和XPath实现如下:

from lxml import html

tree = html.fromstring(resp.text)
titles = tree.xpath("//article[contains(@class,'product_pod')]//h3/a/@title")
prices = tree.xpath("//article[contains(@class,'product_pod')]//p[@class='price_color']/text()")

维护性问题比美观性问题更为重要。依赖类名的选择器,一旦网站设计师重命名了某个类,就会失效。而基于结构的选择器(div > div > span:nth-child(2))则会在有人添加包装器时失效。基于 contains(@class, ...) 或基于稳定属性(如 [@itemprop="price"] 等稳定属性构建的XPath表达式,则能同时应对这两种情况。实际上,选择XPath的团队往往能编写出更具韧性的爬虫程序,尽管其学习曲线略陡。

每个项目选择一种查询语言并保持一致。混合使用 soup.select(...)tree.xpath(...) 的代码库,会让每位新入职的工程师都必须同时掌握这两种技术。我们的XPath指南以及《XPath与CSS选择器对比》中,详细介绍了轴导航模式——正是这些模式使得在标记频繁变更时,切换到XPath是值得的。

对于 Python 网络爬虫团队来说,一个务实的默认做法是:每个新爬虫都先使用 Beautiful Soup 和 CSS 选择器,因为它们的语法与你要爬取的前端代码非常相似。 一旦遇到需要向上遍历树结构、按文本内容过滤或按索引导航的情况,就立即使用XPath。这样大约90%的任务都能用更简单的工具完成,而将表达力更强的工具保留到真正物有所值的时候。

清理并规范化提取的字段

原始抓取的字符串几乎无法直接使用。价格中包含货币符号,日期有六种不同的格式,到处都是空格,偶尔还会出现 \u00a0 不可分隔空格隐藏在看似干净的文本中。将提取与规范化分开处理,以便这两个步骤都能保持可调试性。

import re
from datetime import datetime

def to_float_price(raw: str) -> float | None:
    if not raw:
        return None
    cleaned = re.sub(r"[^\d.,]", "", raw).replace(",", "")
    try:
        return float(cleaned)
    except ValueError:
        return None

def to_iso_date(raw: str, fmt: str) -> str | None:
    try:
        return datetime.strptime(raw.strip(), fmt).date().isoformat()
    except (ValueError, AttributeError):
        return None

def clean_text(raw: str) -> str:
    return " ".join(raw.replace("\xa0", " ").split()) if raw else ""

在对原始记录进行单次处理时应用以下辅助操作:

def normalize(record):
    return {
        "title": clean_text(record["title"]),
        "price": to_float_price(record["price"]),
        "in_stock": "in stock" in clean_text(record["stock"]).lower(),
    }

normalized = [normalize(r) for r in raw_records]

保存前进行去重。基于 URL 或产品 ID 的稳定主键可防止日常 cron 任务在不知不觉中增加行数。若导出至 pandas, df.drop_duplicates(subset=["url"]) 只需一行代码即可处理。

有两个编码陷阱值得注意。首先, requests 会根据请求头推测响应编码,但可能不准确;若发现乱码,请 resp.encoding = "utf-8" 显式设置编码。其次,某些网站会返回 HTML 实体转义的字符,例如 ’ ,即使在现代浏览器中也是如此。Beautiful Soup 会在解析时自动解码这些字符,但如果你直接获取 JSON,请在 html.unescape() 。只有保持一致的规范化处理,才能将一个演示用的爬虫与一个真正可查询的数据集区分开来。

使用 Playwright 抓取 JavaScript 渲染的页面

一旦网站在浏览器中渲染内容,静态工具就会失效。现代的 React、Vue 和 Svelte 应用通常会返回一个几乎为空的 HTML 外壳,然后用获取的 JSON 填充 DOM。Beautiful Soup 只会看到这个外壳,而看不到实际数据。 你需要真正的浏览器环境,而 Playwright 是 Python 中获取这种环境的最简洁方式。

只需安装一次(此安装包同时包含 Chromium 二进制文件):

pip install playwright
python -m playwright install chromium

一个用于 JavaScript 渲染页面的完整、可运行的爬虫:

from playwright.sync_api import sync_playwright

def scrape_js_page(url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (compatible; MyScraper/1.0)",
            viewport={"width": 1366, "height": 900},
        )
        page = context.new_page()
        page.goto(url, wait_until="domcontentloaded", timeout=30_000)

        # Wait for the element that only appears after JS runs.
        page.wait_for_selector("article.product_pod", timeout=15_000)

        cards = page.query_selector_all("article.product_pod")
        results = []
        for card in cards:
            title = card.query_selector("h3 a").get_attribute("title")
            price = card.query_selector("p.price_color").inner_text().strip()
            results.append({"title": title, "price": price})

        browser.close()
        return results

有两个细节决定了这段代码是可靠还是不稳定。

等待策略。 wait_until="domcontentloaded" 在初始 HTML 解析完成时触发。 "networkidle" 会等待网络活动停止 500 毫秒,该策略更严格但速度较慢。在单页应用中,建议优先使用 wait_for_selector 针对你实际需要的具体元素。这比 networkidle ,且比硬编码的 time.sleep.

处理动态加载。对于无限滚动,请使用 page.mouse.wheel(0, 2000) 在循环中,直到记录数停止变化。对于点击后显示的内容, page.click("button.load-more") 并重新选择。对于需要登录才能访问的页面,使用 context.storage_state(path="auth.json") ,并在后续运行中复用状态文件,这样就无需在每次任务中重新认证。

Playwright 还支持异步操作、请求拦截(屏蔽图片和字体以加快速度)以及多浏览器目标。对于大规模爬取,通常会通过 scrapy-playwright。我们的 Playwright 指南对这些模式进行了深入讲解。

对于无限滚动页面,一种常见的模式是滚动直到记录数停止增加:

def scroll_until_stable(page, selector, max_rounds=20):
    prev = 0
    for _ in range(max_rounds):
        page.mouse.wheel(0, 4000)
        page.wait_for_timeout(1000)
        count = len(page.query_selector_all(selector))
        if count == prev:
            break
        prev = count
    return prev

为了进一步降低浏览器开销,请屏蔽不需要的占用大量资源的元素:

context.route("**/*.{png,jpg,jpeg,gif,svg,woff2,mp4}", lambda route: route.abort())

在媒体资源密集的网站上,仅这一行代码通常就能将页面加载时间缩短 40% 或更多,因为您跳过了本会直接丢弃的下载内容。结合 wait_for_selector,这使得 Playwright 在需要 JavaScript 渲染的页面上,其速度与静态爬虫相比真正具有竞争力。当大规模运行 Playwright 时,请为每个工作线程启动一个持久化的浏览器上下文,而不是为每个 URL 启动一个新的浏览器,并在类似请求中复用同一页面。Chromium 的冷启动每页大约需要一秒钟,而这一开销在任何实际爬取中都占据主导地位。

Selenium 作为 JavaScript 页面的替代方案

Selenium 比 Playwright 早出现约十年,许多团队仍在持续集成(CI)中使用它,因为其现有的测试基础设施已经支持 WebDriver。如果你属于这类团队,将其用于数据抓取是一个完全合理的选择。

一个使用 webdriver-manager ,因此无需手动下载 ChromeDriver:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager

opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--window-size=1366,900")

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)
driver.get("https://books.toscrape.com/")

WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "article.product_pod"))
)

results = []
for card in driver.find_elements(By.CSS_SELECTOR, "article.product_pod"):
    title = card.find_element(By.CSS_SELECTOR, "h3 a").get_attribute("title")
    price = card.find_element(By.CSS_SELECTOR, "p.price_color").text.strip()
    results.append({"title": title, "price": price})

driver.quit()

Selenium 依赖于 WebDriver,它是 Python 脚本与实际浏览器进程之间的桥梁。Chrome 使用 ChromeDriver,Firefox 使用 GeckoDriver,Edge 使用 EdgeDriver,且每种驱动都必须与浏览器的主版本号相匹配。

与 Playwright 相比,Selenium 的启动速度较慢,API 略显冗长,且不支持内置请求拦截。其优势在于生态系统的成熟度、在大多数 CI 服务商中的一流支持,以及测试工程师已经熟悉它。 如果您是在从零开始构建,Playwright 更简单;如果您是在扩展现有的测试套件,Selenium 也是不错的选择。我们的 Selenium 入门指南涵盖了扩展模式,以及当默认流程受阻时绕过 Cloudflare 的技巧。

有一点关于 Selenium 的细节值得牢记:始终优先使用 WebDriverWait 并配合显式条件(presence_of_element_located, element_to_be_clickable) 而不是 time.sleep。显式等待会在条件满足的瞬间立即完成;而睡眠操作在加载速度快的页面上会浪费时间,在加载速度慢的页面上仍会失败。

使用 Scrapy 实现规模扩展:爬虫、管道和导出

Scrapy 并非又一个解析库。它是一个完整的爬取框架:包含异步引擎、用于处理请求头和代理的中间件堆栈、用于数据清理和持久化的项目管道,以及内置的 JSON、JSONL 和 CSV 导出功能。 当您的任务涉及大约一千个 URL、需要重试或包含多个输出阶段时,不妨尝试使用它。

搭建项目:

scrapy startproject bookstore
cd bookstore
scrapy genspider books books.toscrape.com

编辑 bookstore/spiders/books.py:

import scrapy

class BooksSpider(scrapy.Spider):
    name = "books"
    start_urls = ["https://books.toscrape.com/catalogue/page-1.html"]
    custom_settings = {
        "DOWNLOAD_DELAY": 0.5,
        "CONCURRENT_REQUESTS": 8,
        "USER_AGENT": "Mozilla/5.0 (compatible; MyScraper/1.0)",
        "RETRY_TIMES": 3,
    }

    def parse(self, response):
        for card in response.css("article.product_pod"):
            yield {
                "title": card.css("h3 a::attr(title)").get(),
                "price": card.css("p.price_color::text").get(),
                "stock": card.css("p.instock::text").re_first(r"\S.*"),
            }
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, self.parse)

运行并导出:

scrapy crawl books -O books.jsonl

仅需这一条命令,即可爬取所有页面,跟随“下一页”链接,应用您的延迟和重试设置,并将结果流式输出为 JSONL。 -O 参数会覆盖文件; -o 则追加内容。

有两项 Scrapy 功能值得尽早学习:

项管道会将每个抓取到的项依次处理通过一系列 Python 类。可将其用于数据标准化、去重和持久化。一个 PriceCleanerPipeline 可能用于去除货币符号;一个 SQLitePipeline 可能将数据插入数据库。通过 settings.pyITEM_PIPELINES.

中间件会挂接到每个请求或响应上。代理轮换、头部轮换以及 Cloudflare 处理都属于这一范畴。社区提供的中间件如 scrapy-rotating-proxiesscrapy-user-agents 只需两行配置即可集成。

当遇到 JavaScript 内容较多的页面时,请不要放弃 Scrapy。添加 scrapy-playwright 并使用 meta={"playwright": True}。这样既能保留 Scrapy 的并发处理、管道和导出功能,又仅在需要时为相关页面承担浏览器执行成本。我们的 Scrapy 实战指南和 scrapy-playwright 集成指南都详细介绍了这种混合模式。

一个用于规范化价格并去除重复项的简易管道如下所示:

# bookstore/pipelines.py
import re

class BookstorePipeline:
    def __init__(self):
        self.seen = set()

    def process_item(self, item, spider):
        title = item.get("title")
        if title in self.seen:
            raise DropItem(f"duplicate: {title}")
        self.seen.add(title)
        raw_price = item.get("price") or ""
        item["price"] = float(re.sub(r"[^\d.]", "", raw_price) or 0)
        return item

将其接入 settings.py 使用 ITEM_PIPELINES = {"bookstore.pipelines.BookstorePipeline": 300}。整数表示优先级;数值越低,执行越早。添加更多用于验证、数据存储和 Slack 警报的类,整个后处理链便会变得声明式。

何时应跳过 Scrapy。对于单次抓取单个页面的情况,Scrapy 的项目框架未免有些大材小用。但对于任何周期性任务,或涉及 URL 数量超过几百个的任务,其默认的并发和重试机制能立即抵消编写模板代码的成本。 一条有用的经验法则:如果你发现自己正在围绕 Requests 编写自己的异步循环、线程池或重试装饰器,那就说明你已经重构了足够多的 Scrapy 功能,此时直接使用 Scrapy 更为明智。

针对反机器人目标,请使用 Web 爬取 API

有些网站会抵御你迄今为止所了解的所有方法。它们会识别你的 TLS 握手指纹、屏蔽数据中心 IP、首次访问时弹出验证码,或者返回 200 OK 状态码,但响应体却写着“请启用 JavaScript”。Playwright 可以应对其中一些情况;住宅代理则能应对更多情况。 当上述组合仍无法奏效时,使用托管式爬取 API 便是务实的解决方案。

爬取 API 接收一个 URL 并返回 HTML。它负责处理请求层:浏览器渲染、代理轮换、重试以及反机器人规避。 您只需保留现有的 Beautiful Soup 或 lxml 解析代码,并替换 fetch 调用即可。这意味着您无需维护代理池、更新浏览器指纹,也不必调试为何某地区的验证码破解器今天运行得更慢。

典型的调用示例如下:

import requests

API_ENDPOINT = "https://api.webscrapingapi.com/v2"
params = {
    "api_key": "YOUR_KEY",
    "url": "https://example.com/hard-target",
    "render_js": "true",
    "proxy_type": "residential",
    "country": "us",
}
resp = requests.get(API_ENDPOINT, params=params, timeout=60)
resp.raise_for_status()
html = resp.text

# Parse with the same code you would use on a static page.
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml")

参数至关重要。 render_js=true 在服务器端通过无头浏览器处理目标页面。 proxy_type=residential 通过模拟家庭网络流量的 IP 地址进行路由。 country=us 当目标网站根据地区提供不同内容时,对请求进行地理定位。

有两点限制值得特别说明。首先,该服务按请求次数收费,因此抓取 API 的成本会随请求量呈线性增长;如果您的页面是静态的且没有任何阻塞机制,那么使用 Requests 配合 Beautiful Soup 的方案要便宜几个数量级。 其次,部分供应商会将庞大的代理池数量作为营销数据公布;在您通过供应商的实时文档确认这些具体数字之前,请仅将其视为参考指标。正确的决策并非“默认使用 API”,而是“当抓取层出现问题时才使用 API”。

一个有用的经验法则:如果你每周花在调试阻塞问题上的时间超过一天,而不是解析错误,那么抓取层就不再是你需要解决的问题,而是应该由其他人来处理。将其交由爬取 API 处理,从而腾出工程时间,用于只有你的团队才能完成的提取和建模工作。

处理 HTTP 错误、重试和超时

没有重试策略的爬虫,注定会在投入生产后的第一个夜晚就失败。只要你愿意倾听,HTTP 响应会明确告诉你该怎么做。

状态

含义

正确应对措施

200

成功

解析并继续

301 / 302

重定向

跟随(请求默认执行此操作)

403

禁止

轮换IP或用户代理,检查反机器人机制

404

未找到

跳过并记录;资源已不存在或URL错误

429

请求过多

Honor Retry-After,请退后,放慢速度

5xx

服务器错误

采用指数退避重试,然后放弃

请求加上 urllib3 仅需几行代码即可实现正确的重试策略:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry = Retry(
    total=5,
    backoff_factor=1.5,           # 1.5s, 3s, 6s, 12s, 24s
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "HEAD"],
    respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))

resp = session.get(url, timeout=(5, 30))    # (connect, read)

为每次请求显式设置 timeout 值。一个由 (connect, read) 秒的元组比单个数字更安全,并能避免“一个缓慢的源服务器导致整个爬取受阻”这一经典故障。

在 Playwright 中,超时设置按操作分别处理: page.goto(url, timeout=30_000) 以及 page.wait_for_selector(sel, timeout=15_000)。重试必须显式设置,因为 Playwright 不会自动重试。请将导航操作封装在自定义循环中,或使用 tenacity 作为声明式装饰器:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(4), wait=wait_exponential(multiplier=1.5, min=2, max=30))
def goto_with_retry(page, url):
    page.goto(url, wait_until="domcontentloaded", timeout=30_000)

每次重试时都要记录状态码、URL 和尝试次数。无提示的重试正是爬虫在返回过期数据的同时仍能“运行”数周的原因。 还有一点需要注意:应限制每个 URL 的总重试配额,而不仅仅是尝试次数。一个经过四次重试耗时 90 秒的请求,比一个在 15 秒内快速失败并继续处理的请求更糟糕,因为慢速失败会阻塞共享该工作线程的其他所有 URL。

避免被封锁:代理、请求头和速率限制

大多数封禁归结于三个信号:你的请求头不像浏览器发出的、请求速度过快,以及你的 IP 地址被列入了数据中心的封禁列表。这三点都要解决。

轮换使用真实的用户代理池,并发送真实浏览器会发送的请求头。网站会通过 Accept, Accept-LanguageAccept-Encoding ,而不仅仅是 User-Agent.

import random, time

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...",
    # ...more real UAs
]

def browser_headers():
    return {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
    }

在请求间隔中加入抖动。固定的 time.sleep(1) 比随机的人类时间间隔更容易被检测到。

time.sleep(random.uniform(1.2, 3.5))

轮换代理,最好是住宅代理。数据中心 IP 容易被批量封禁,因为检测起来成本很低。住宅代理通过真实用户设备进行路由,因此更难被识别。

PROXIES = [
    "http://user:pass@proxy1.example.com:8000",
    "http://user:pass@proxy2.example.com:8000",
    # ...
]

def get(url):
    proxy = random.choice(PROXIES)
    return requests.get(
        url,
        headers=browser_headers(),
        proxies={"http": proxy, "https": proxy},
        timeout=(5, 30),
    )

快速识别防御失败。如果响应状态码为 200,且正文包含“请稍候……”、“需要人工干预”或“cf-chl-bypass”等内容,则表明这是 Cloudflare 的验证挑战,而非真实内容。

def looks_blocked(resp):
    body = resp.text[:5000].lower()
    return any(k in body for k in [
        "just a moment", "attention required", "cf-chl-bypass",
        "captcha", "access denied",
    ])

looks_blocked 触发时,请轮换代理、大幅降低请求频率,并考虑将请求升级为爬虫API。我们的《代理轮换》和《无封禁》操作指南对IP封禁恢复模式有更深入的说明。

无需重启进程即可从 IP 封禁中恢复。将正常工作的代理保存在 collections.deque中。当某个代理收到 403 错误或 CAPTCHA 验证时,将其轮换至后备队列,并设置冷却时间后再让其回到前线。如果整个代理池处于冷状态,应暂停处理而非崩溃:

from collections import deque
pool = deque(PROXIES)

def next_proxy():
    proxy = pool.popleft()
    pool.append(proxy)
    return proxy

对于最难攻克的目标,这还不够,但它能让一个行为规范的爬虫在无需人工干预的情况下,持续数月对大多数网站进行爬取。当这种方法不再奏效时,这就是将抓取层替换为托管 API 的信号,而不是继续扩大您的代理池。

存储抓取数据:CSV、JSON、SQLite 及适用于大型语言模型(LLM)的格式

存储格式的选择取决于数据的使用方式。请慎重选择;事后的格式转换过程十分痛苦。

CSV 适用于电子表格和快速交接:

import csv
with open("books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price", "in_stock"])
    writer.writeheader()
    writer.writerows(records)

JSON 适用于嵌套记录和 API 格式输出:

import json
with open("books.json", "w", encoding="utf-8") as f:
    json.dump(records, f, ensure_ascii=False, indent=2)

SQLite 适用于需要去重和查询的重复运行。它仅需一个文件,无需服务器,且随 Python 自带:

import sqlite3
conn = sqlite3.connect("books.db")
conn.execute(
    "CREATE TABLE IF NOT EXISTS books ("
    " url TEXT PRIMARY KEY,"
    " title TEXT, price REAL, scraped_at TEXT)"
)
conn.executemany(
    "INSERT OR REPLACE INTO books VALUES (?, ?, ?, ?)",
    [(r["url"], r["title"], r["price"], r["scraped_at"]) for r in records],
)
conn.commit()

Parquet 适用于最终将导入 DuckDB、Spark 或数据仓库的分析工作负载: pandas.DataFrame(records).to_parquet("books.parquet"). 采用列式存储且支持类型定义,因此下游查询速度快。

适用于大型语言模型(LLM)的 JSONL 输出

如果您的抓取数据将用于喂养 LLM 或嵌入管道,请以 JSONL 格式输出:每行一个 JSON 对象,每个文档包含一条记录。每条记录应包含一个稳定的 ID、来源 URL、时间戳以及一个 text 模型可进行分块的字段。

def to_llm_record(item):
    return {
        "id": item["url"],
        "source_url": item["url"],
        "scraped_at": item["scraped_at"],
        "title": item["title"],
        "text": f"{item['title']}\n\nPrice: {item['price']}\n\n{item.get('description', '')}",
        "metadata": {"category": item.get("category"), "in_stock": item["in_stock"]},
    }

with open("books.jsonl", "w", encoding="utf-8") as f:
    for r in records:
        f.write(json.dumps(to_llm_record(r), ensure_ascii=False) + "\n")

请将分块大小控制在约 2000 个令牌以内,保留源 URL 以供引用,且切勿合并来自不同来源的记录。正是这种每条记录的元数据,才能确保下游 RAG 管道的准确性。

将结果导入 pandas 或数据仓库。对于分析工作流,请将 JSONL 或 SQLite 直接加载到数据框中,并在内存中继续处理:

import pandas as pd
df = pd.read_json("books.jsonl", lines=True)
df["price"] = df["price"].astype(float)
df.to_parquet("books.parquet")

对于规模更大的数据, duckdb.read_json("books.jsonl") 可直接对同一文件执行 SQL 操作,而无需将其加载到内存中。这两种方法都能使数据提取阶段与分析阶段完全解耦,这正是当爬虫发生变化而分析逻辑保持不变时所需要的。

安排和监控周期性运行的爬虫

手动运行的爬虫只是业余爱好。按计划运行的爬虫则是数据基础设施,它需要像其他任何任务一样严格遵守纪律。

在 Linux 上,Cron 是安排任务最快捷的方式。使用以下命令进行编辑: crontab -e:

undefined0 6 * * * /path/to/.venv/bin/python /path/to/scraper.py >> /var/log/scraper.log 2>&1

该配置每天 06:00 运行,并捕获标准输出和标准错误。如果您的任务持续时间超过几分钟或存在依赖关系,建议使用 systemd 定时器,它能提供正确的启动/停止语义、重启策略,并可与 journalctl。对于开发过程中的进程内调度, schedule 库轻量且易于阅读:

import schedule, time
schedule.every().day.at("06:00").do(run_job)
while True:
    schedule.run_pending()
    time.sleep(30)

采用结构化日志。纯文本日志无法搜索;JSON 日志无需后处理即可通过管道传输到 Loki、Datadog 或 SQLite 表中:

import logging, json
logging.basicConfig(level=logging.INFO, format="%(message)s")

def log_event(event, **fields):
    logging.info(json.dumps({"event": event, **fields}))

log_event("page_scraped", url=url, status=resp.status_code, items=len(items))

针对关键指标设置警报:反复出现的 403 错误、项目数量的突然下降(通常表明网站更改了标记),以及总运行时间超过阈值。一个静默返回零行的爬虫比一个高调崩溃的爬虫更糟糕,因为你可能数周都不会察觉。 设置“预期最小行数”检查,若数值低于该阈值则触发自我告警:

MIN_EXPECTED = 400
if len(items) < MIN_EXPECTED:
    log_event("row_count_alert", got=len(items), expected=MIN_EXPECTED)
    raise SystemExit(2)     # non-zero exit code trips your cron alerter

对于长时间运行的 Python 网页抓取任务,还应每 N 页发送一次心跳信号,以便外部监视工具(如 Healthchecks.io、Uptime Kuma 或简单的 cron 哨兵)能够区分进程卡死与单纯运行缓慢的情况。沉默是您必须在设计中防范的故障模式。

调试故障的爬虫:一份可重复使用的检查清单

抓取工具出现故障的原因寥寥无几。请按顺序逐项检查此清单;问题几乎总是在前三个步骤中就能解决。

  1. 检查原始 HTML 的长度。 print(len(resp.text)). 若 body 部分小于几千字节,通常意味着遇到验证页面、速率限制,或是 JavaScript 执行前生成的空壳页面。如果与正常运行时的基准值相比长度有所下降,问题出在抓取层,而非解析器。
  2. 将渲染后的页面保存到磁盘并在浏览器中打开。 Path("debug.html").write_bytes(resp.content). 十有八九,这能立即显示您获取的是真实内容、登录页面,还是“请启用 JavaScript”的提示。
  3. 将选择器与已知有效的快照进行对比。保留一个小型 tests/fixtures/ 目录,存放抓取器正常工作时期的真实页面样本。当出现故障时, diff 请将当前 HTML 与基准快照进行对比,查找类名更改、新增的包装器或被移除的元素。
  4. 检查懒加载情况。如果项目数量减少,但页面在浏览器中看起来正常,那么该网站很可能切换到了虚拟化列表或无限滚动。重新打开“网络”选项卡,查找前端在挂载后现在调用的 JSON 端点。
  5. 查找隐藏的 JSON API。前端会随着时间推移逐步重构为基于 API 的模式。过去需要 Playwright 处理的功能,如今往往只需一次 fetch() 调用 /api/products?page=2。该端点稳定、快速,且一旦存在,几乎总是正确的答案。
  6. 验证 HTTP 语义。你收到的是 200 但 JSON 内容为空?是否 403 间歇性出现?Cookie 设置错误?在断定是解析器出错之前,请先针对状态码、响应头和 Cookie 添加日志记录。

Python 爬虫的法律与道德规范

在许多司法管辖区,针对公共数据的网络爬取是合法的,但“合法”并不等同于“负责任”。两者都要做到位。

尊重 robots.txt《机器人排除协议》(Robots Exclusion Protocol)现已成为正式的互联网标准(作为 IETF RFC 9309 发布),虽然该协议本身并非法律,但许多服务条款文档都通过引用将其纳入其中。请使用 Python 的内置 urllib.robotparser 解析该协议,并跳过被禁止的路径:

from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
if not rp.can_fetch("MyScraper/1.0", target_url):
    return

仔细阅读目标网站的服务条款。有些网站明确禁止自动化数据采集;即使未违反任何法律,忽视此类条款也可能构成违约。请特别注意需要登录的网站,因为跨越身份验证边界会大幅改变法律责任的性质。

切勿随意抓取个人数据。如果数据包含任何可识别个人身份的信息(姓名、电子邮件、地址、IP 地址、与用户相关的 Cookie),您很可能已处于《通用数据保护条例》(GDPR)、《英国数据保护法》或《加州消费者隐私法案》(CCPA)等法律的监管范围内。在构建用于存储此类数据的处理管道之前,请咨询法律顾问。

如有官方 API请优先使用;切勿绕过付费墙或身份验证;即使目标网站未强制要求,也请自行设置请求速率限制。 一个运行缓慢、礼貌且能可持续扩展的爬虫,远比一个速度快却导致整个公司被IP封禁的爬虫更有价值。我们的合规框架详细介绍了各国的法律环境。

最后,请设置一个清晰、诚实的 User-Agent ,其中应说明您的项目并提供联系地址(MyProject/1.0 (+https://example.com/contact))。这一小小的礼节能让网站所有者在动用WAF之前先有联系对象,实际上这能显著降低被封IP的概率。

三个入门练习项目

巩固 Python 网页爬取技术栈的最佳方式,是针对三种不同类型的目标构建相同的核心爬虫。这些项目规模都足够小,一个下午就能完成,同时又足够贴近实际,能让你掌握大规模爬取时会用到的模式。

价格追踪器。选择一个演示性质的电商页面(books.toscrape.com 安全且稳定)。每天将产品名称、价格和库存数据抓取到 SQLite 中一次,保留每个快照,并使用 pandas 绘制每种产品的价格走势图。 这将教你如何实现幂等操作、按 URL 去重以及时间序列存储。一旦你的测试站点添加了“折扣”字段,你还能练习如何应对模式漂移。我们的产品数据指南中还提供了针对真实电商目标的相关模式。

职位列表聚合器。从公共招聘网站(或类似 realpython.github.io/fake-jobs/)。对职位名称、工作地点和发布日期进行规范化处理,然后跨页面去重。该项目强调规范化的重要性,并让你理解为何数据提取和清洗需要作为独立步骤进行。我们的职位数据教程涵盖了从单个招聘平台扩展到五个平台时的扩展模式。

新闻标题仪表盘。从公共 RSS 源或新闻聚合网站抓取新闻标题和发布时间戳。使用 title, url, published_at, source。该项目为你后续的大语言模型(LLM)工作奠定基础:JSONL格式可直接接入嵌入管道,而每条记录的元数据(来源、时间戳)支持大量引用场景下的RAG。我们的新闻数据指南介绍了基于同一基础抓取器的变更监控方法。

同样的三阶段管道(获取、解析、提取),三种不同的数据格式。这就是你内化该模式的方式。

发布爬虫前的生产环境检查清单

在将爬虫从本地脚本升级为生产任务之前,请逐项检查此清单。遗漏任何一项,都可能导致“昨天还好好的”变成凌晨 3 点收到的警报。

关键要点

  • 从最轻量级的工具开始。Requests 配合 Beautiful Soup 即可处理大多数静态页面;仅当数据由 JavaScript 渲染时才使用 Playwright,仅当需要扩展性、管道和重试功能时才使用 Scrapy。
  • 编写代码前,请先在开发者工具中检查目标网站。隐藏的 JSON 接口通常比抓取 HTML 更可靠。
  • 将数据提取与规范化分离。先存储原始值,在第二轮处理中进行清理,并通过稳定的键值进行去重。
  • 阻塞是请求层的问题。轮换用户代理、添加延迟、使用住宅代理,并显式检测验证页面。
  • 像处理真实任务一样部署爬虫:结构化日志、重试、超时、模式漂移警报以及生产环境检查清单。无声失败是你必须在设计中防范的故障模式。

常见问题

网络爬取与使用官方 API 有什么区别?

官方 API 是一种受支持、带版本号的协议,具有速率限制、身份验证和稳定的字段名称。 爬取则是读取专为人类设计的页面,并从可能随时变更的 HTML 中推断结构。如果存在 API,它通常更快、更便宜,且在法律上更安全。仅在没有 API 能满足您的数据需求,或者 API 条款排除了您的使用场景时,才进行爬取。

抓取公共网站时,每秒多少次请求是安全的?

虽然没有放之四海皆准的标准,但一个合理的起始点是:每个域名每 2 到 5 秒发送 1 次请求,并采用抖动定时策略。如果网站在 robots.txt 或文档中公布了速率限制,请严格遵守。注意监测 HTTP 429 响应,并呈指数级降低请求频率。只有在获得网站明确许可,或通过已与目标网站协商好负载的托管服务的情况下,才可进行更快的爬取。

当目标网站更改 HTML 时,如何确保我的 Python 爬虫继续正常运行?

在持续集成(CI)中使用固定的 HTML 测试数据对选择器进行测试,在每次运行时监控每个字段的填充率,并在填充率下降时触发警报。优先使用基于语义属性的选择器(data-testid, itemprop)优先于自动生成的类名。保持提取和规范化解耦,确保单个选择器失效仅导致单个字段失败,而非整个任务失败。对解析逻辑进行版本控制,确保回滚只需一次提交。

何时应从自托管的 Python 爬虫切换到托管式爬取 API?

当问题出在请求层(而非解析器)时应进行切换。相关迹象包括:尽管轮换代理仍反复出现 403 错误、首次访问即出现验证码、TLS 指纹被拦截,以及基础设施维护时间超过数据逻辑开发时间。 保留现有的解析代码;只需替换请求层。如果目标网站是静态且未受保护的,自托管方案的成本仍会低得多。

如果我计划稍后将爬取的数据输入到大型语言模型(LLM)中,应该如何存储这些数据?

请使用 JSONL 格式,每行一个记录。每个记录都需要一个稳定的 id、一个 source_url、一个 scraped_at 时间戳、一个用于嵌入的纯文本 text 字段用于嵌入,以及一个 metadata 用于过滤的对象。确保单个 text 片段大小控制在2000个令牌左右,以免下游分块器进行任意拆分。切勿合并来自不同来源的记录,并保留URL以便RAG管道能够引用它们。

总结

2026 年的 Python 网页抓取,重点不再是学习又一个新库,而是为每个目标选择合适的库,然后将其封装在能确保其持续运行的基础架构中。使用 Requests 和 Beautiful Soup 进行初步抓取,即可覆盖大多数静态页面。 Playwright 负责处理 JavaScript。Scrapy 则能将抓取规模扩展至数千个 URL。XPath、代理轮换、结构化日志记录以及模式漂移警报,能将脚本转变为真正可以放手不管的任务。而当目标网站的抓取层真正具有敌意时,抓取 API 才是务实的“安全阀”,而非默认选择。

最重要的习惯是从小处着手。不要为了抓取一个本可用 lxml。在编写出可运行的爬虫之前,不要搭建代理池。先发布简单版本,只有当当前版本达到极限时才添加新层,并为所有环节添加监控,以便在仪表盘发现之前就察觉到网站的变化。

当遇到会识别浏览器指纹、屏蔽数据中心 IP 或首次访问时弹出验证码的目标网站时,WebScrapingAPI 团队会负责处理请求层(代理轮换、JS 渲染、重试),这样您就可以保留 Python 解析代码,从而避免卷入反机器人技术的军备竞赛。 请在您最棘手的 URL 上试用,看看整个处理流程中还有多少部分仍由您掌控。

关于作者

Raluca Penciuc, 全栈开发工程师 @ WebScrapingAPI

Raluca Penciuc

全栈开发工程师

Raluca Penciuc 是 WebScrapingAPI 的全栈开发工程师,主要负责开发爬虫、优化规避机制,并探索可靠的方法以降低在目标网站上的被检测概率。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。