四重采集通道

上一讲我们已经知道:

爬虫的核心问题不是“怎么写代码”,而是先判断: 数据到底在哪里?

这一讲就围绕这个问题展开。

同样是抓职位信息,数据可能出现在四个地方:

HTML 页面里
JavaScript 渲染后的页面里
接口 JSON 里
浏览器偷偷请求到的接口响应里

所以就对应四种采集通道:

1. 静态 HTML 采集
2. 动态页面采集
3. API 直连采集
4. on_response 响应监听采集

1. 第一重:静态 HTML 采集

1.1 它是什么?

静态 HTML 采集,就是:

直接请求网页源码
↓
拿到 HTML
↓
从 HTML 里提取数据

这是最传统、最基础的爬虫方式。

比如网页源码里直接有:

<div class="job">
  <h2>数据分析师</h2>
  <span class="company">某科技公司</span>
  <span class="salary">15k-25k</span>
</div>

那我们就可以直接从 HTML 里提取:

职位名称:数据分析师
公司名称:某科技公司
薪资:15k-25k

1.2 生活化理解

你可以把它理解为:

网页把菜单直接贴在墙上
你拍一张照片
然后从照片里抄菜名

数据已经在页面源码里了,不需要点击,不需要等待,不需要浏览器执行 JavaScript。

1.3 最小代码长什么样?

import requests
from bs4 import BeautifulSoup

url = "https://example.com/jobs"

response = requests.get(url)
html = response.text

soup = BeautifulSoup(html, "html.parser")

jobs = soup.select(".job")

for job in jobs:
    title = job.select_one("h2").get_text(strip=True)
    company = job.select_one(".company").get_text(strip=True)
    salary = job.select_one(".salary").get_text(strip=True)

    print(title, company, salary)

这段代码做了几件事:

requests.get(url)        请求网页
response.text            拿到 HTML 字符串
BeautifulSoup(html)      把 HTML 变成可查询对象
soup.select(".job")      找到所有职位块
select_one("h2")         找职位标题
get_text(strip=True)     提取干净文本

示例

import requests
from bs4 import BeautifulSoup

url = "http://www.zwwwblog.top/share/category_%E5%BE%AE%E6%9C%8D%E5%8A%A1"

response = requests.get(url)
html = response.text

soup = BeautifulSoup(html, "html.parser")

blogs = soup.select(".blogItemsMainContain")

print(f"共找到 {len(blogs)} 条内容\n")
print("=" * 80)

for i, blog in enumerate(blogs, 1):
    title = blog.select_one("#blogItemTitle").get_text(strip=True)
    Summary = blog.select_one("#blogItemSummary").get_text(strip=True)
    img_tag = blog.select_one("#blogItemSummaryImgContain img")
    ImgContain = img_tag['src'] if img_tag else ''

    print(f"\n第 {i} 条:")
    print(f"标题:{title}")
    print(f"简介:{Summary}")
    print(f"头图:{ImgContain}")
    print("-" * 80)
2-Learning/05-项目/03-Python与爬虫/07-爬虫课程六讲/assets/image-b50e4cf1 2-Learning/05-项目/03-Python与爬虫/07-爬虫课程六讲/assets/image-34cff48c

1.4 它适合什么场景?

适合:

学校就业网公告列表
简单企业官网招聘页
服务端直接渲染的列表页
不需要登录、不需要点击、不需要滚动的页面

比如很多学校就业网站,列表和详情内容直接在 HTML 里,这种就可以优先考虑静态 HTML 采集。

通用项目里的学校采集链路,本质上就是“列表页解析 + 明细页抓取”的两段式采集:先抽取列表容器 HTML,再进入详情页提取正文内容。

1.5 优点和缺点

优点:

速度快
代码简单
资源消耗小
不需要打开浏览器
适合大批量抓取

缺点:

遇到 JavaScript 动态加载会抓不到数据
遇到复杂点击、滚动、分页不方便
页面结构一变,解析规则容易失效

2. 第二重:动态页面采集

2.1 它是什么?

动态页面采集,就是用程序控制一个真实浏览器。

常见工具是:

Playwright
Selenium

我们现在重点看 Playwright,因为已有项目大量使用它。

动态页面采集流程是:

启动浏览器
↓
打开网页
↓
等待页面加载
↓
点击筛选项
↓
滚动页面
↓
点击下一页或加载更多
↓
从渲染后的页面里提取数据

2.2 生活化理解

静态 HTML 采集像是“直接看纸质菜单”。

动态页面采集像是:

你真的进了一家餐厅
点开菜单
点击分类
翻页
展开详情
再把内容抄下来

它更像真人操作浏览器。

2.3 最小代码长什么样?

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    page.goto("https://example.com/jobs")

    # 点击“加载更多”
    page.click("text=加载更多")

    # 等待职位卡片出现
    page.wait_for_selector(".job-card")

    jobs = page.query_selector_all(".job-card")

    for job in jobs:
        title = job.query_selector(".title").inner_text()
        company = job.query_selector(".company").inner_text()
        print(title, company)

    browser.close()

这段代码对应的动作是:

launch()              启动浏览器
new_page()            打开新标签页
goto()                访问网页
click()               点击按钮
wait_for_selector()   等待某个元素出现
query_selector_all()  找到多个元素
inner_text()          提取页面文字

示例

import time
from playwright.sync_api import sync_playwright

# 目标网站地址
url = "http://www.zwwwblog.top/share/Index"

# 启动 Playwright 浏览器自动化框架
with sync_playwright() as p:
    # 启动 Chromium 浏览器,headless=False 表示显示浏览器窗口
    browser = p.chromium.launch(headless=False)
    # 创建一个新的浏览器标签页
    page = browser.new_page()

    print("正在访问网页...")
    try:
        # 访问网站首页,设置60秒超时,DOM加载完成即可继续
        page.goto(url, timeout=60000, wait_until="domcontentloaded")
        print("网页加载成功!")
    except Exception as e:
        # 如果页面加载失败,关闭浏览器并退出程序
        print(f"网页加载失败: {e}")
        browser.close()
        exit()

    print("等待导航栏加载...")
    try:
        # 等待导航栏的"分类"按钮出现,最多等待10秒
        page.wait_for_selector("#category", timeout=10000)
        print("导航栏加载成功!")
    except Exception as e:
        # 如果导航栏加载失败,关闭浏览器并退出程序
        print(f"导航栏加载失败: {e}")
        browser.close()
        exit()

    print("\n鼠标悬浮到'分类'菜单...")
    # 获取"分类"菜单元素
    category_menu = page.query_selector("#category")
    # 模拟鼠标悬浮操作,触发下拉菜单显示
    category_menu.hover()

    # 等待1.5秒,让下拉菜单动画完成
    time.sleep(1.5)

    print("获取所有分类选项...")
    # 获取下拉菜单中所有的分类链接元素
    categories = page.query_selector_all("#categoryDropDown a.menuLinkStyle")

    # 存储分类信息的列表
    category_list = []
    # 遍历所有分类元素,提取名称和链接
    for category in categories:
        # 提取分类名称,去除空白并按换行符分割,取第一行
        category_name = category.inner_text().strip().split('\n')[0]
        # 获取分类链接的 href 属性
        category_href = category.get_attribute("href")
        # 将分类信息保存到字典中
        category_list.append({
            'name': category_name,
            'href': category_href
        })

    print(f"共找到 {len(category_list)} 个分类\n")

    # 遍历所有分类,逐个抓取内容
    for idx, cat_info in enumerate(category_list, 1):
        # 获取当前分类的名称和链接
        category_name = cat_info['name']
        category_href = cat_info['href']

        print("=" * 80)
        print(f"\n正在抓取第 {idx}/{len(category_list)} 个分类: {category_name}")
        print(f"分类链接: {category_href}")
        print("-" * 80)

        try:
            # 重新获取"分类"菜单元素(避免元素引用失效)
            category_menu = page.query_selector("#category")
            # 悬浮到分类菜单,显示下拉列表
            category_menu.hover()
            # 等待1秒让下拉菜单完全显示
            time.sleep(1)

            # 根据 href 属性精确定位要点击的分类链接
            category_link = page.query_selector(f"a[href='{category_href}']")
            # 点击该分类链接
            category_link.click()

            print("等待内容加载...")
            # 等待笔记列表容器加载完成,最多等待30秒
            page.wait_for_selector(".blogItemsMainContain", timeout=30000)
            # 额外等待2秒,确保所有内容完全渲染
            time.sleep(2)

            # 获取当前分类下所有的笔记卡片元素
            blogs = page.query_selector_all(".blogItemsMainContain")
            print(f"\n该分类共找到 {len(blogs)} 条内容:\n")

            # 遍历当前分类的所有笔记
            for i, blog in enumerate(blogs, 1):
                # 查找标题元素
                title_elem = blog.query_selector("#blogItemTitle")
                # 查找简介元素
                summary_elem = blog.query_selector("#blogItemSummary")
                # 查找头图 img 元素
                img_container = blog.query_selector("#blogItemSummaryImgContain img")

                # 提取标题文本,如果元素不存在则返回默认值
                title = title_elem.inner_text().strip() if title_elem else '无标题'
                # 提取简介文本,如果元素不存在则返回默认值
                summary = summary_elem.inner_text().strip() if summary_elem else '无简介'
                # 获取图片链接,如果元素不存在则返回默认值
                img_src = img_container.get_attribute("src") if img_container else '无头图'

                # 格式化输出笔记信息
                print(f"  [{i}] 标题: {title}")
                # 如果简介超过50字,截断显示
                if len(summary) > 50:
                    print(f"      简介: {summary[:50]}...")
                else:
                    print(f"      简介: {summary}")
                print(f"      头图: {img_src}")
                print()

            print(f"✓ 分类 '{category_name}' 抓取完成!\n")

        except Exception as e:
            # 捕获抓取过程中的异常,打印错误信息但不中断程序
            print(f"✗ 抓取分类 '{category_name}' 时出错: {e}\n")

        # 如果不是最后一个分类,返回首页准备抓取下一个
        if idx < len(category_list):
            print("返回首页准备下一个分类...")
            # 直接访问首页,而不是使用后退按钮(更稳定)
            page.goto(url, timeout=60000, wait_until="domcontentloaded")
            # 等待2秒让页面完全加载
            time.sleep(2)

            # 等待导航栏加载完成,为下一次循环做准备
            page.wait_for_selector("#category", timeout=10000)

    print("\n" + "=" * 80)
    print("所有分类抓取完成!")
    # 关闭浏览器
    browser.close()
2-Learning/05-项目/03-Python与爬虫/07-爬虫课程六讲/assets/PixPin_2026-04-24_17-53-45-899207ce

2.4 它适合什么场景?

适合:

必须点击筛选项才出现数据
必须滚动到底部才加载更多
页面是 Vue / React / Angular 渲染的
职位详情在弹窗或新 tab 中打开
需要模拟移动端页面
需要处理前端路由

比如企业招聘官网常见这些情况:

点击城市
点击职位类别
点击加载更多
滚动页面加载下一批职位
详情页是前端路由

通用项目里的企业采集链路就增强了动态加载和多分页能力,包括自动滚动加载、加载更多按钮、分页函数和新开 tab 抓详情等设计。

2.5 项目中的例子

猎聘项目中,脚本会用 Playwright 打开页面,并通过浏览器真实操作筛选项。比如选择企业职位、公司性质、公司规模、经验等,然后进入抓取流程。猎聘脚本不是直接拼 URL,而是通过页面控件驱动筛选,这就是典型的动态页面采集。

同时,猎聘脚本还会启动本地 Chromium、设置代理、关闭自动化特征、拦截图片/字体/媒体资源,目的是更像真实浏览器并降低资源消耗。

2.6 优点和缺点

优点:

适应复杂页面
可以点击、滚动、翻页
可以处理 JavaScript 渲染
接近真实用户行为

缺点:

速度慢
资源消耗大
浏览器容易崩
并发成本高
更容易遇到反爬
选择器容易受页面改版影响

所以真实项目里一般不会无脑全用 Playwright。

能用接口就用接口;接口不好搞,再用 Playwright。

3. 第三重:API 直连采集

3.1 它是什么?

API 直连采集,就是不抓网页,而是直接请求网站背后的数据接口。

比如网页上看到职位列表,其实浏览器背后可能请求了这个接口:

https://example.com/api/jobs?page=1&city=shanghai

接口返回的不是 HTML,而是 JSON:

{
  "jobs": [
    {
      "title": "数据分析师",
      "company": "某科技公司",
      "salary": "15k-25k",
      "city": "上海"
    }
  ]
}

这时候我们就可以直接请求接口。

3.2 生活化理解

动态页面采集像是:

去餐厅点菜,再从菜单上抄

API 直连像是:

直接找后厨要一份电子菜单表格

它更直接、更干净。

3.3 最小代码长什么样?

import requests

url = "https://example.com/api/jobs"

params = {
    "page": 1,
    "city": "shanghai",
    "keyword": "数据分析"
}

response = requests.get(url, params=params)

data = response.json()

for job in data["jobs"]:
    title = job["title"]
    company = job["company"]
    salary = job["salary"]
    city = job["city"]

    print(title, company, salary, city)

这段代码的重点是:

不解析 HTML
不打开浏览器
直接解析 JSON

示例

import requests
import time
import json

# API 接口地址(从浏览器开发者工具 Network 面板中找到的真实接口)
api_url = "https://www.zwnsyw.top/api/dress/listByCursor"

# 设置请求体数据(完全按照浏览器实际发送的格式)
payload = {
  "category": "",
  "cursor": "1916105854908362754",
  "pageSize": 12,
  "searchText": "",
  "sortField": "id",
  "sortOrder": "ascend",
}

# 设置完整的请求头(POST 请求需要指定 Content-Type)
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",
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Content-Type": "application/json",
    "Referer": "https://www.zwnsyw.top/waterfall",
    "Origin": "https://www.zwnsyw.top",
    "Connection": "keep-alive",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin"
}

print("=" * 80)
print("开始 API 直连采集...")
print("=" * 80)

try:
    # 发送 POST 请求到 API 接口(注意:使用 json 参数自动序列化)
    print("\n正在请求 API 接口...")
    print(f"请求 URL: {api_url}")
    print(f"请求方式: POST")
    print(f"请求体: {json.dumps(payload, ensure_ascii=False, indent=2)}\n")

    # 使用 json 参数,requests 会自动将字典序列化为 JSON 字符串
    response = requests.post(api_url, json=payload, headers=headers, timeout=10)

    # 打印响应状态码
    print(f"响应状态码: {response.status_code}")

    # 检查响应状态码
    if response.status_code == 200:
        print("✓ HTTP 请求成功!\n")

        # 解析 JSON 响应数据
        try:
            data = response.json()

            # 检查接口返回的业务状态码
            if data.get("code") == 0:
                print("✓ 接口返回数据正常!\n")

                # 获取数据列表
                records = data["data"]["records"]
                cursor = data["data"].get("cursor", "")  # 下一页的游标
                is_last = data["data"].get("last", False)  # 是否最后一页

                print(f"共获取到 {len(records)} 条服饰数据")
                print(f"当前游标: {cursor if cursor else '首页'}")
                print(f"是否最后一页: {is_last}")
                print("-" * 80)

                if len(records) == 0:
                    print("\n⚠️ 未获取到数据\n")
                else:
                    # 遍历每条服饰记录
                    for idx, dress in enumerate(records, 1):
                        # 提取基本信息
                        name = dress.get("name", "未知名称")
                        introduction = dress.get("introduction", "无简介")
                        story = dress.get("story", "无故事")

                        # 提取服饰属性
                        ethnic_group = dress.get("ethnicGroup", "未知")
                        epoch = dress.get("epoch", "未知朝代")
                        location = dress.get("location", "未知地点")
                        category = dress.get("category", "未知分类")
                        tags = ", ".join(dress.get("tags", []))

                        # 提取图片链接(需要拼接完整 URL)
                        base_url = "https://www.zwnsyw.top"
                        picture_url = base_url + dress.get("pictureUrl", "") if dress.get("pictureUrl") else "无图片"

                        # 提取用户信息
                        user = dress.get("user", {})
                        user_name = user.get("userName", "未知用户")
                        user_avatar = user.get("userAvatar", "无头像")

                        # 格式化输出
                        print(f"\n[{idx}] {name}")
                        if len(introduction) > 60:
                            print(f"    简介: {introduction[:60]}...")
                        else:
                            print(f"    简介: {introduction}")
                        print(f"    民族: {ethnic_group} | 朝代: {epoch} | 地点: {location}")
                        print(f"    分类: {category} | 标签: {tags}")
                        print(f"    图片: {picture_url}")
                        print(f"    上传者: {user_name}")
                        print("-" * 80)

                    print("\n✓ 数据采集完成!")

                print("=" * 80)

            else:
                print(f"✗ 接口返回业务错误")
                print(f"错误码: {data.get('code')}")
                print(f"错误信息: {data.get('message', '未知错误')}")
                print(f"错误描述: {data.get('description', '无')}")

        except json.JSONDecodeError as e:
            print(f"✗ JSON 解析失败: {e}")
            print(f"响应内容前500字符: {response.text[:500]}")

    else:
        print(f"✗ HTTP 请求失败,状态码: {response.status_code}")
        print(f"响应内容: {response.text[:500]}")

except requests.exceptions.Timeout:
    print("✗ 请求超时,请检查网络连接")
except requests.exceptions.ConnectionError:
    print("✗ 连接失败,请检查网络或 API 地址")
except Exception as e:
    print(f"✗ 发生未知错误: {type(e).__name__}: {e}")
    import traceback
    traceback.print_exc()
2-Learning/05-项目/03-Python与爬虫/07-爬虫课程六讲/assets/image-7e118cb6

3.4 它适合什么场景?

适合:

接口容易找到
接口参数简单
接口不需要复杂签名
接口返回字段完整
接口稳定

比如很多企业招聘官网、部分校招官网、部分职位系统,其实都能找到职位接口。

通用项目里就有 auto_api 采集通道,原理是绕开前端渲染,直接请求官方职位接口,然后把接口数据映射成内部 detail_*.json 字段模型。

3.5 怎么发现接口?

用浏览器开发者工具。

步骤大概是:

打开网页
↓
按 F12
↓
进入 Network 面板
↓
刷新页面
↓
筛选 Fetch / XHR
↓
点击请求
↓
看 Response 里有没有职位 JSON

如果看到 Response 里有:

{
  "jobName": "产品经理",
  "workCity": "北京",
  "salary": "20k-30k"
}

那就说明这个接口可能可以用。

3.6 优点和缺点

优点:

最快
最干净
最省资源
字段结构清晰
不依赖页面样式
适合大规模采集

缺点:

接口可能有签名
接口可能需要 Cookie
接口参数可能复杂
接口可能有频控
接口可能随版本变化
有些详情字段不在接口里

所以 API 直连是首选,但不是永远可行。

4. 第四重:on_response 响应监听采集

4.1 它是什么?

这是非常实用的一种中间方案。

它的思路是:

我不直接破解接口
也不只从 DOM 抠文字
我让浏览器正常打开页面
然后监听浏览器收到的接口响应

也就是说:

页面自己去请求接口
我在旁边“偷听”接口返回的数据

这就是 on_response


4.2 生活化理解

API 直连像是你直接去问后厨:

把电子菜单给我

on_response 像是:

你坐在前台旁边
等服务员从后厨拿菜单出来
你顺手看一眼菜单内容

你不用自己知道后厨怎么下单,也不用完全破解接口参数。

4.3 最小代码长什么样?

from playwright.sync_api import sync_playwright

def handle_response(response):
    url = response.url

    if "/api/jobs" in url:
        try:
            data = response.json()
            print("抓到接口数据:", data)
        except:
            pass

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    page.on("response", handle_response)

    page.goto("https://example.com/jobs")

    page.wait_for_timeout(5000)

    browser.close()

关键代码是:

page.on("response", handle_response)

意思是:

只要页面收到任何网络响应
就交给 handle_response 处理

然后我们在里面判断:

if "/api/jobs" in response.url:

意思是:

只处理职位接口
其他图片、CSS、JS 都忽略

4.4 它适合什么场景?

适合:

页面背后有接口
但接口参数复杂
接口有签名
接口需要浏览器环境
接口需要先点击筛选项才能触发
直接 requests 请求很麻烦
DOM 解析又不稳定

这类场景非常常见。

4.5 项目中的例子

猎聘就是很典型的 on_response 方案。

猎聘脚本通过页面真实操作触发搜索接口,然后监听猎聘搜索接口返回的数据,把 jobCardList 保存起来。之后再从页面 DOM 中拿职位跳转链接,通过职位 ID 去接口 JSON 里反查对应职位基础字段。这样比纯 DOM 抓列表更稳定。

猎聘公司版也监听公司职位条件接口和公司职位列表接口,用接口返回的条件集合生成抓取组合,而不是完全依赖页面上渲染出来的筛选项。

通用项目里也有 auto_on_response 通道,原理是在页面对象上注册 page.on('response', handler),匹配目标接口 URL,然后提取响应 JSON 并落盘为内部结构。

4.6 优点和缺点

优点:

不用完全破解接口参数
比纯 DOM 抓取稳定
可以拿到结构化 JSON
适合复杂动态网站
能配合点击、筛选、滚动使用

缺点:

仍然要启动浏览器
速度比 API 直连慢
需要判断哪些 response 是目标接口
接口响应时机不好控制
如果页面不触发接口,就监听不到

5. 四种采集通道怎么选?

可以用这个判断顺序:

第一步:先看 HTML 源码里有没有数据
有 → 静态 HTML 采集

没有 → 看 Network 里有没有接口 JSON
有,而且接口好请求 → API 直连

有,但接口参数复杂 / 签名复杂 → on_response 监听

没有明显接口,必须点击/滚动/渲染后才有 → Playwright 动态页面采集

更直观一点:

数据直接在 HTML 里
→ requests + BeautifulSoup

数据在接口 JSON 里,接口好调
→ requests + json

数据在接口 JSON 里,但接口不好直接调
→ Playwright + page.on("response")

数据要靠点击、滚动、渲染才出现
→ Playwright 操作页面

6. 四种方式对比

采集通道 核心工具 数据来源 速度 稳定性 难度 适合场景
静态 HTML requests + BeautifulSoup HTML 源码 简单公告页、服务端渲染页面
动态页面 Playwright 渲染后的 DOM 点击、滚动、加载更多、复杂前端
API 直连 requests JSON 接口 很快 接口清晰、参数简单
on_response Playwright 浏览器收到的接口响应 中高 接口复杂但页面能触发

7. 用招聘网站举例

假设我们要抓一个招聘网站。

场景一:学校就业网公告

页面源码里直接有:

<a href="/detail/123">某公司2026校园招聘</a>

适合:

静态 HTML 采集

流程:

请求公告列表
↓
解析公告标题和链接
↓
请求详情页
↓
提取正文

场景二:企业官网职位页

页面打开后职位才慢慢出现,需要滚动或点加载更多。

适合:

动态页面采集

流程:

打开页面
↓
滚动到底部
↓
点击加载更多
↓
等职位卡片出现
↓
提取职位链接

场景三:企业官网职位接口

Network 里发现:

/api/recruit/jobs?page=1

返回 JSON。

适合:

API 直连采集

流程:

请求接口
↓
解析 JSON
↓
保存 detail_*.json

场景四:猎聘这类复杂平台

页面需要真实操作筛选项,接口又有复杂参数。

适合:

动态页面 + on_response 监听

流程:

打开猎聘搜索页
↓
点击筛选条件
↓
监听职位搜索接口
↓
拿到职位列表 JSON
↓
打开职位详情页补抓长文本
↓
保存并入库

这正是猎聘项目的核心路线:页面操作、接口监听、详情页补抓、落本地 JSON,再调用内部接口入库。

8. 现在要建立的关键认知

不要把爬虫理解成“我会 requests”或者“我会 Playwright”。

真正的爬虫思维是:

先判断数据在哪里
再选择采集通道
再决定解析方式
最后考虑稳定性和工程化

也就是:

数据在 HTML?
用 HTML 解析。

数据在 JSON?
用接口解析。

接口不好直接请求?
用 response 监听。

页面必须交互?
用浏览器自动化。

数据在图片?
后面再加 OCR。

9. 这四种通道之间不是互斥的

真实项目里经常组合使用。

比如猎聘:

Playwright 打开页面
+ 点击筛选条件
+ on_response 监听列表接口
+ DOM 抓详情页字段
+ 本地 JSON 落盘
+ 内部接口入库

这不是单一通道,而是组合拳。

再比如通用采集系统:

有些站点走 DOM
有些站点走 API
有些站点走 on_response
后面统一进入 HTML 清洗、模型抽取、质控上传

它的设计思想就是“不押注单一抓取方式”,而是根据站点差异使用多通道采集冗余。

10. 小结

                    数据在哪里?
                         ↓
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
      HTML             JSON             渲染后页面
        ↓                ↓                ↓
静态 HTML 采集      API 直连采集      Playwright 动态采集
                         ↓
              接口复杂但页面能触发?
                         ↓
                 on_response 监听

爬虫不是固定用某一种技术,而是根据数据出现的位置选择采集通道。


组内导航:⬅️ 爬虫是什么 | 🏠 00-爬虫课程六讲 | ➡️ 爬虫的基础积木——每个动作能得到什么效果