爬虫的技术选型往往取决于目标页面的渲染方式与抓取规模。用一个代码案例贯穿始终并不现实:用 requests 抓取 JavaScript 动态渲染的页面会返回空壳 HTML,而用 Scrapy 去处理单页几个字段的临时任务又显得过重。本文按“静态页面提取 → 大规模工程化抓取 → 动态渲染处理”的递进路线展开,每个阶段附可运行的代码示例,最后补充反爬应对的实操策略。
对于服务端直接返回完整 HTML 的静态页面,requests 负责 HTTP 通信,BeautifulSoup 负责 DOM 解析,是最低学习成本的选择。requests 本身不解析页面,也不执行 JavaScript,它只拿回服务器返回的原始内容。这一限制决定了它只适合初始 HTML 中已包含目标数据的场景。
以练习站点为例,抓取首页所有书籍的标题与价格:
import requests
from bs4 import BeautifulSoup
def fetch_books(url: str) -> list[dict]:
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/122.0.0.0 Safari/537.36"
)
}
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
books = []
for card in soup.select("article.product_pod"):
title_tag = card.select_one("h3 a")
price_tag = card.select_one("p.price_color")
if not title_tag or not price_tag:
continue
books.append({
"title": title_tag.get("title", "").strip(),
"price": price_tag.get_text(strip=True),
"url": "https://books.ssjdxm.com/" + title_tag.get("href", ""),
})
return books
if __name__ == "__main__":
for b in fetch_books("https://books.ssjdxm.com/"):
print(b)这里有两个容易忽略的细节。其一,raise_for_status() 在状态码非 2xx 时抛出异常,避免后续解析空内容时产生难以定位的错误。其二,select_one 在未命中时返回 None,后续调用 .get() 或 .get_text() 会直接报 AttributeError,显式判空比事后调试更省时间。BeautifulSoup 对畸形 HTML 的容错性较好,适合结构不那么规整的页面;如果追求解析速度且 HTML 结构稳定,可以换用 lxml 作为解析器后端,XPath 表达力也更强。
当目标从“抓一页”变成“抓一万页”,手写循环加 time.sleep 的脚本在并发、去重、失败重试、数据导出上都会变得难以维护。Scrapy 把这些环节拆成独立组件:引擎负责调度,下载器负责请求,Spider 负责解析,Item Pipeline 负责清洗与存储。它的异步设计基于 Twisted,在同等网络条件下吞吐量显著高于同步脚本。
下面是一个完整的 Scrapy Spider,抓取全部书籍,并跟随“Next”按钮翻页:
import scrapy
class BooksSpider(scrapy.Spider):
name = "books_full"
allowed_domains = ["wap.njjiamei.com"]
start_urls = ["http://wap.njjiamei.com/"]
def parse(self, response):
for card in response.css("article.product_pod"):
yield {
"title": card.css("h3 a::attr(title)").get(default="").strip(),
"price": card.css("p.price_color::text").get(default="").strip(),
"detail_url": response.urljoin(
card.css("h3 a::attr(href)").get()
),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)运行方式:scrapy crawl books_full -o books.json。-o 参数会根据文件后缀自动选择导出格式,JSON、JSON Lines、CSV 均可。response.follow 内部会调用 urljoin 处理相对路径,同时 Scrapy 自带的请求去重机制会阻止同一 URL 被重复抓取,翻页场景下不需要额外写去重逻辑。
Scrapy 的代价是学习曲线。项目结构、settings.py 中的并发与延迟配置、中间件机制,都需要一定时间熟悉。如果任务只是定期抓取少量页面,requests + BeautifulSoup 的脚本配合 cron 定时执行可能更直接。
当目标数据由 JavaScript 在浏览器端渲染时,requests 拿到的 HTML 中只有空容器或占位脚本。Playwright 驱动真实浏览器引擎(Chromium、Firefox、WebKit),等待页面渲染完成后再提取内容。与 Selenium 相比,Playwright 的 API 设计更现代,自动等待机制减少了因元素未就绪而导致的随机失败。
以下示例等待书籍列表渲染完成后提取数据:
from playwright.sync_api import sync_playwright
def scrape_dynamic(url: str) -> list[dict]:
results = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, wait_until="networkidle")
page.wait_for_selector("article.product_pod")
cards = page.query_selector_all("article.product_pod")
for card in cards:
title_el = card.query_selector("h3 a")
price_el = card.query_selector("p.price_color")
results.append({
"title": title_el.get_attribute("title") if title_el else "",
"price": price_el.inner_text() if price_el else "",
})
browser.close()
return resultswait_until="networkidle" 等待网络请求基本停止,wait_for_selector 确保目标元素出现在 DOM 中。Playwright 也支持拦截网络请求、复用浏览器上下文(context)来保持登录态,适合需要交互(点击、滚动、填表)才能加载数据的场景。代价是资源消耗:每个浏览器实例占用数百 MB 内存,吞吐量远低于纯 HTTP 方案,不适合大规模并发抓取。
即使代码逻辑正确,请求仍可能被 403 或虚假数据拦截。以下是可落地的策略,不涉及 JS 逆向等深度对抗。
请求头伪装是第一道门槛。 Scrapy 默认的 User-Agent 是 Scrapy/版本号,许多站点会直接拒绝。在 settings.py 中替换为常见浏览器 UA 是最低成本的修复。requests 同理,至少携带 User-Agent 和 Accept-Language。
频率控制不能靠固定间隔。 固定 time.sleep(0.1) 产生的请求间隔过于规律,反而容易触发基于行为特征的风控。使用随机区间更接近人类浏览节奏:
import random, time
def polite_delay():
time.sleep(random.uniform(0.8, 2.5))在 Scrapy 中对应 DOWNLOAD_DELAY 配合 RANDOMIZE_DOWNLOAD_DELAY = True(默认开启)。如果并发开到 32 仍然秒被封,问题通常不在代码,而在于请求密度本身已经超出了目标站点的容忍阈值。
代理不是万能药,但不换 IP 一定走不远。 单一 IP 的请求量累积到一定程度后,即使频率很低也可能被信誉系统降权。代理轮换需要配合有效性检测,免费代理池的可用率极低,不值得投入时间调试。对于学习用途,控制总量、降低频率比折腾代理更实际。
遵守 robots.txt 是底线。 Scrapy 的 ROBOTSTXT_OBEY = True 是默认配置,不要为了抓取本被禁止的路径而关闭它。目标站点的服务条款和版权声明同样需要关注,爬虫的技术能力不构成绕过这些约束的理由。
场景 | 推荐方案 | 关键理由 |
|---|---|---|
静态页面、少量字段、快速验证 | requests + BeautifulSoup | 代码直观,依赖少,调试成本低 |
多页/多层级抓取、需要持久化与去重 | Scrapy | 异步调度、内置去重与导出,项目结构可维护 |
数据由 JS 渲染、需要交互 | Playwright(或 Selenium) | 真实浏览器环境,能拿到动态生成的 DOM |
大规模抓取且对性能敏感 | Scrapy + lxml 解析器 | 异步吞吐 + C 库解析速度 |
爬虫的工程复杂度应当与任务规模匹配。一个只跑一次、抓取几百条数据的脚本,不值得引入 Scrapy 的完整项目结构;而需要持续运行、增量更新、多站点管理的采集任务,从 Scrapy 开始会比从脚本逐步演化更省力。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。