Python 是网络爬虫的首选语言,理由充分:几行 requests 和 BeautifulSoup 代码就能将一个实时网页转化为可保存、查询和分析的结构化数据。只要你能读懂 HTML 并会写循环,今天就能构建一个可运行的爬虫。
本指南将向你展示如何端到端地使用 Python 抓取网站。你将安装标准工具栈,获取页面、解析内容、选择目标元素、提取干净的字段、循环遍历分页,并将结果写入 CSV。我们使用一个公开的练习网站,让每段代码都能真正运行。然后是实事求是的部分:普通 requests 在 JavaScript 渲染的页面上会失效,大规模使用时会被封锁,你将了解 Crawling API 如何通过单次调用同时解决这两个问题。
你将构建什么
一个小型 Python 爬虫,从公开练习网站读取分页的名言列表,提取每条名言的文本、作者和标签,跟随"下一页"链接直到没有更多页面,并将所有内容保存到 CSV 文件。这个模式, 获取、解析、循环、存储, 是你今后编写的几乎每个爬虫的基础骨架。
我们的目标是 quotes.toscrape.com,一个专门为学习爬虫而构建的网站。它是静态的、结构清晰,而且可以自由抓取,让你专注于技术本身,而不必在第一次尝试时就与封锁作斗争。
前置条件
开始所需不多。
基本 Python 知识。你应该能够运行脚本并使用 pip 安装包。掌握循环、函数和字典即可。
Python 3.8 或更高版本。用 python --version 检查版本。如果没有,请从 python.org 安装。
教程的前半部分仅此而已。你需要的两个库通过一条命令安装,我们接下来介绍。
设置项目
创建虚拟环境,将项目依赖与系统的其余部分隔离,然后安装完成工作的两个库。
python --version python -m venv scraper_env source scraper_env/bin/activate pip install requests beautifulsoup4
在 Windows 上,请用 scraper_env\Scripts\activate 替代 source 那行来激活环境。两个依赖各司其职:requests 通过 HTTP 获取页面,beautifulsoup4 解析返回的 HTML,让你可以通过标签和 CSS 类提取各个元素。
第一步:获取页面
每次抓取都从一个 HTTP 请求开始。向 URL 发送 GET 请求,在做任何其他操作之前检查状态码是否为 200,你就拿到了页面的 HTML。
import requests url = "https://quotes.toscrape.com/page/1/" 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 防止爬虫在服务器卡顿时永久挂起。运行此代码,你应该能看到真实 HTML 的前 500 个字符打印到终端。这证明在你编写任何选择器之前,请求已经正常工作。
第二步:用 BeautifulSoup 解析 HTML
原始 HTML 只是一个字符串。要选择元素,需要将其加载到 BeautifulSoup 中,它会将标记转化为可通过标签名和 CSS 类查询的树形结构。在浏览器中打开页面,右键单击一条名言,选择"检查",查看结构:在这个网站上,每条名言位于 div.quote 中,文本在 span.text 内,作者在 small.author 内,标签在 a.tag 内。
from bs4 import BeautifulSoup soup = BeautifulSoup(response.text, "html.parser") quotes = soup.select("div.quote") print(f"Found {len(quotes)} quotes on this page")
html.parser 参数告诉 BeautifulSoup 使用哪个引擎;它随 Python 一起提供,无需额外安装。select 方法接受 CSS 选择器并以列表形式返回所有匹配元素,因此 div.quote 会返回页面上所有十个名言块。如果你更喜欢 find 和 find_all,它们通过方法调用风格完成同样的工作。更深入的介绍请参见如何在 Python 中使用 BeautifulSoup。
第三步:提取字段
现在从每个名言块中取出数据。遍历元素,从每个子元素读取文本,并为每条名言收集一个干净的字典。将选择器封装在一个小型辅助函数中,可以防止某个字段缺失导致整次运行崩溃。
def text_of(element, selector): el = element.select_one(selector) return el.get_text(strip=True) if el else None def parse_quotes(soup): rows = [] for quote in soup.select("div.quote"): tags = [t.get_text(strip=True) for t in quote.select("a.tag")] rows.append({ "text": text_of(quote, "span.text"), "author": text_of(quote, "small.author"), "tags": ", ".join(tags), }) return rows
text_of 辅助函数同时做两件有用的事:它查询单个元素,当元素缺失时返回 None,而不是在 .get_text() 调用时抛出异常。标签需要列表推导式,因为每条名言有多个标签,将它们拼接成一个字符串可以使每行扁平且适合 CSV。调用 parse_quotes(soup) 即可得到一个整洁的字典列表,每条名言对应一个。
第四步:跟踪翻页
抓取一页只是演示;真实列表跨越多个页面。这个网站用 li.next a 元素链接下一页,当该元素消失时,说明你到达了末尾。因此循环很简单:获取当前页面,解析它,找到下一个链接,重复直到没有下一个链接。
import time BASE = "https://quotes.toscrape.com" def scrape_all(): all_rows = [] next_url = f"{BASE}/page/1/" 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, "html.parser") all_rows.extend(parse_quotes(soup)) next_link = soup.select_one("li.next a") next_url = BASE + next_link["href"] if next_link else None time.sleep(1) return all_rows
while next_url 循环一直运行,直到下一个链接的选择器返回空,此时 next_url 变为 None,循环自然结束。该网站上的 href 是相对路径,因此要在前面加上基础 URL 使其成为绝对路径。页面之间的 time.sleep(1) 不是可选的礼节性做法:在实际目标上控制请求频率是保持在网站速率限制之内最简单的方式。
第五步:保存到 CSV
只存在于内存中的数据在脚本结束时就消失了。将其写入 CSV 文件,便可在电子表格中打开、加载到 pandas,或传递给后续流程。Python 内置的 csv 模块无需额外依赖即可完成这件事。
import csv def save_csv(rows, filename="quotes.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) if __name__ == "__main__": data = scrape_all() save_csv(data) print(f"Saved {len(data)} quotes to quotes.csv")
DictWriter 将每个字典的键与 CSV 列匹配,因此表头行会从你已经选择的字段名中自动生成。newline="" 参数防止在 Windows 上行与行之间出现空白行,encoding="utf-8" 保证引号和带重音符号的作者名完整保留。运行脚本即可得到包含每页所有名言的完整 CSV。这就是一个完整的、可运行的爬虫。
普通 requests 失效的场景
上面的练习网站是静态的,这正是它作为第一个目标的好处。真实世界的网站鲜有如此友好。当你将这段代码指向一个严肃的目标时,两个问题立即出现,而且都无法通过调整选择器来解决。
JavaScript 渲染的页面
许多现代网站发送近乎空白的 HTML 外壳,然后在浏览器中通过 JavaScript 构建可见内容。requests 只能获取初始外壳,不会运行任何脚本。因此当你解析响应时,找不到你在浏览器中看到的任何数据,因为这些数据只有在页面的 JavaScript 执行后才会出现。普通请求根本无法看到这些内容。关于这个问题的完整分析,请参见如何使用 Python 抓取 JavaScript 页面。
大规模封锁
第二道墙是反机器人防御。数据中心 IP、重复的请求模式以及不像真实浏览器的流量,会被 CAPTCHA 挑战或直接封锁。你的爬虫可能运行十次请求然后开始返回 403 或空页面。添加请求头和睡眠有所帮助,但在任何真实数据量下,你需要能被识别为真实访客的 IP 地址,而单台机器无法提供。更深入的方法论请参见如何在不被封锁的情况下抓取网站。
解决方案:在单次调用中完成渲染和轮换
你可以自己运行无头浏览器来渲染 JavaScript,并维护轮换住宅代理池来解决 IP 问题。这行得通,但将这些部分整合并保持正常运行是大部分的工程工作量,与你真正想要的数据毫无关系。
Crawling API 将两者合并为一次请求。你发送 URL,它在受信任的轮换 IP 后端的真实浏览器中渲染页面,并返回完整的 HTML 供你用已经编写好的 BeautifulSoup 代码解析。在你已有的库基础上安装官方客户端。
pip install crawlbase
以下是修改前后的对比。普通请求在 JavaScript 密集的页面上返回外壳;Crawling API 调用返回渲染后的页面。下方的解析层完全不需要改变。
# Before: plain requests, breaks on JS pages and blocks response = requests.get(url, headers=headers, timeout=10) html = response.text # After: Crawling API renders the page behind a trusted IP from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) options = {"ajax_wait": "true", "page_wait": 5000} result = api.get(url, options) html = result["body"].decode("utf-8") if result["status_code"] == 200 else None # Same parser as before, unchanged soup = BeautifulSoup(html, "html.parser") rows = parse_quotes(soup)
两个等待选项在客户端渲染目标上非常重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在页面加载后再等待固定的毫秒数,让延迟元素在捕获前出现。对于在浏览器中渲染的网站使用 JavaScript token;对于静态页面普通 token 更快。重要的是 html 流入你在第三步编写的同一个 parse_quotes 函数,因此采用该 API 只是一行替换,而不是重写。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML,对于名言练习网站这样的网站已经足够。JavaScript(JS)token 先在真实浏览器中渲染页面,适用于在客户端构建内容的网站。如果你在真实目标上解析的字段返回为空,切换到 JS token 通常就能解决。
普通 requests 在 JavaScript 页面上失效,大规模使用时被封锁。Crawling API 在真实浏览器中渲染页面,在服务器端轮换住宅 IP,并将完整的 HTML 返回给你,让你已编写好的 BeautifulSoup 代码在普通请求无法触及的目标上继续正常工作。在你自己搭建无头浏览器集群之前,先在免费层试用。
实用的 Python 爬虫库
这个两库组合可以处理大多数静态任务,但随着需求增长,还有几个值得了解的库。
- requests 是获取页面的主力 HTTP 客户端。简单、可靠,是静态目标的正确默认选择。
- BeautifulSoup 将 HTML 和 XML 解析为可导航的树形结构。它对混乱的标记具有容忍性,而真实页面总是存在这种情况。
- Selenium 驱动真实浏览器,可以渲染 JavaScript 并通过点击和输入与页面交互。功能强大,但运行起来较重,在大规模使用时速度较慢。
- Scrapy 是一个具备内置并发、重试和管道的完整爬取框架。当一个脚本成长为真正的项目时,可以考虑它。
- pandas 本身不是爬虫,但爬取的数据经常在这里进行清洗、分析和导出为其他格式。
保持爬虫健康的习惯
运行一次的爬虫很容易;持续运行的爬虫需要一些纪律。无论你使用普通 requests 还是管理型 API,以下原则都适用。
-
控制请求频率。请求之间的小延迟,如上面的
time.sleep(1),能让你保持在速率限制之内,远离封锁列表。在紧密循环中高频请求网站是被限速最快的方式。 -
处理错误。页面会变化,字段会缺失,服务器会出现故障。检查状态码,防止选择器返回
None时崩溃,并包裹风险解析逻辑,让一个问题页面不会导致整次运行失败。 - 预期标记会漂移。类名和结构随时可能改变,不会另行通知。当某个字段开始返回空时,重新检查实时页面并更新选择器。定期维护是正常工作,不是出了问题的信号。
-
尊重目标。阅读网站的
robots.txt和条款,将请求量保持在合理范围内,只收集公开数据。
核心要点
- 核心循环是:获取、解析、循环、存储。requests 获取 HTML,BeautifulSoup 提取字段,分页遍历各页,csv 模块保存结果。
- 在选择之前先检查。打开页面的开发者工具,找到承载数据的标签和类,然后将每个字段映射到 CSS 选择器。
-
控制频率并做好防护。在请求之间添加延迟,在元素缺失时返回
None,防止一个问题页面导致整次运行崩溃。 - 普通 requests 有两个限制。它无法运行 JavaScript,大规模使用时会被封锁,选择器无法解决这两个问题。
- Crawling API 通过单次调用解决两者。它在受信任的轮换 IP 后端渲染页面并返回完整的 HTML,让你现有的 BeautifulSoup 解析器保持正常工作,无需任何修改。
常见问题
我需要同时使用 requests 和 BeautifulSoup 吗?
对于典型的静态网站,是的,它们自然地配合使用。requests 通过 HTTP 获取页面,将原始 HTML 作为字符串提供;BeautifulSoup 将该字符串转化为可通过标签和 CSS 类查询的树形结构,用于提取各个字段。requests 负责下载,BeautifulSoup 负责提取。
为什么我抓取的数据是空的,而页面上明明有内容?
几乎肯定是因为网站用 JavaScript 渲染内容。requests 只获取初始的 HTML 外壳,不运行脚本,所以你在浏览器中看到的数据在你解析的内容中并不存在。你需要先渲染页面,可以通过无头浏览器或 Crawling API 的 JavaScript token,然后 BeautifulSoup 才能找到那些字段。
如何抓取多个页面?
找到网站用于下一页的链接或模式,然后循环。如果有"下一页"按钮,按照第四步所示跟随其 href 直到它消失。如果 URL 遵循像 /page/2/ 这样的数字模式,可以在范围循环中构建它们。无论哪种方式,都要在页面之间添加短暂延迟,保持礼貌并避免被封锁。
如何避免在抓取时被封锁?
添加延迟控制请求频率,发送真实的 User-Agent 请求头,并变换目标而不是高频请求同一路径。在大规模使用时,你还需要看起来像真实访客的 IP,而单台机器无法提供。通过轮换住宅 IP 路由(无论是通过 Crawling API 还是 Smart AI Proxy)是防止大规模运行触发速率限制的关键。
何时应该使用 Crawling API 而不是普通 requests?
对于静态、低流量的目标,使用普通 requests,就像本指南中的练习网站一样,普通请求能直接返回数据。当页面是 JavaScript 渲染的、你被封锁或受到挑战,或者需要以单个 IP 无法支撑的流量进行抓取时,切换到 Crawling API。因为该 API 返回 HTML,你现有的解析器不需要任何改动。
使用 Python 进行网络爬虫合法吗?
抓取公开数据通常是被允许的,但这取决于网站的服务条款、你所在的司法管辖区以及你对数据的用途。开始之前先检查网站的 robots.txt 和条款,避免受 GDPR 等隐私法保护的个人数据,切勿抓取登录后的内容。如有疑虑,只收集公开数据,并将请求量控制在不会给服务器造成压力的范围内。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

