写在前面
上一篇我们完成了完整的岗位数据爬虫,但有一个前提假设:数据直接在 HTML 源码里。
问题是,现实中很多网站根本不是这样的。你在浏览器里明明白白看到一堆职位数据,按 Ctrl+U 查看源码,里面却空空如也。你用 requests 去请求,拿回来的 HTML 里找不到任何有用的数据。这类页面叫动态页面,需要用完全不同的技术来处理。
本篇的目标:
- 理解动态页面的工作原理(为什么源码里看不到数据)
- 掌握 Playwright 库,爬取需要 JS 渲染的页面
- 了解 Scrapy 框架,写出企业级的爬虫项目
- 掌握更高级的反爬应对策略
本篇文件结构
| 文件名 | 用途 | 创建方式 |
|---|---|---|
dynamic_page_demo.html |
动态页面演示文件 | 一、新建 |
playwright_demo.py |
Playwright 基础练习 | 二、逐步追加 |
dynamic_scraper.py |
动态页面爬虫实战 | 三、逐步追加 |
quotes_dynamic.json |
动态爬虫输出(自动生成) | 不需要手动创建 |
scrapy_quotes/ |
Scrapy 项目目录 | 四、自动生成 |
一、动态页面的秘密
1.1 先感受一下问题
在开始讲原理之前,我们先做一个实验,让你亲眼看到问题出在哪里。
【新建文件】 在项目目录下新建 dynamic_page_demo.html,写入以下 HTML 代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>动态页面演示</title>
</head>
<body>
<h1>职位列表(动态加载)</h1>
<!-- 这个容器在源码中是空的,数据是 JavaScript 动态插入的 -->
<div id="job-list"></div>
<script>
// 这段 JavaScript 在页面加载后,动态创建职位数据并插入页面
var jobs = [
{"title": "AI测试工程师", "salary": "25-40K", "company": "智云科技"},
{"title": "高级AI测试开发", "salary": "35-60K", "company": "星辰互联"},
{"title": "大模型测试工程师", "salary": "30-50K", "company": "深智科技"}
];
var container = document.getElementById("job-list");
for (var i = 0; i < jobs.length; i++) {
var div = document.createElement("div");
div.className = "job-item";
var h3 = document.createElement("h3");
h3.className = "job-title";
h3.textContent = jobs[i].title;
var salarySpan = document.createElement("span");
salarySpan.className = "salary";
salarySpan.textContent = jobs[i].salary;
var companySpan = document.createElement("span");
companySpan.className = "company";
companySpan.textContent = jobs[i].company;
div.appendChild(h3);
div.appendChild(salarySpan);
div.appendChild(companySpan);
container.appendChild(div);
}
</script>
</body>
</html>
【运行】 用浏览器打开 dynamic_page_demo.html,你会看到页面上有 3 条职位信息:AI测试工程师、高级AI测试开发、大模型测试工程师。
然后在浏览器里按 Ctrl+U 查看源码。注意看 <div id="job-list"> 这一行------它是空的 !里面什么都没有。3 条职位数据在哪里?在 <script> 标签里的 JavaScript 代码中。是 JavaScript 在浏览器加载页面后,动态创建了这些 HTML 元素,插入到了页面中。
现在我们用 Python 试试同样的事:
【新建文件】 在项目目录下新建 playwright_demo.py,写入以下代码:
python
"""
Playwright 基础练习
演示动态页面和静态页面的区别
"""
import os
import pathlib
from bs4 import BeautifulSoup
# ========== 实验一:用传统方式读取动态页面 ==========
print("=" * 50)
print("实验一:传统方式(模拟 requests 的行为)")
print("=" * 50)
with open("dynamic_page_demo.html", "r", encoding="utf-8") as f:
html = f.read()
soup = BeautifulSoup(html, "lxml")
items = soup.find_all("div", class_="job-item")
print(f"找到 {len(items)} 个职位") # 输出 0!
print(f"job-list 容器内容:")
job_list = soup.find("div", id="job-list")
print(f" {job_list}") # <div id="job-list"></div>,空的!
【运行】 在终端执行:
bash
python playwright_demo.py
运行结果会告诉你:找到 0 个职位。job-list 容器是空的。
这就是动态页面的核心问题:数据不是存在 HTML 里,而是 JavaScript 在浏览器中动态生成的。requests 只能拿到 HTML 源码,不会执行 JavaScript,所以它看不到 JS 生成的内容。
1.2 JavaScript 是怎么"变"出数据来的
理解了问题,再来看看背后发生了什么。
传统静态页面的工作流程是这样的:
- 浏览器发送请求
- 服务器返回完整的 HTML(数据就在 HTML 里)
- 浏览器解析 HTML,直接显示页面
动态页面的工作流程多了一步:
- 浏览器发送请求
- 服务器返回 HTML 骨架(只有页面结构,没有数据)
- 浏览器解析 HTML,开始执行 JavaScript
- JavaScript 做了两件事之一(或者两件都做):
- 操作 DOM:在 HTML 里动态创建元素、插入数据(就像我们刚才的演示文件)
- 发起 AJAX 请求:偷偷向服务器的 API 接口发请求,拿到 JSON 数据,再插入页面
- JavaScript 把数据插入到页面中,你才看到了内容
这就是为什么你在浏览器里看得到数据,但查看源码却看不到------源码是第 2 步的产物,数据是第 4 步产生的。
真实网站几乎都是第 4 步的第二种方式(AJAX 请求):
比如你在招聘网站搜索"AI测试",页面上的职位列表不是服务器直接返回的 HTML,而是 JavaScript 向一个 API 接口(比如
/api/jobs?keyword=AI测试)发请求,拿到 JSON 数据后再动态渲染到页面上的。这也是为什么前面第三篇提到"用 F12 的 Network 标签找 API 接口"------如果你能找到那个 AJAX 请求的 API,直接调用它拿 JSON 比解析 HTML 方便一百倍。
1.3 那我们该怎么办?
面对动态页面,有三种策略,按优先级排列:
策略一(最优):直接调用 API 接口
用 F12 的 Network 标签找到 JavaScript 发的那个 AJAX 请求,直接用 requests 调用它,拿到 JSON 数据。不需要 Playwright,不需要渲染页面。这是实际工作中最常用的方式。
策略二(次优):用 Playwright 模拟浏览器
找不到 API,或者 API 有复杂的加密参数,那就用 Playwright 打开一个真实的浏览器(无界面模式),等 JavaScript 执行完,再从渲染后的页面中提取数据。
策略三(备用):逆向 JavaScript 逻辑
分析 JS 代码,搞清楚数据加密、参数签名的逻辑,然后用 Python 复现。这是最高级的技巧,需要 JS 逆向能力,后续文章会涉及。
本篇我们重点讲策略二:Playwright,因为它是通用性最强的方案,不管什么动态页面都能处理。
二、Playwright 入门 ------ 像真浏览器一样爬
2.1 Playwright 是什么
Playwright 是微软开源的浏览器自动化工具。和 requests 的本质区别是:
- requests:只发 HTTP 请求,拿到 HTML 源码就完事了,不会执行 JavaScript
- Playwright:启动一个真实的浏览器内核(Chromium / Firefox / WebKit),执行完整的页面加载流程,包括 HTML 解析、CSS 渲染、JavaScript 执行,等页面完全渲染好之后,再让你提取数据
简单说:Playwright 就是用代码控制一个真实浏览器。浏览器能看到什么,它就能拿到什么。
2.2 安装
【运行】 在终端依次执行以下命令:
bash
pip install playwright
playwright install chromium
第一条安装 Python 库,第二条下载 Chromium 浏览器内核(大约 100 多 MB,需要一点时间)。为什么还要下载浏览器?因为 Playwright 需要真实浏览器来渲染页面,光有 Python 库不够。
如果网络慢,可以用国内镜像
bash
python -m pip install playwright -i https://pypi.tuna.tsinghua.edu.cn/simple
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright"
python -m playwright install chromium
2.3 第一次使用 Playwright
【追加到 playwright_demo.py】 在上一段代码末尾继续添加:
python
# ========== 实验二:用 Playwright 渲染动态页面 ==========
print("\n" + "=" * 50)
print("实验二:Playwright 渲染后提取")
print("=" * 50)
from playwright.sync_api import sync_playwright
# 获取 HTML 文件的绝对路径(Playwright 需要完整路径)
file_path = pathlib.Path("dynamic_page_demo.html").resolve()
file_url = file_path.as_uri()
with sync_playwright() as p:
# 启动浏览器(headless=True 表示无界面模式,不会弹出浏览器窗口)
browser = p.chromium.launch(headless=True)
# 新建一个浏览器标签页
page = browser.new_page()
# 打开本地 HTML 文件
page.goto(file_url)
# 等待 JavaScript 执行完成,页面中出现 .job-item 元素
page.wait_for_selector(".job-item")
# 获取渲染后的完整 HTML(JavaScript 已执行,DOM 已更新)
rendered_html = page.content()
# 关闭浏览器
browser.close()
# 用 BeautifulSoup 解析渲染后的 HTML
soup = BeautifulSoup(rendered_html, "lxml")
items = soup.find_all("div", class_="job-item")
print(f"找到 {len(items)} 个职位") # 输出 3!
for item in items:
title = item.find("h3").text
salary = item.find("span", class_="salary").text
company = item.find("span", class_="company").text
print(f" {title} | {salary} | {company}")
【运行】 在终端执行 python playwright_demo.py。
对比两次输出:
实验一(传统方式):找到 0 个职位
实验二(Playwright):找到 3 个职位
同样的 HTML 文件,同样用 BeautifulSoup 解析。区别只在于:Playwright 先让浏览器执行了 JavaScript,把动态生成的 DOM 元素都渲染好了,然后再把渲染后的完整 HTML 给你。这时候 BeautifulSoup 当然能找到数据了。
2.4 Playwright 核心 API 详解
刚才的代码用到了 Playwright 的几个核心 API,逐个说清楚。
启动浏览器和创建页面:
python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 启动浏览器
# headless=True: 无界面模式,后台运行(爬虫用这个)
# headless=False: 有界面模式,会弹出浏览器窗口(调试用这个)
browser = p.chromium.launch(headless=True)
# 创建新标签页
page = browser.new_page()
# ... 你的操作 ...
# 用完记得关闭
browser.close()
with sync_playwright() as p 是一个上下文管理器,保证资源被正确释放。p.chromium 是 Chromium 浏览器,也可以用 p.firefox 或 p.webkit。一般用 Chromium 就够了。
导航到页面:
python
# 打开网页
page.goto("https://quotes.toscrape.com/js/")
# 打开本地文件
page.goto("file:///path/to/local/file.html")
# goto 的常用参数
page.goto("https://example.com", timeout=30000) # 超时30秒(毫秒)
page.goto("https://example.com", wait_until="networkidle") # 等到网络空闲
wait_until 参数控制什么时候认为页面"加载完成":
load:等 HTML、CSS、图片加载完(默认)networkidle:等网络 500ms 没有新请求(JS 动态加载数据时推荐用这个)domcontentloaded:等 DOM 解析完就行,最快
等待元素出现(关键!):
python
# 等待某个元素出现在页面上(最多等 10 秒)
page.wait_for_selector(".job-item", timeout=10000)
# 等待某个元素消失
page.wait_for_selector(".loading-spinner", state="hidden")
# 等待固定时间(不推荐,但偶尔有用)
page.wait_for_timeout(2000) # 等 2 秒
为什么需要等待?因为 JavaScript 是异步执行的。页面加载完不代表 JS 执行完了,数据可能还没插入到 DOM 中。wait_for_selector 会一直等到目标元素出现为止,比 wait_for_timeout 固定等待更可靠。
获取页面内容:
python
# 获取渲染后的完整 HTML 字符串
html = page.content()
# 截图(调试用)
page.screenshot(path="debug.png")
# 获取当前 URL
print(page.url)
page.content() 返回的是渲染后的 HTML------JavaScript 已经执行完、DOM 已经更新完的完整页面代码。这就是我们拿去给 BeautifulSoup 解析的东西。
提取元素数据:
python
# 查找单个元素
element = page.query_selector(".job-title")
print(element.text_content()) # 获取文本
print(element.get_attribute("href")) # 获取属性
# 查找所有匹配元素
elements = page.query_selector_all(".job-item")
for el in elements:
title = el.query_selector(".job-title").text_content()
print(title)
# 也可以用 CSS 选择器
element = page.query_selector("div.job-list > div.job-item:first-child")
这里有一个选择:你可以用 Playwright 自己的 API 提取数据(如上面的 query_selector),也可以用 page.content() 拿到 HTML 后交给 BeautifulSoup 解析。
两种方式怎么选:
- 简单页面、字段少 → 用 Playwright 的
query_selector直接提取 - 结构复杂、字段多、需要嵌套查找 → 拿 HTML 给 BeautifulSoup 解析(更灵活)
我推荐后者:用 Playwright 负责渲染页面,用 BeautifulSoup 负责解析数据。各司其职,代码更清晰。
三、实战:Playwright 爬取动态页面
现在用 Playwright 爬一个真实的动态页面。
3.1 目标分析
目标是 https://quotes.toscrape.com/js/ 。这是上一篇练习用的网站的 JavaScript 版本------同样的名人名言数据,但这次是通过 JavaScript 动态加载的。
你可以现在就试试:在浏览器中打开这个页面,看到名言内容。然后按 Ctrl+U 查看源码------你会发现源码里完全没有名言数据 ,只有一段 <script> 标签去加载一个 JS 文件,由 JS 文件来动态渲染页面。
这和我们上一篇爬的 quotes.toscrape.com 是完全不同的挑战。上一篇用 requests + BeautifulSoup 就能搞定,这次必须用 Playwright。
3.2 完整代码
【新建文件】 在项目目录下新建 dynamic_scraper.py,写入以下代码:
python
"""
动态页面爬虫实战
用 Playwright 渲染 JavaScript 动态页面,提取名言数据
目标:https://quotes.toscrape.com/js/
特点:数据由 JavaScript 动态加载,HTML 源码中没有数据
"""
import json
import time
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
from urllib.parse import urljoin
def scrape_quotes_with_playwright(base_url, max_pages=5):
"""
用 Playwright 爬取动态加载的名言数据
参数:
base_url: 目标网址
max_pages: 最大爬取页数
返回:
所有名言数据的列表
"""
all_quotes = []
with sync_playwright() as p:
# 启动无界面浏览器
browser = p.chromium.launch(headless=True)
page = browser.new_page()
current_url = base_url
page_num = 1
while current_url and page_num <= max_pages:
print(f"\n[第 {page_num} 页] {current_url}")
# 打开页面
page.goto(current_url)
# 等待 JavaScript 渲染完成,页面中出现 .quote 元素
page.wait_for_selector(".quote", timeout=10000)
# 获取渲染后的 HTML
rendered_html = page.content()
# 用 BeautifulSoup 解析渲染后的 HTML
soup = BeautifulSoup(rendered_html, "lxml")
quote_divs = soup.find_all("div", class_="quote")
print(f" 找到 {len(quote_divs)} 条名言")
for div in quote_divs:
text_el = div.find("span", class_="text")
author_el = div.find("small", class_="author")
tag_els = div.find_all("a", class_="tag")
all_quotes.append({
"text": text_el.get_text(strip=True) if text_el else "",
"author": author_el.get_text(strip=True) if author_el else "未知",
"tags": [t.get_text(strip=True) for t in tag_els]
})
# 查找下一页按钮
next_btn = soup.find("li", class_="next")
if next_btn:
next_a = next_btn.find("a")
if next_a and next_a.get("href"):
# 用 page.url(当前页面实际 URL)作为基准
# 并确保以 / 结尾,这样相对路径才能正确解析
base = page.url if page.url.endswith("/") else page.url + "/"
current_url = urljoin(base, next_a["href"])
print(f" 下一页: {current_url}")
else:
current_url = None
else:
current_url = None
page_num += 1
if current_url:
time.sleep(1)
# 关闭浏览器
browser.close()
return all_quotes
# ========== 主程序 ==========
if __name__ == "__main__":
BASE_URL = "https://quotes.toscrape.com/js"
print("=" * 60)
print("动态页面爬虫启动(Playwright 版)")
print("=" * 60)
quotes = scrape_quotes_with_playwright(BASE_URL, max_pages=5)
# 打印结果
print(f"\n{'=' * 60}")
print(f"爬取完成!共获取 {len(quotes)} 条名言")
print(f"{'=' * 60}")
for i, q in enumerate(quotes[:5], 1):
print(f"\n [{i}] {q['text'][:70]}...")
print(f" 作者:{q['author']}")
print(f" 标签:{', '.join(q['tags'])}")
# 保存数据
with open("quotes_dynamic.json", "w", encoding="utf-8") as f:
json.dump(quotes, f, ensure_ascii=False, indent=2)
print(f"\n数据已保存到 quotes_dynamic.json")
【运行】 在终端执行:
bash
python dynamic_scraper.py
3.3 运行结果
============================================================
动态页面爬虫启动(Playwright 版)
============================================================
[第 1 页] https://quotes.toscrape.com/js
找到 10 条名言
[第 2 页] https://quotes.toscrape.com/js/page/2/
找到 10 条名言
[第 3 页] https://quotes.toscrape.com/js/page/3/
找到 10 条名言
[第 4 页] https://quotes.toscrape.com/js/page/4/
找到 10 条名言
[第 5 页] https://quotes.toscrape.com/js/page/5/
找到 10 条名言
============================================================
爬取完成!共获取 50 条名言
============================================================
[1] "The world as we have created it is a process of our thinking. It ...
作者:Albert Einstein
标签:change, deep-thoughts, thinking, world
...
数据已保存到 quotes_dynamic.json
3.4 和静态版本的代码对比
你可能注意到了:这段代码的解析逻辑和上一篇静态版本的几乎一模一样。唯一的区别在于数据来源:
| 环节 | 静态页面(上一篇) | 动态页面(本篇) |
|---|---|---|
| 获取 HTML | requests.get(url) |
page.goto(url) + page.content() |
| 解析数据 | BeautifulSoup | BeautifulSoup(完全一样) |
| 翻页 | requests 请求下一页 URL | Playwright 打开下一页 URL |
| 等待 | 不需要 | wait_for_selector() 等 JS 执行完 |
这就是我说"Playwright 负责渲染、BeautifulSoup 负责解析"的好处------解析代码完全复用,只需要换掉数据获取方式。
四、Scrapy 框架入门 ------ 企业级爬虫怎么写
4.1 什么时候需要 Scrapy
前面我们写的爬虫都是单文件脚本,几十行到几百行。当你需要爬取的规模变大------比如要爬几十个网站、几万个页面、需要并发请求、自动去重、中间件处理、数据入库------单文件脚本就撑不住了。这时候需要一个爬虫框架来帮你管理复杂的爬虫工程。
Scrapy 是 Python 生态中最成熟的爬虫框架,也是企业中最常用的。它帮你解决了:
- 并发请求:自动管理多个请求同时发出去,不用自己写线程/协程
- 请求去重:自动记录已爬过的 URL,不会重复请求
- 自动限速:可配置的请求间隔,不会把目标服务器打崩
- 中间件:可以在请求发出前/响应返回后插入自定义逻辑(比如自动加代理、自动换 UA)
- 数据管道:爬到的数据自动流入处理管道(清洗、验证、存储到数据库)
- 断点续爬:中断后可以从上次的地方继续
4.2 Scrapy 的架构
Scrapy 的工作流程涉及几个核心组件,我用一个简单的流程来说明:
- Spider(爬虫):你写的爬虫代码,定义"从哪个 URL 开始、怎么提取数据、怎么找下一页"
- Scheduler(调度器):管理待爬取的 URL 队列,自动去重
- Downloader(下载器):负责发送 HTTP 请求、接收响应
- Middleware(中间件):在请求和响应之间做加工处理(加代理、换 UA、处理 Cookie)
- Item Pipeline(数据管道):处理 Spider 提取出来的数据(清洗、存储、导出)
工作流程:
Spider 给出初始 URL → 调度器排队 → 下载器发送请求拿到响应 → 响应交回 Spider → Spider 提取数据和新的 URL → 新 URL 回到调度器排队 → 提取的数据流入 Pipeline 处理
你作为开发者,主要写三个东西:Spider (怎么爬)、Item (数据格式)、Pipeline(怎么存)。其他组件 Scrapy 帮你管。
4.3 创建 Scrapy 项目
【运行】 在终端执行以下命令创建 Scrapy 项目:
bash
pip install scrapy
scrapy startproject scrapy_quotes
如果慢,可以用镜像
bash
pip install scrapy -i https://pypi.tuna.tsinghua.edu.cn/simple
scrapy startproject scrapy_quotes
第一条安装 Scrapy 框架,第二条自动生成项目骨架。执行完后,你的项目目录下会多出一个 scrapy_quotes/ 文件夹,结构是这样的:
scrapy_quotes/(项目根目录)
├── scrapy.cfg(部署配置,不用改)
└── scrapy_quotes/(项目主目录)
├── init.py
├── items.py(数据模型定义在这里)
├── middlewares.py(中间件,本篇不用改)
├── pipelines.py(数据处理管道,后面要改)
├── settings.py(项目配置,后面要改)
└── spiders/
├── init.py
└── (你的爬虫文件放在这里,接下来要创建)
Scrapy 自动帮你生成了这些文件的模板,我们只需要修改其中几个。
4.4 定义数据模型(Item)
Item 定义了你爬取的数据长什么样,相当于数据的"模板"。
【替换 scrapy_quotes/scrapy_quotes/items.py 的内容】 把这个文件的内容替换为:
python
"""
数据模型定义
定义爬取的名言数据包含哪些字段
"""
import scrapy
class QuoteItem(scrapy.Item):
"""名言数据模型"""
text = scrapy.Field() # 名言内容
author = scrapy.Field() # 作者
tags = scrapy.Field() # 标签列表
这里用 scrapy.Field() 定义了三个字段。Item 的作用是规范数据格式------你提取出来的数据会按照这个模板封装,后面的 Pipeline 也按这个格式来处理。
为什么用 Item 而不是直接用字典?两个原因:一是 Item 有类型检查,防止你写错字段名;二是 Item 和 Scrapy 的 Pipeline、导出功能无缝配合,用字典反而不方便。
4.5 编写爬虫(Spider)
Spider 是 Scrapy 项目的核心,定义了"从哪开始爬、怎么提取数据、怎么找下一页"。
【新建文件】 在 scrapy_quotes/scrapy_quotes/spiders/ 目录下新建 quotes_spider.py,写入以下代码:
python
"""
名言爬虫 Spider
爬取 quotes.toscrape.com 的名人名言数据
"""
import scrapy
from scrapy_quotes.items import QuoteItem
class QuotesSpider(scrapy.Spider):
# 爬虫名称(运行时用来指定跑哪个爬虫)
name = "quotes"
# 允许爬取的域名(防止爬到别的网站去)
allowed_domains = ["quotes.toscrape.com"]
# 起始 URL(从这里开始爬)
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
"""
解析页面,提取数据和下一页链接
Scrapy 会自动调用这个方法来处理每个响应
"""
# 提取当前页所有名言
quote_divs = response.css("div.quote")
for div in quote_divs:
item = QuoteItem()
item["text"] = div.css("span.text::text").get()
item["author"] = div.css("small.author::text").get()
item["tags"] = div.css("a.tag::text").getall()
yield item
# 查找下一页并继续爬取
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
这段代码虽然短,但每一行都有讲究:
name = "quotes" :爬虫的名字,运行时通过 scrapy crawl quotes 来指定。
allowed_domains:安全限制,确保爬虫只在这个域名内活动,不会意外爬到其他网站。
start_urls :爬虫的入口,Scrapy 会自动请求这些 URL,然后把响应交给 parse() 方法。
response.css() :Scrapy 的 Response 对象自带 CSS 选择器方法,不用额外引入 BeautifulSoup。::text 提取文本内容,::attr(href) 提取属性值,get() 取第一个,getall() 取全部。
yield item:把提取的数据"推"给 Pipeline 处理。
response.follow(next_page, callback=self.parse) :生成下一页的请求,交给 Scrapy 调度器。callback=self.parse 表示拿到响应后继续调用 parse() 方法处理。这就是 Scrapy 的自动翻页机制------你只需要告诉它"下一页在哪里",它会自动排队、去重、请求。
4.6 数据处理管道(Pipeline)
Pipeline 接收 Spider 提取的数据,负责清洗和存储。
【替换 scrapy_quotes/scrapy_quotes/pipelines.py 的内容】 把这个文件的内容替换为:
python
"""
数据处理管道
接收 Spider 提取的数据,负责清洗和存储
"""
import json
class JsonExportPipeline:
"""将数据导出为 JSON 文件的管道"""
def open_spider(self, spider):
"""爬虫启动时调用,打开文件"""
self.file = open("quotes_scrapy.json", "w", encoding="utf-8")
self.data = []
def close_spider(self, spider):
"""爬虫结束时调用,保存并关闭文件"""
json.dump(self.data, self.file, ensure_ascii=False, indent=2)
self.file.close()
spider.logger.info(f"数据已保存,共 {len(self.data)} 条")
def process_item(self, item, spider):
"""
每提取到一条数据就会调用一次
可以在这里做数据清洗、验证、存储
"""
# 简单的数据清洗
cleaned_item = {
"text": item.get("text", "").strip(),
"author": item.get("author", "未知").strip(),
"tags": item.get("tags", [])
}
# 如果数据不合法就丢弃
if not cleaned_item["text"]:
spider.logger.warning(f"丢弃无效数据:{cleaned_item}")
return None # 返回 None 表示丢弃这条数据
self.data.append(cleaned_item)
return cleaned_item
Pipeline 的三个方法对应三个生命周期节点:
open_spider():爬虫启动时调用一次,用来初始化(打开文件、连接数据库等)process_item():每提取到一条数据调用一次,用来处理数据(清洗、验证、存储)close_spider():爬虫结束时调用一次,用来收尾(写入文件、关闭数据库连接等)
process_item() 返回 None 表示丢弃这条数据,返回 item 表示继续传给下一个 Pipeline。这里我们做了简单清洗和有效性检查,无效数据直接丢弃。
4.7 修改项目配置
【追加到 scrapy_quotes/scrapy_quotes/settings.py】 在文件末尾追加以下配置:
python
# ========== 自定义配置 ==========
# 激活我们的数据管道(数字越小越先执行)
ITEM_PIPELINES = {
"scrapy_quotes.pipelines.JsonExportPipeline": 300,
}
# 请求间隔(秒),礼貌爬取
DOWNLOAD_DELAY = 1
# 遵守 robots.txt 规则
ROBOTSTXT_OBEY = True
# 设置 User-Agent
DEFAULT_REQUEST_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
}
# 控制并发数(默认是 16,调小一点更礼貌)
CONCURRENT_REQUESTS = 4
# 日志级别
LOG_LEVEL = "INFO"
这些配置的含义:
- ITEM_PIPELINES:激活我们写的数据管道。数字 300 是优先级,数字越小越先执行(如果你有多个管道,比如一个清洗、一个存数据库,可以用不同数字控制顺序)
- DOWNLOAD_DELAY:每次请求之间间隔 1 秒,别把人家服务器打崩了
- ROBOTSTXT_OBEY:遵守 robots.txt 规则,这是基本的行业规范
- CONCURRENT_REQUESTS:同时发 4 个请求,不是 16 个,更保守更安全
4.8 运行 Scrapy 项目
【运行】 在终端执行:
bash
cd scrapy_quotes
scrapy crawl quotes
第一条进入项目目录,第二条启动名为 "quotes" 的爬虫。你会看到 Scrapy 的日志输出:
2026-09-14 10:30:00 [scrapy.core.engine] INFO: Spider opened
2026-09-14 10:30:01 [scrapy.core.engine] INFO: Crawled (200) <GET https://quotes.toscrape.com/>
2026-09-14 10:30:02 [scrapy.core.engine] INFO: Crawled (200) <GET https://quotes.toscrape.com/page/2/>
...
2026-09-14 10:30:20 [scrapy_quotes.pipelines] INFO: 数据已保存,共 100 条
2026-09-14 10:30:20 [scrapy.core.engine] INFO: Spider closed (finished)
爬取完成后,项目目录下会生成 quotes_scrapy.json,包含 100 条名言数据(10 页 × 每页 10 条)。
Scrapy 自动完成了:并发请求、URL 去重、自动翻页、数据流入 Pipeline、最终导出 JSON。这些如果用原生代码写,至少几百行。
4.9 什么时候用 Scrapy,什么时候用 requests / Playwright
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 爬几个页面,一次性脚本 | requests + BeautifulSoup | 简单直接,不用装框架 |
| 动态页面,偶尔爬一下 | Playwright | 渲染 JS,简单够用 |
| 大规模爬虫,持续运行 | Scrapy | 并发、去重、中间件、Pipeline 全都有 |
| 需要登录 + 动态渲染 + 大规模 | Scrapy + Playwright 中间件 | 高阶组合,后续文章会讲 |
简单说:小爬虫用 requests,动态页面用 Playwright,大项目用 Scrapy。
五、反爬进阶
前面几篇提到了基础的反爬应对(加 User-Agent、加延迟),这里补充几个更高级的策略。
5.1 User-Agent 轮换
单个 User-Agent 用久了也可能被识别为异常。维护一个 UA 列表,每次请求随机选一个。
【只看不跑】 独立的 UA 轮换代码示例:
python
import random
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
]
def get_random_headers():
return {"User-Agent": random.choice(USER_AGENTS)}
# 使用
response = requests.get(url, headers=get_random_headers())
在 Scrapy 中,可以在中间件里实现自动轮换,每个请求自动换一个 UA,更优雅。
5.2 代理 IP
当你请求太频繁,服务器会封你的 IP。用代理 IP 就是"换一个身份继续爬"。
【只看不跑】 独立的代理 IP 代码示例:
python
import requests
# 代理 IP 格式:协议://IP:端口
proxies = {
"http": "http://123.45.67.89:8080",
"https": "http://123.45.67.89:8080",
}
response = requests.get(url, headers=headers, proxies=proxies)
代理 IP 的来源:
- 免费代理:网上有很多免费代理 IP 列表,但稳定性和速度都很差,仅适合学习
- 付费代理:商业代理服务,稳定可靠,按量或按时间计费
- 自建代理池:自己维护一组代理服务器,成本高但完全可控
实际工作中一般用付费代理或者自建代理池。Scrapy 中可以用中间件实现自动切换代理。
5.3 请求频率控制
【只看不跑】 独立的频率控制代码示例:
python
import time
import random
# 最简单:固定延迟
time.sleep(1)
# 更好:随机延迟(不容易被模式识别)
time.sleep(random.uniform(1, 3))
# 进阶:根据响应状态动态调整
response = requests.get(url, headers=headers)
if response.status_code == 429:
# 被限流了,等久一点
print("被限流了,等待 10 秒...")
time.sleep(10)
elif response.status_code == 200:
# 正常,短暂等待
time.sleep(random.uniform(1, 2))
5.4 综合应对策略速查
| 反爬手段 | 应对策略 | 难度 | 适合用在 |
|---|---|---|---|
| 检查 User-Agent | 轮换 UA 列表 | ★☆☆ | 所有项目 |
| 频率限制 | 随机延迟 + 动态调整 | ★☆☆ | 所有项目 |
| Cookie / 登录墙 | Session 保持登录态 | ★★☆ | 需要登录的网站 |
| JS 动态渲染 | Playwright 渲染 | ★★☆ | 动态页面 |
| IP 封禁 | 代理 IP 轮换 | ★★★ | 大规模爬取 |
| 验证码 | 打码平台 / 手动 | ★★★ | 有验证码的网站 |
| JS 加密参数 | JS 逆向工程 | ★★★★ | 高防护网站 |
根据你的爬取规模和目标网站的防护等级,选择合适的组合。一般项目,前四项就够了。
六、本篇小结
本篇的核心收获:
- 动态页面原理:JavaScript 通过 DOM 操作或 AJAX 请求动态加载数据,HTML 源码中看不到
- Playwright 核心能力:启动真实浏览器、渲染 JS、等待元素、获取渲染后的 HTML
- Playwright + BeautifulSoup 组合:Playwright 负责渲染,BeautifulSoup 负责解析,各司其职
- 动态页面实战:完整跑通了 Playwright 爬取 JS 渲染页面的流程
- Scrapy 框架:理解了 Spider / Item / Pipeline 三大核心组件的职责和协作方式
- Scrapy 实战:从创建项目到运行,完整跑通了一个 Scrapy 爬虫
- 反爬进阶:UA 轮换、代理 IP、动态频率控制
到这里,你的爬虫能力工具箱已经齐了:
- requests + BeautifulSoup → 静态页面(简单、快速)
- Playwright → 动态页面(渲染 JS)
- Scrapy → 大规模爬虫(工程化)
接下来,我们不再继续爬虫了。爬到的那些岗位数据已经在手里了,该用 Pandas 来做深入的数据清洗和分析了。
下一篇:【测AI】第06篇:数据清洗实战 ------ Pandas 处理爬取的 JD 数据
- Pandas 核心概念:DataFrame 和 Series
- 读取爬取的 JSON / CSV 数据
- 数据清洗:缺失值处理、格式统一、异常值检测
- 数据转换:薪资解析、技能标签拆分
- 数据分析:分组统计、交叉分析
- 产出一份完整的"AI 测试岗位技能需求分析报告"