【测AI】第05篇:Python 爬虫进阶 —— 动态页面爬取与 Scrapy 框架

写在前面

上一篇我们完成了完整的岗位数据爬虫,但有一个前提假设:数据直接在 HTML 源码里。

问题是,现实中很多网站根本不是这样的。你在浏览器里明明白白看到一堆职位数据,按 Ctrl+U 查看源码,里面却空空如也。你用 requests 去请求,拿回来的 HTML 里找不到任何有用的数据。这类页面叫动态页面,需要用完全不同的技术来处理。

本篇的目标:

  1. 理解动态页面的工作原理(为什么源码里看不到数据)
  2. 掌握 Playwright 库,爬取需要 JS 渲染的页面
  3. 了解 Scrapy 框架,写出企业级的爬虫项目
  4. 掌握更高级的反爬应对策略

本篇文件结构

文件名 用途 创建方式
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 是怎么"变"出数据来的

理解了问题,再来看看背后发生了什么。

传统静态页面的工作流程是这样的:

  1. 浏览器发送请求
  2. 服务器返回完整的 HTML(数据就在 HTML 里)
  3. 浏览器解析 HTML,直接显示页面

动态页面的工作流程多了一步:

  1. 浏览器发送请求
  2. 服务器返回 HTML 骨架(只有页面结构,没有数据)
  3. 浏览器解析 HTML,开始执行 JavaScript
  4. JavaScript 做了两件事之一(或者两件都做):
    • 操作 DOM:在 HTML 里动态创建元素、插入数据(就像我们刚才的演示文件)
    • 发起 AJAX 请求:偷偷向服务器的 API 接口发请求,拿到 JSON 数据,再插入页面
  5. 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 的工作流程涉及几个核心组件,我用一个简单的流程来说明:

  1. Spider(爬虫):你写的爬虫代码,定义"从哪个 URL 开始、怎么提取数据、怎么找下一页"
  2. Scheduler(调度器):管理待爬取的 URL 队列,自动去重
  3. Downloader(下载器):负责发送 HTTP 请求、接收响应
  4. Middleware(中间件):在请求和响应之间做加工处理(加代理、换 UA、处理 Cookie)
  5. 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 测试岗位技能需求分析报告"
相关推荐
凡泰极客科技1 小时前
从数据质检到运行审计:金融AI智能体如何安全且合规落地?
人工智能·安全·金融
空堂与归1 小时前
320B MoE 开源:IQuest-Q1 重构智能体编码
人工智能·重构·开源
CAIE研习社1 小时前
传统电气岗位与AI的适配度:一次设计、现场、运维、算法分别看
人工智能
张彦峰ZYF1 小时前
从 ABC Legal 的 Managed Agents 实践,看企业如何把零散自动化变成可审计、可进化、可计算的生产系统
人工智能·自动化·prompt·agent·abc legal·managed agents·agent 平台
Token架构师1 小时前
给 Claude Code 套一层「进程外」策略:OpenShell 落地与审计日志分析
人工智能·安全·架构
huisheng_qaq1 小时前
【Python基础篇-07】深入理解python的面向对象编程
python·多态·继承·面向对象编程·封装
FanetheDivine1 小时前
学习python 2.语法速览
python
TMT星球1 小时前
旗舰手机的中场战事:AI为核,影像为王
人工智能·智能手机
霸道流氓气质1 小时前
AI应用成本优化完全指南:Token压缩、语义缓存与模型路由的Java生产级实战
java·人工智能·缓存