如何构建网络爬虫:从零开始的Python指南
Advanced Data Extraction Specialist
TL;DR:
- 网络爬虫进行三项操作:获取网页、从HTML中提取字段、为下一个页面重复这些操作。 一旦这三步掌握,每个后续构建的爬虫都是对同一循环的变化。
requests加上BeautifulSoup可在不到30行代码中处理静态网页。 在练习网站books.toscrape.com上,一个循环浏览所有50个列表页面并返回1000个结构化的书籍记录。- 分页就是跟踪“下一页”链接直到它不再出现。 你在每个页面上读取下一页的锚链接,将其解析为绝对URL,并一直继续,直到该链接消失。
- 普通HTTP无法看到JavaScript渲染的内容。 实践页面quotes.toscrape.com/js/在
requests中返回零个引用元素,因为标记是在页面加载后由客户端绘制的。 - Scrapeless Scraping Browser在云端渲染那些页面并返回已绘制的DOM。 一旦云浏览器执行这个JavaScript页面,它将返回10个引用元素,因此你现有的BeautifulSoup解析可以正常工作。
- 免费开始。 新的Scrapeless账户包括免费的Scraping Browser运行时——请在app.scrapeless.com注册。
介绍:构建一个真实的爬虫,然后处理反抗的页面
每个网络爬虫都做三件事:获取网页、从HTML中提取你想要的字段,并移动到下一个页面。库会变化,网站会变化,但这个循环不会变化。在一个可以安全访问的页面上学会它,这个模式将适用于你之后抓取的几乎所有东西。
摩擦现象稍后会显现。大多数人编写的第一个爬虫在静态HTML页面上工作得非常完美,然后在他们尝试的下一个网站上返回一个空的壳,因为该网站在初始响应后使用JavaScript渲染其内容。普通HTTP下载服务器发送的字节;它不会运行页面。因此,你在浏览器中可以看到的数据不在爬虫接收到的响应中。
本指南将整个过程从零开始用Python构建。静态层使用requests和BeautifulSoup针对一个书籍目录抓取沙箱和一个引用抓取沙箱——这两个网站专门存在以供抓取。然后它转向诚实的限制:当普通HTTP返回一个空页面时,工作升级到Scrapeless Scraping Browser,这是一个在云端渲染页面并返回你在本地会解析的同一DOM的浏览器。如果你更愿意使用JavaScript,则同样的静态与动态的分割将在Cheerio和Puppeteer教程中介绍。
你可以用它做什么
- 收集产品目录。 浏览分页面的列表,并将标题、价格和库存状态提取到CSV或JSON文件中。
- 追踪价格变化。 按计划重新运行同一个爬虫,并对比数字以监测降价情况。
- 构建用于分析或人工智能的数据集。 将非结构化HTML的页面转换为你的笔记本或模型可以读取的干净行。
- 监测可用性。 检查许多页面上的项目是否有库存,而无需手动逐个点击。
- 为研究管道提供数据。 将引用、文章或评论提取到一个结构化存储中以进行下游处理。
- 访问JavaScript渲染的页面。 将返回一个空壳的页面通过普通HTTP升级到云浏览器,并保持相同的解析代码。
为什么选择Scrapeless Scraping Browser
Scrapeless Scraping Browser是一个可定制的、反检测的云浏览器,专为网络爬虫和AI代理设计。对于专门的从零开始的爬虫,它解决了requests无法解决的一个问题——运行页面——并带来:
- 云端JavaScript渲染。 在客户端绘制其内容的页面返回一个完全渲染的DOM,因此BeautifulSoup将结果解析得与静态页面完全一样。
- 195个国家的住宅代理。 通过国家代码锁定出口地理位置,使页面返回本地访客所见的相同内容。
- 反检测指纹识别。 云浏览器呈现一致的人类浏览器界面,而不是简单的HTTP客户端签名。
- 一个API密钥覆盖所有功能。 Python SDK生成一个
browser_ws_endpoint,你可以通过Playwright连接;同一个密钥覆盖运行时。
在app.scrapeless.com的免费计划中获取你的API密钥。
前提条件
- Python 3.10或更新版本
- 静态层的
requests和beautifulsoup4 - JavaScript渲染层的
scrapelessSDK和playwright - 一个 Scrapeless 账户和 API 密钥 —— 请在 app.scrapeless.com 注册
- 对终端有基本的了解
安装
首先安装两个静态库:
bash
pip install requests beautifulsoup4
requests 通过 HTTP 获取页面;beautifulsoup4 解析返回的 HTML —— 由 HTML 标准 定义的标记 —— 形成一个可以用 CSS 选择器查询的树。这个组合足以处理第 1 到 4 步中的每个静态页面。在第 5 步中的 JavaScript 渲染层会添加另外两个包,在那里安装。
步骤 1 — 获取页面并确认你得到了真实的 HTML
爬虫从一个请求开始。发送一个 GET 请求到目标 URL,按照 HTTP 语义 检查状态码,并确认 HTML 实际上包含你预期的记录,然后再写任何选择器。
python
import requests
from bs4 import BeautifulSoup
url = "http://books.toscrape.com/"
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0 (tutorial-scraper)"}, timeout=30)
print("status:", resp.status_code, "bytes:", len(resp.text))
soup = BeautifulSoup(resp.text, "html.parser")
books = soup.select("article.product_pod")
print("本页书籍卡片数量:", len(books))
对于 books.toscrape.com,输出 status: 200 和 本页书籍卡片数量: 20。User-Agent 头标识了你的客户端;许多服务器会拒绝未发送该头的请求。如果在真实网站上返回的数量为零,那是页面是 JavaScript 渲染的早期信号 —— 第 5 步将处理这种情况。
步骤 2 — 发现记录,然后提取其字段
首先选择重复的容器,然后读取其中的字段。更倾向于使用稳定的挂钩 —— 一种语义标签、一个 data-* 属性或一个描述数据的类 —— 而不是一个在下次重新设计时会失效的哈希类。在 books.toscrape.com,每本书都是一个 article.product_pod,字段通过可预测的子选择器依附在其中。
python
import requests
from bs4 import BeautifulSoup
resp = requests.get(
"http://books.toscrape.com/",
headers={"User-Agent": "Mozilla/5.0 (tutorial-scraper)"},
timeout=30,
)
resp.encoding = resp.apparent_encoding # 目录使用 £; 让 requests 自行检测
soup = BeautifulSoup(resp.text, "html.parser")
pod = soup.select_one("article.product_pod")
title = pod.h3.a["title"]
price = pod.select_one("p.price_color").get_text(strip=True)
in_stock = "In stock" in pod.select_one("p.instock.availability").get_text()
print(title, "|", price, "|", "有货" if in_stock else "缺货")
输出 A Light in the Attic | £51.77 | 有货。resp.encoding = resp.apparent_encoding 这一行很重要:如果没有它,英镑符号将解码为乱码,因为响应头和文档的实际编码不一致。从已经包含该字段的元素中读取字段 —— 标题位于锚的 title 属性中,因此一个选择器可以完成发现和提取。
步骤 3 — 继续分页,直到 "下一页" 链接消失
一个列表通常不会在一页上完全显示。可靠的模式是在每一页读取下一页链接,根据当前 URL 解析它,并循环直到没有下一链接为止。没有页码需要猜测,也没有硬编码的范围需要维护。
python
import requests
from urllib.parse import urljoin
from bs4 import BeautifulSoup
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0 (tutorial-scraper)"})
records = []
url = "http://books.toscrape.com/catalogue/page-1.html"
pages = 0
while url:
resp = session.get(url, timeout=30)
resp.encoding = resp.apparent_encoding
soup = BeautifulSoup(resp.text, "html.parser")
for pod in soup.select("article.product_pod"):
records.append({
"title": pod.h3.a["title"],
"price": pod.select_one("p.price_color").get_text(strip=True),
"in_stock": "In stock" in pod.select_one("p.instock.availability").get_text(),
})
pages += 1
next_link = soup.select_one("li.next > a")
url = urljoin(url, next_link["href"]) if next_link else None
print("已爬取页面:", pages, "| 收集到记录:", len(records))
在 books.toscrape.com,这段代码遍历了所有 50 页列表,收集了 1,000 条记录。requests.Session 在请求间重用底层连接,比每次都调用新 requests.get 更快。urljoin 将相对的 href(如 page-2.html)转换为绝对 URL,以便下一个请求能够正确解析。
在免费计划中获取你的 API 密钥: app.scrapeless.com
第4步 - 写出结构化输出
抓取的数据只有在结构化后才有用。第3步中的 records 列表已经是一个拥有一致键的字典列表,因此写入 CSV 或 JSON 只需几行代码。提前决定你的模式——每行的键相同——缺失的字段将变为 None,而不会导致崩溃。
python
import csv
import json
# records 是在第3步中构建的字典列表。
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)
with open("books.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2)
print("写入了", len(records), "行到 books.csv 和 books.json")
这就是一个完整的、可工作的爬虫:抓取、发现、提取、分页、存储。它将处理任何静态 HTML 网站,只需将选择器替换为该网站的标记即可。
普通 HTTP 的止步之处:JavaScript 渲染页面
上面的爬虫在一个网站用 JavaScript 渲染内容时会崩溃。练习页面 quotes.toscrape.com/js/ 正是为了这个课程而构建——引号是在页面加载后通过脚本注入的,而不是出现在初始 HTML 中。
python
import requests
from bs4 import BeautifulSoup
resp = requests.get("https://quotes.toscrape.com/js/",
headers={"User-Agent": "Mozilla/5.0"}, timeout=30)
soup = BeautifulSoup(resp.text, "html.parser")
print("字节数:", len(resp.text), "| 引用元素:", len(soup.select("div.quote")))
这返回 字节数: 5806 和 引用元素: 0。请求成功——状态 200,真实字节——但解析后的页面中没有引号,因为 requests 从未执行创建引号的 JavaScript。没有选择器的调整可以解决这个问题;数据确实不在响应中。页面必须由浏览器运行。
第5步 - 使用 Scrapeless 爬虫浏览器渲染页面
在每个页面上本地运行一个完整的浏览器是负担重且容易被阻塞的。更干净的路径是云浏览器:Scrapeless 创建一个会话,在其端渲染页面,并返回完成的 DOM——你可以用之前写的同样的 BeautifulSoup 代码进行解析。安装两个额外的包,然后通过 Chrome DevTools 协议连接 Playwright。
bash
pip install scrapeless playwright
playwright install chromium
playwright install chromium 一次性下载本地协议客户端;实际渲染仍然在 Scrapeless 的云中运行。首先导出你的密钥(export SCRAPELESS_API_KEY=your_api_token_here),然后:
python
from scrapeless import Scrapeless
from scrapeless.types import ICreateBrowser
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
client = Scrapeless() # 从环境中读取 SCRAPELESS_API_KEY
session = client.browser.create(
ICreateBrowser(proxy_country="US", session_ttl=180)
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.browser_ws_endpoint)
ctx = browser.contexts[0] if browser.contexts else browser.new_context()
page = ctx.pages[0] if ctx.pages else ctx.new_page()
page.goto("https://quotes.toscrape.com/js/", wait_until="domcontentloaded", timeout=60_000)
page.wait_for_timeout(4_000) # 让客户端脚本绘制引号
html = page.content()
browser.close()
soup = BeautifulSoup(html, "html.parser")
quotes = soup.select("div.quote")
print("渲染的字节数:", len(html), "| 引用元素:", len(quotes))
print(quotes[0].select_one("small.author").get_text(strip=True))
这返回 渲染的字节数: 9246 和 引用元素: 10,并打印 阿尔伯特·爱因斯坦——与通过普通 HTTP 获取的零结果相比。session.browser_ws_endpoint 是一个 wss://browser.scrapeless.com/... URL;Playwright 的 connect_over_cdp 与它进行 DevTools 协议通信,因此页面在云中运行,而你的代码在本地运行。wait_until="domcontentloaded" 加上一个短的固定等待时间比在脚本驱动的页面上等待网络空闲更可靠。SDK 和 CLI 界面在 docs.scrapeless.com 上有文档。
你将获得的结果
一旦云浏览器返回渲染的 HTML,解析与静态层是相同的——相同的选择器,相同的字典结构:
json
[
{
"text": "我们所创造的世界是我们思维的过程。",
"author": "阿尔伯特·爱因斯坦",
"tags": ["变化", "深思", "思考", "世界"]
}
]
// 模式完全反映了 div.quote 解析的输出。值是示例数据。
关于渲染页面的一些诚实观察:
- 时间对于渲染的页面很重要。 引用只有在页面脚本运行后才会出现,因此在
domcontentloaded之后的短暂等待使它们得以显示。如果时间太短,你会解析到一个空页面。 - 选择器仍然存在漂移。
div.quote、span.text和small.author是该沙箱的标记;在真实网站上,当页面发生变化时,请重新检查选择器。 - 缺失字段是正常的。 一条记录可能缺少标签或价格;将每个字段视为可为空的,这样你的写入器就能持续生成干净的行。
- 固定你的出境。
proxy_country="US"保持渲染内容与美国访客一致;调整代码以匹配你需要的地区。
结论:从一个页面到一个真实的数据管道
一个有效的抓取器简化为四个步骤:获取页面,发现重复的记录,提取其字段,并跟随分页直到结束。这样通过requests和BeautifulSoup覆盖了静态网页。唯一无法访问的情况 — 使用JavaScript构建内容的页面 — 可以顺利转向Scrapeless Scraping Browser,它在云端渲染页面,然后返回相同的DOM,你的解析器已经理解。
从这里开始,以每个生产抓取器的相同方式进行扩展:保持选择器紧凑,并在标记发生变化时重新检查它们,固定出境地理以匹配该页面服务的受众,把缺失字段视为可为空,并仅在实际需要渲染的页面上使用云浏览器。对于JavaScript的同样分割,Cheerio和Puppeteer指南在Node.js中讨论了静态与动态的决策。在你准备大规模运行时,可以在定价页上比较运行时选项。
准备构建你的AI驱动数据管道吗?
加入我们的社区,领取免费的计划,并与正在构建抓取管道的开发者连接:Discord · Telegram。
在app.scrapeless.com注册,获取免费的抓取浏览器运行时,并将上面的模式调整到你的抓取器需要的网站、页面和区域。
常见问题
问:网络抓取合法吗?
抓取公开可见的数据在许多司法管辖区被广泛允许,但规则因国家和网站而异。审查目标网站的服务条款和机器人排除协议,避免个人或门控数据,并就商业用途咨询律师。本指南中的实践网站专门为抓取而存在。
问:我要构建抓取器需要代理吗?
对于低流量抓取器针对宽松网站,不需要。对于通过IP限制速度或按地区变更内容的网站,则需要 — 通过住宅代理转发,并固定国家以便页面返回本地访客看到的内容。步骤5中的抓取浏览器层包括带有proxy_country设置的代理出境。
问:为什么我的抓取器返回一个空页面?
几乎总是因为页面使用JavaScript在初始响应后呈现其内容。简单的requests下载服务器的字节,但从不运行页面的脚本,因此你在浏览器中看到的数据不在响应中。通过打印元素计数来确认,如“平凡HTTP停止的地方”部分所示,然后使用云浏览器渲染页面。
问:我该如何处理显示“访问被拒绝”或挑战页面的网站?
那是对请求的反机器人检查。通过Scrapeless抓取浏览器渲染页面,固定美国住宅出境,并在同一浏览器会话中首先加载该网站的首页,以预热会话,然后再访问目标页面,这样请求带有一致的、类似人类的浏览器界面。
问:我的选择器在重新设计后停止工作了 — 该怎么办?
标记会变化。重新检查页面,优先选择稳定的挂钩(语义标签、data-*属性、描述数据的类),并更新选择器。构建在可用的最耐用的挂钩上可以保持抓取器在修复之间的运行时间更长。
问:我可以并行抓取多少页面?
保持并发适度 — 每个主机大约三个工作者是单个网站的合理上限,这样你可以保持在礼貌的请求速率内,避免触发速率限制。云浏览器会话比HTTP请求更珍贵,所以将它们限制得比你的静态抓取更严格。
问:我必须为所有内容使用浏览器吗?
不,你不应该。使用 requests 和 BeautifulSoup 的静态层更快且成本更低,因此对于每个发送渲染 HTML 的页面都应使用它。仅对于真正需要执行 JavaScript 的页面升级到云浏览器。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



