互联网上的大多数数据以非结构化 HTML 的形式存在,散落在为人眼而非脚本所建的页面上。网络爬取是将这些数据转化为可保存、可查询、可分析的结构化记录的方式,无需手动复制粘贴,也不必等待网站可能永远不会提供的 API。价格追踪、市场调研、潜在客户名单和训练集收集都以同样的方式开始:获取页面、解析、保存你关心的字段。
本指南从头到尾演示完整的 Python 工作流程。你将搭建工具包,发送请求,用 CSS 选择器解析 HTML,处理 JavaScript 渲染的页面,跟随分页,将结果存储为 CSV 或 JSON,并应对规模化时出现的封锁问题。每个代码片段都是真实可复制的,整个演练限定在一个专为练习而建的公开练习网站的公开数据上。
你将构建什么
一个小型、完整的 Python 爬虫,它从公开练习网站读取分页的书单,从每个条目提取干净的记录,遍历所有页面直到结束,并将所有内容写入磁盘。这种"获取、解析、循环、存储"的结构,是你今后编写的几乎每个爬虫的骨架。
- 标题。每个列表卡片中的产品名称。
- 价格。显示的价格字符串,可清洗为数字。
- 库存状态。商品是否有货。
- 评分。每张卡片上附带的星级评分。
- URL。到详情页面的绝对链接。
我们以 books.toscrape.com 为目标,这是一个专门为练习爬取而建的沙盒网站。它是静态的、结构良好的,完全可以爬取,所以你可以专注于技术,而不必在第一次尝试时就与封锁周旋。
网络爬取如何运作
爬虫只是一个 HTTP 客户端加上一个解析器。客户端请求 URL,服务器返回 HTML;解析器将该 HTML 加载为一棵可以按标签、类或 CSS 选择器查询的树,你将想要的值复制到记录列表中。搜索引擎自 1993 年最初的爬虫以来就是这样工作的,其机制几乎没有改变:发现 URL,获取每个 URL,提取结构化字段,继续前进。
改变的是现代网络。许多网站现在发送一个几乎空白的 HTML 壳,并在浏览器中用 JavaScript 渲染可见内容,大多数严肃的目标也会抵御自动化流量。这两个现实,客户端渲染和反爬虫防御,是一份"全面"指南不能止步于 requests 和 BeautifulSoup 的原因。我们将从简单的技术栈开始,因为它能教授基础知识,然后展示它在哪里会失效以及用什么替代。
搭建 Python 工具包
Python 爬取生态系统深度丰富,但你只需要少量工具就能覆盖几乎所有工作。以下是现代工具包及每个工具何时派上用场。
- requests 发送 HTTP 请求并返回响应。它是静态页面的正确默认选择。
- BeautifulSoup 将 HTML 解析为可导航的树,并能容忍真实页面总是存在的杂乱标记。
- lxml 是 BeautifulSoup 可以使用的快速解析后端,在你需要时还提供完整的 XPath 支持。
- Selenium 或 Playwright 驱动真实浏览器,因此可以渲染 JavaScript 并通过点击和输入与页面交互。
- Scrapy 是一个内置并发、重试和管道的完整爬取框架,适用于一个脚本发展为真正项目的场景。
如果你想要更广泛的调研,请参阅最佳 Python 网络爬取库。本教程从一个干净的虚拟环境和完成核心工作的两个库开始。
python --version python -m venv scraper_env source scraper_env/bin/activate pip install requests beautifulsoup4 lxml
在 Windows 上,使用 scraper_env\Scripts\activate 代替 source 行来激活环境。你需要 Python 3.8 或更高版本;用 python --version 检查,如果缺少请从 python.org 安装。环境激活后,你就可以发送第一个请求了。
第一步:发送请求并读取响应
每次爬取都从一个 HTTP 请求开始。向 URL 发送 GET 请求,在做任何事情之前先确认状态码为 200,然后页面的 HTML 就在手了。
import requests url = "https://books.toscrape.com/catalogue/page-1.html" headers = {"User-Agent": "Mozilla/5.0 (scraper tutorial)"} response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: print(response.text[:500]) else: print(f"Request failed: {response.status_code}")
两个小习惯立竿见影。User-Agent 请求头让你的请求看起来像浏览器而非匿名脚本,许多网站更倾向于此。timeout 在服务器停滞时阻止爬虫无限等待。运行这段代码,你应该能在终端看到打印出来的 500 个真实 HTML 字符,这在你写任何选择器之前就证实了获取是有效的。
那个裸露的 requests.get 在静态练习页面上可以工作,但在真实目标上它会在 JavaScript 上失效,并在规模化时被封锁。Crawling API 接收相同的 URL,在轮换住宅 IP 后面的真实浏览器中渲染页面,并返回完整的 HTML,因此后续步骤中的解析代码保持不变,你无需自己运行无头浏览器集群和代理池。
第二步:用选择器解析 HTML
原始 HTML 只是一个字符串。要选取元素,你需要将其加载到 BeautifulSoup 中,它将标记转化为可以按标签名和 CSS 类查询的树。在浏览器中打开页面,右键点击一张书卡,选择检查以读取结构:在这个网站上,每本书位于 article.product_pod 中,标题在 h3 a 的 title 属性中,价格在 p.price_color,库存状态在 p.instock,评分以类名的形式编码在 p.star-rating 上。
from bs4 import BeautifulSoup soup = BeautifulSoup(response.text, "lxml") books = soup.select("article.product_pod") print(f"Found {len(books)} books on this page")
"lxml" 参数告诉 BeautifulSoup 使用你安装的快速 lxml 后端;如果你跳过了安装,改用 "html.parser",它随 Python 附带。select 方法接受一个 CSS 选择器并以列表形式返回所有匹配项,所以 article.product_pod 会给你页面上全部二十张书卡。如果你更喜欢 find 和 find_all,它们用方法调用风格完成同样的工作。要深入了解这两种风格,请参阅如何在 Python 中使用 BeautifulSoup;要了解 CSS 选择器和 XPath 的区别,请参阅使用 XPath 和 CSS 选择器进行网络爬取。
第三步:提取干净的字段
现在从每张卡片中提取数据。遍历元素,从每个子元素读取值,并为每本书收集一个整洁的字典。将选择器封装在一个小型辅助函数中,防止缺失字段使整个运行崩溃。
from urllib.parse import urljoin BASE = "https://books.toscrape.com/catalogue/" def text_of(element, selector): el = element.select_one(selector) return el.get_text(strip=True) if el else None def parse_books(soup): rows = [] for card in soup.select("article.product_pod"): link = card.select_one("h3 a") rating = card.select_one("p.star-rating") rows.append({ "title": link["title"] if link else None, "price": text_of(card, "p.price_color"), "availability": text_of(card, "p.instock"), "rating": rating["class"][1] if rating else None, "url": urljoin(BASE, link["href"]) if link else None, }) return rows
text_of 辅助函数查询单个元素,在缺失时返回 None,而不是对空值调用 .get_text() 而抛出异常。标题和 URL 来自属性而非文本,因此我们直接从 <a> 标签读取。评分以第二个类名的形式存储在 p.star-rating 上(例如 class="star-rating Three"),所以我们取第二个类名。urljoin 将相对 href 转换为绝对 URL。调用 parse_books(soup) 就能得到干净的字典列表,每本书一个。
第四步:处理 JavaScript 渲染的页面
练习网站是静态的,这正是它成为好的第一个目标的原因。许多真实网站不是:它们发送一个几乎空白的壳,并用 JavaScript 在浏览器中构建内容。requests 只检索那个初始壳,从不运行脚本,所以当你解析响应时,你在浏览器中看到的字段根本不存在。
经典的解决方案是真实浏览器。Playwright(或 Selenium)启动 Chromium,让页面的 JavaScript 运行,然后将完全渲染的 HTML 交给你,流入你已经写好的同一个 BeautifulSoup 解析器。
# pip install playwright && playwright install chromium from playwright.sync_api import sync_playwright def render(url): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto(url, wait_until="networkidle") html = page.content() browser.close() return html soup = BeautifulSoup(render(url), "lxml")
wait_until="networkidle" 选项会等到页面停止发出网络请求,这通常足以让客户端渲染的内容出现。这是可行的,但无头浏览器很重:规模化时速度慢,内存消耗大,而且在网站检测到自动化时会不稳定。关于这个问题的完整处理,请参阅如何用 Python 爬取 JavaScript 页面和专项Playwright 网络爬取指南。
第五步:跟随分页
单个页面是演示;真实目录跨越多个页面。这个网站用 li.next a 元素链接到下一页,当它消失时你就到达了末尾。所以循环很简单:获取当前页面,解析它,找到下一个链接,重复直到没有下一个链接。
import time def scrape_all(): all_rows = [] next_url = BASE + "page-1.html" while next_url: response = requests.get(next_url, headers=headers, timeout=10) if response.status_code != 200: print(f"Stopped at {next_url}: {response.status_code}") break soup = BeautifulSoup(response.text, "lxml") all_rows.extend(parse_books(soup)) next_link = soup.select_one("li.next a") next_url = urljoin(next_url, next_link["href"]) if next_link else None time.sleep(1) return all_rows
while next_url 循环在下一链接选择器返回空时运行,此时 next_url 变为 None,循环自然结束。网站的 href 是相对路径,因此 urljoin 将其相对于当前页面解析。页面之间的 time.sleep(1) 在真实目标上不是可选的礼貌行为:控制请求节奏是保持在网站速率限制之下最简单易行的方法。
第六步:将数据存储为 CSV 或 JSON
只存在于内存中的数据会在脚本结束时消失。将其写入磁盘,以便在电子表格中打开、加载到 pandas,或提供给接下来的任何步骤。Python 内置的 csv 和 json 模块无需额外依赖就能处理两种格式。CSV 非常适合平坦的表格记录;JSON 保留嵌套结构,对其他程序更友好。如果你不确定选哪个,请参阅 JSON 与 CSV 主要区别。
import csv, json def save_csv(rows, filename="books.csv"): if not rows: return with open(filename, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows) def save_json(rows, filename="books.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(rows, f, indent=2, ensure_ascii=False) if __name__ == "__main__": data = scrape_all() save_csv(data) save_json(data) print(f"Saved {len(data)} books")
DictWriter 将每个字典的键匹配到 CSV 列,因此标题行从你已经选择的字段名自动写出。newline="" 防止 Windows 上行与行之间出现空行,encoding="utf-8" 保持重音字符完整。对于较大的项目,你会写入数据库而不是文件,但记录是相同的:字典列表可以整洁地映射到 SQL 行或文档存储。运行脚本,你就拥有了跨所有页面的每本书的完整导出。这就是一个完整、可运行的爬虫。
输出的样子
每条记录是一个平坦的字典,可以整洁地序列化为 JSON。books.json 中的单个条目看起来像这样。
{ "title": "A Light in the Attic", "price": "£51.77", "availability": "In stock", "rating": "Three", "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html" }
价格仍然带有货币符号,评分是一个单词而不是数字,这是正常的:爬虫捕获页面显示的内容,在分析之前需要一个独立的清洗步骤将 "£51.77" 转换为 51.77,将 "Three" 转换为 3。将提取和清洗作为独立步骤,使两者都更容易调试。
爬虫为何被封锁,以及如何保持不被封锁
练习网站从不反击,但真实目标会。规模化时会出现两道墙,没有一道可以通过调整选择器来解决。
第一道是反爬虫防御。数据中心 IP、重复的请求模式,以及不像真实浏览器的流量,都会受到 CAPTCHA 挑战或被彻底封锁。你的爬虫可能正常工作十个请求,然后开始返回 403 或空页面。第二道是客户端渲染,已在第四步中介绍:裸露的请求无法看到浏览器用 JavaScript 构建的内容。你可以自己维护一个轮换住宅代理池并运行无头浏览器集群来对抗这两者,但将这些组合在一起并保持其健康运行占据了大部分工程工作量,而这一切与你真正想要的数据毫无关系。
托管爬取 API 将两者合并为一个请求。你发送 URL,它在可信的轮换 IP 后面的真实浏览器中渲染页面,并为你已经写好的同一个解析器返回完整的 HTML。将官方客户端安装在你现有的库旁边。
pip install crawlbase
保管好你的 Crawlbase 令牌;它是每次调用的身份验证密钥。替换只需一行:在你调用 requests.get 的地方,改为调用 API,返回的 HTML 流入同一个 parse_books 函数。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def fetch(url): options = {"ajax_wait": "true", "page_wait": 2000} result = api.get(url, options) if result["status_code"] == 200: return result["body"].decode("utf-8") return None html = fetch(url) soup = BeautifulSoup(html, "lxml") # same parser, unchanged rows = parse_books(soup)
ajax_wait 和 page_wait 选项在客户端渲染目标上很重要:ajax_wait 等待异步内容完成,page_wait 固定等待若干毫秒,使延迟加载的元素在捕获前出现。因为 API 返回 HTML,将其接入是一行代码的改变而不是重写。关于完整的反封锁策略,包括请求头策略和代理轮换,请参阅如何在不被封锁的情况下爬取网站。
Crawlbase 提供用于获取静态 HTML 的普通令牌,以及先在真实浏览器中渲染页面的 JavaScript 令牌。对于像练习目录这样的静态页面使用普通令牌;对于在客户端构建内容的任何网站切换到 JavaScript 令牌。如果在真实目标上解析字段返回空结果,通常 JavaScript 令牌就是解决方案。
扩展到单个脚本之外
"获取-解析-循环-存储"的模式可以带你走很长一段路,但两种需求最终会让你超越单个脚本。第一是并发:当你有数千个 URL 时,一次爬取一个页面很慢。第二是结构:重试、去重和数据管道不应该放在临时循环中。这正是 Scrapy 的用武之地。它开箱即提供并行请求、自动重试、请求调度和条目管道,所以你描述要提取什么,框架处理编排。
即使有了 Scrapy,上一节的两道墙也不会消失:对于 JavaScript 页面你仍然需要渲染,在大量访问时需要可信 IP 来避免封锁。清晰的分工是让框架管理并发和管道,同时托管 API 处理渲染和轮换,这样 Scrapy spider 的下载器简单地通过 Crawling API 路由每个请求。这让你的代码专注于数据,并将与数据无关的基础设施外包出去。
负责任且合法地爬取
爬取公开数据通常是允许的,但你如何做以及你收集什么,比行为本身更重要。在将爬虫指向任何网站之前,阅读其 robots.txt 和服务条款:前者告诉你网站希望自动化客户端避开哪些路径,后者设定了你通过使用它而同意的规则。控制请求节奏使你永远不会给服务器造成压力,诚实标识你的客户端,当官方 API 存在时优先使用,因为 API 是所有者为编程使用实际构建的访问路径,可以免去解析 HTML 的脆弱性。
通过将收集范围限定在公开的、非个人数据上来保持在正确的一侧。避免任何需要登录的内容、任何需要绕过你所接受条款的内容,以及 GDPR 和 CCPA 等隐私制度所涵盖的个人数据,在这些情况下收集可能需要同意和合法依据。不要重新分发你爬取的受版权保护的媒体内容,当项目是商业性质或涉及受监管数据时,要获得你对任何其他数据源所需的法律许可。负责任的爬取主要是常识:只取公开的内容,轻柔地取,尊重网站已经发布的意愿。
核心要点
- 核心循环是获取、解析、循环、存储。requests 获取 HTML,BeautifulSoup 提取字段,分页遍历所有页面,csv 或 json 模块保存结果。
- 根据页面选择工具。requests 和 BeautifulSoup 覆盖静态网站;Playwright 或 Selenium 渲染 JavaScript;Scrapy 在规模化时增加并发和管道。
- 选择前先检查。打开页面的开发工具找到持有数据的标签和类,然后将每个字段映射到 CSS 选择器。
- 普通 requests 有两个限制。它无法运行 JavaScript,规模化时会被封锁,这两点都不是选择器能修复的。
- 托管 API 通过一次调用解决两者。Crawling API 在可信的轮换 IP 后面渲染页面并返回完整的 HTML,你现有的解析器无需改动即可继续工作。
常见问题
什么是网络爬取?
网络爬取是从网页自动提取数据的过程。脚本请求一个 URL,服务器返回 HTML,解析器提取你想要的特定字段,并将其保存为 CSV、JSON 或数据库等结构化格式。这是将为人类阅读而建的页面转化为可以大规模查询和分析的数据的方式。
开始需要哪些 Python 库?
对于典型的静态网站,requests 和 BeautifulSoup 就足够了:requests 下载页面,BeautifulSoup 按标签和 CSS 类提取字段。添加 lxml 以获得更快的解析和 XPath 支持,在网站使用 JavaScript 渲染时添加 Playwright 或 Selenium,在需要并发和管道的大型项目中添加 Scrapy。
为什么我爬取的数据是空的,明明页面上有内容?
几乎总是因为网站用 JavaScript 渲染其内容。requests 只检索初始 HTML 壳,不运行脚本,所以你在浏览器中看到的数据不存在于你解析的内容中。先渲染页面,要么用无头浏览器,要么用 Crawling API 的 JavaScript 令牌,BeautifulSoup 才能找到字段。
如何爬取多个页面?
找到网站用于下一页的链接或规律,然后循环。如果有"下一页"按钮,跟随其 href 直到它消失,如第五步所示。如果 URL 遵循像 page-2.html 这样的数字规律,你可以在范围循环中构建它们。无论哪种方式,在页面之间添加短暂延迟以保持礼貌和不被封锁。
如何在爬取时避免被封锁?
添加延迟控制请求节奏,发送真实的 User-Agent 请求头,避免猛烈攻击单个路径。在规模化时,你还需要看起来像真实访客的 IP,单台机器无法提供这一点。通过轮换住宅 IP 路由(无论是通过 Crawling API 还是 Smart AI Proxy)是让大量运行不触发速率限制和 CAPTCHA 的方法。
网络爬取合法吗?
爬取公开数据通常是允许的,但取决于网站的服务条款、你所在的司法管辖区以及你对数据的用途。开始之前检查 robots.txt 和条款,避免隐私法律(如 GDPR 和 CCPA)所涵盖的个人数据,不要爬取登录后的内容,当官方 API 存在时优先使用。如有疑问,只收集公开数据,并将访问量保持在不给服务器造成压力的范围内。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
