首页
/ Scrapy 爬虫调试完全指南:parse 命令、Scrapy Shell、实时流量抓包与 TLS 解密

Scrapy 爬虫调试完全指南:parse 命令、Scrapy Shell、实时流量抓包与 TLS 解密

2026-09-05 21:06:55作者:戚魁泉Nursing

本文围绕 Scrapy 官方的爬虫调试文档(docs/topics/debug.rst),系统讲解排查 Spider 行为问题的四种层次手段:用 parse 命令做方法级快速验证、用 Scrapy Shell 与 inspect_response 深入回调内部、用日志固化现场,以及在必要时借助 mitmproxy/Wireshark 观察与解密真实的 TLS 网络流量。读完本文,你可以针对“某个回调没拿到数据”“某层深度请求不符合预期”“服务器返回内容需要肉眼确认”“需要确认真正发出的请求头与 TLS 握手细节”这四类典型问题,分别拿到一套可直接复制执行的排查方案。

从一个典型的多层回调 Spider 说起

官方调试文档以一个三层结构的 Spider 作为贯穿全文的调试对象:入口页(start_urls)解析出条目列表,列表页通过 cb_kwargs 传递半成品 Item,详情页再补全字段后返回 Item。

import scrapy
from myproject.items import MyItem


class MySpider(scrapy.Spider):
    name = "myspider"
    start_urls = (
        "http://example.com/page1",
        "http://example.com/page2",
    )

    def parse(self, response):
        # <processing code not shown>
        # collect `item_urls`
        for item_url in item_urls:
            yield scrapy.Request(item_url, self.parse_item)

    def parse_item(self, response):
        # <processing code not shown>
        item = MyItem()
        # populate `item` fields
        # and extract item_details_url
        yield scrapy.Request(
            item_details_url, self.parse_details, cb_kwargs={"item": item}
        )

    def parse_details(self, response, item):
        # populate more `item` fields
        return item

这个结构的价值在于它覆盖了爬虫开发中最常见的调试场景:

  • parse 是列表入口,parse_item 是中间层,parse_details 是叶子层;
  • parse_details 依赖上一层通过 cb_kwargs 传入的 item,如果传递链路断裂,回调里就会“收不到 item”——这正是后文 Scrapy Shell 与日志方案要解决的典型问题。

用 parse 命令做方法级快速验证

最基础、最轻量的调试手段是 parse 命令。它的作用是以指定 URL 为起点真实地运行爬虫引擎,但把每一层的回调输出(抓到的 Items 和生成的 Requests)按深度分级打印出来,让你可以在“方法级别”检查 Spider 各部分的行为。它的优点灵活、上手简单,缺点是不能调试方法内部代码(方法内部要用后文的 Shell 或 IDE 断点方案)。

查看某一层深度的抓取结果

要查看某个具体 URL 对应的 parse_item 层抓到什么 Item,执行:

$ scrapy parse --spider=myspider -c parse_item -d 2 <item_url>
[ ... scrapy log lines crawling example.com spider ... ]

>>> STATUS DEPTH LEVEL 2 <<<
# Scraped Items  ------------------------------------------------------------
[{'url': <item_url>}]

# Requests  -----------------------------------------------------------------
[]

其中 -c parse_item 指定“第一层响应用哪个回调来解析”(对应 Spider 的方法名),-d 2 指定最大解析深度。注意第一层响应实际走 parse_item,其后由该回调派生的请求继续解析,直到达到 -d 给定的深度上限。

用 -v 逐级查看每一层状态

加上 --verbose-v)选项后,会按深度逐层打印状态,方便逐层核对:

$ scrapy parse --spider=myspider -c parse_item -d 2 -v <item_url>
[ ... scrapy log lines crawling example.com spider ... ]

>>> DEPTH LEVEL: 1 <<<
# Scraped Items  ------------------------------------------------------------
[]

# Requests  -----------------------------------------------------------------
[<GET item_details_url>]


>>> DEPTH LEVEL: 2 <<<
# Scraped Items  ------------------------------------------------------------
[{'url': <item_url>}]

# Requests  -----------------------------------------------------------------
[]

parse 命令源码 可以看到,非 verbose 模式下打印的是 max_level(所有 Items 与 Requests 中出现的最大深度)的汇总结果;verbose 模式则遍历 1..max_level 逐层输出,这正是上面两组输出的来源。

检查单个 start_url 抓到的全部条目

不想指定回调时,直接从某个 start_url 开始按 Spider 默认链路(默认回调为 parse)跑三层深度:

$ scrapy parse --spider=myspider -d 3 'http://example.com/page1'

parse 命令的完整选项

官方文档示例只用了 --spider-c-d-v 四个选项。对照 scrapy/commands/parse.pyadd_options 的定义,parse 命令实际还提供了一整套更细的开关,可以按需组合:

选项 作用
--spider 显式指定使用哪个 Spider,不做 URL 匹配查找
--pipelines 让 Items 走一遍 Item Pipeline 再打印
--nolinks 不打印派生请求(“要跟随的链接”)
--noitems 不打印抓取到的 Items
--nocolour 禁用 pygments 彩色输出
-r / --rules CrawlSpider 场景下,用 LinkExtractor 规则自动推断回调
-c / --callback 指定第一层响应的解析回调,覆盖默认推断逻辑
-m / --meta 向请求注入额外 meta,值必须是合法 JSON 字符串,如 --meta='{"foo": "bar"}'
--cbkwargs 向请求注入额外 cb_kwargs,值必须是合法 JSON 字符串,如 --cbkwargs='{"foo": "bar"}'
-d / --depth 最大解析深度,默认 1
-v / --verbose 逐层打印每个深度级别的结果

两个与调试直接相关的机制值得注意:

  1. -m / --cbkwargsprepare_request 中会直接 update 到请求的 meta / cb_kwargs 上。这意味着你可以不改一行代码,就模拟 cb_kwargs 传了某个值的情形,验证 parse_details(self, response, item) 是否按预期消费了该参数——对“item 丢失”类问题非常实用。
  2. 后续深度的回调继承自第一层请求的原始回调(见 scraped_datareq.meta["_callback"] = req.callback 后再统一替换成内部追踪回调)。因此 -c parse_item 指定后,第二层派生的请求仍会用 parse_item 去解析,而不是各自 Request 上写的回调。理解这一点能避免“为什么 -c 没生效/生效过头”的困惑。

parse 命令的实现细节还包括:它通过 set_spiderclsspider_loader.load(opts.spider) 加载 Spider 类,并把 Spider 的 start 方法临时替换为“只发出对指定 URL 的单个请求”,从而把一次完整的 crawl 收敛为“以该 URL 为根的有界子爬取”。

进入回调内部:Scrapy Shell 与 inspect_response

parse 命令能告诉你“parse_details 这一层产出了什么”,但回答不了“为什么有时 parse_details 收到的 item 是空的”。对回调内部逻辑的调试,Scrapy 的“面包黄油”是 scrapy shell(参见 Scrapy Shell 文档),核心手段是 inspect_response

from scrapy.shell import inspect_response


def parse_details(self, response, item=None):
    if item:
        # populate more `item` fields
        return item
    else:
        inspect_response(response, self)

当异常分支被触发时,inspect_response 会在当前爬取中原地打开一个交互式 Shell,其中已经预置了 responserequestspider 等对象,你可以直接在回调现场执行 response.xpath(...)、检查 response.headers 等,无需退出爬取、无需重现网络请求。

scrapy/shell.pyinspect_response 实现看,它做了三件事:保存当前的 SIGINT 处理函数(防止 Shell 期间误按 Ctrl-C 杀掉引擎)、构造 Shell(spider.crawler, loop=loop) 并以 response=response, spider=spider 启动、Shell 关闭后恢复 SIGINT 处理。同时 Shell.populate_vars 展示了 Shell 环境中可用的完整对象集:scrapy 模块、crawleritemsettingsspiderrequestresponse,以及快捷函数 fetch(在 scrapy shell 命令中可用,向引擎发起真实请求)、view(即 open_in_browser)和 shelp。需要注意 inspect_response 走的是“与反应器同线程”的模式,此时 fetch 不可用(fetch 依赖反应器运行在独立线程,见 scrapy/shell.py 的架构注释)——在回调现场你能操作手头的 response,但不能发起新的抓取。

在浏览器中查看响应

有时最直接的验证就是“肉眼看看这个响应在浏览器里长什么样”,尤其是判断服务端是否对爬虫返回了降级页面(空壳 HTML、验证码页等)。Scrapy 提供了 open_in_browser

from scrapy.utils.response import open_in_browser


def parse_details(self, response):
    if "item name" not in response.text:
        open_in_browser(response)

它的工作方式(见 scrapy/utils/response.py 的实现):把响应体写入一个临时文件后用系统默认浏览器打开;对 HtmlResponse 会在 <base href="..."> 处注入 base 标签(插入在 doctype 之后、以标准方式解析页面时生效),使页面上的图片、样式等外部资源链接相对页面 URL 正常解析。此外 Scrapy Shell 的交互式环境里也内置了同名快捷函数 view(response)(见 Shell.populate_vars),在 Shell 中不必 import 即可调用。

用日志固化现场

Shell 和浏览器是“即时”手段,而日志的优势在于每次运行都会留下记录,事后还能反复回看,适合生产环境长期埋点:

def parse_details(self, response, item=None):
    if item:
        # populate more `item` fields
        return item
    else:
        self.logger.warning("No item received for %s", response.url)

self.logger 是 Spider 的实例级 logger,日志会带上 Spider 名作为上下文;注意它采用惰性格式化(%s 占位符),比字符串拼接开销更低。关于 Scrapy 日志体系(级别、格式、定制 LOG_FORMATTER 等)的完整说明,参见 Logging 文档

观察实时网络流量

有些问题必须看到“线上真实字节”才能定位:请求头的最终取值、头部顺序与格式、TLS 握手细节。官方文档特别指出:Scrapy 自身无法记录这些,因为底层 HTTP 库会自行计算最终请求头并规范化响应头,Scrapy 层面的 request.headers 与真正上线的报文可能存在差异。文档给出了两条互补的路径:

  1. 被动抓包(Wireshark 一类工具):用抓包工具直接捕获 Spider 发出的流量。由于请求通常走 TLS,必须解密才能看到明文——这需要 TLS 会话密钥日志,获取方式见下文“解密 TLS 流量”。这种方式完全被动,不会干扰 Spider。
  2. 中间人代理(mitmproxy):在 Spider 与目标服务器之间架一个 mitmproxy,配置简单,除了看流量还可以修改流量;但它不是被动的——原本 Spider 到服务器的一条直连变成了“Spider → mitmproxy → 服务器”两条连接,底层连接行为与正常爬取不同,可能改变服务器端行为,且同一爬取中难以与常规代理叠加使用。

用 mitmdump 拦截并记录流量

最简单的组合方式:

第一步,启动一个 mitmdump 实例(默认监听 8080 端口),--flow-detail 2 表示打印请求/响应头部但不打印 body:

mitmdump --flow-detail 2

第二步,把 http://127.0.0.1:8080 作为代理交给 Spider。最简单是设置环境变量,由 HttpProxyMiddleware 通过 getproxies() 读取:

https_proxy=http://127.0.0.1:8080 scrapy crawl myspider

如果只想让部分请求过 mitmproxy,则按请求设置 proxy meta 键(如 Request(url, meta={"proxy": "http://127.0.0.1:8080"}))。从 HttpProxyMiddleware.process_request 可以确认其优先级:request.meta["proxy"] 存在时优先生效并解析为最终代理地址与凭据,否则才回落到环境变量/配置中的代理。

解密 TLS 流量

无论走 Wireshark 解密还是分析中间人链路上的明文,前提往往都是拿到 TLS 会话密钥。Scrapy 会把其 HTTPS 连接产生的会话密钥写入 SSLKEYLOGFILE 环境变量指向的文件,采用 NSS key log 格式(Wireshark 等流量分析工具原生理解该格式):

SSLKEYLOGFILE=/tmp/sslkeylog scrapy crawl myspider

从源码可以确认该行为的落点:_get_keylog_filename 读取 SSLKEYLOGFILE 环境变量,尝试以追加模式打开该文件,打开失败(如路径不可写)时会打一条 Cannot write TLS session keys to ... 的 warning 并放弃记录。对应的行为验证见 tests/test_core_downloader.pytests/utils/bases/download_handlers_http.py 中围绕 SSLKEYLOGFILE 的用例(覆盖了“keylog 文件写入成功”和“路径不存在/不可写时告警”两种情形)。

安全警告:任何能读取该密钥日志文件的人,都可以解密其中记录的全部连接流量,包括流量中携带的任何凭据。务必将其视为敏感文件管理(限制权限、用后删除)。

用 Visual Studio Code 断点调试 Spider

前几种手段都是“无断点”的运行时观察,而最完整的调试体验当然还是 IDE 断点。官方文档给出了一段可直接使用的 launch.json 配置(docs/topics/debug.rst):

{
    "version": "0.1.0",
    "configurations": [
        {
            "name": "Python: Launch Scrapy Spider",
            "type": "python",
            "request": "launch",
            "module": "scrapy",
            "args": [
                "runspider",
                "${file}"
            ],
            "console": "integratedTerminal"
        }
    ]
}

要点说明:

  • "module": "scrapy""args": ["runspider", "${file}"] 组合,等价于以调试器启动 python -m scrapy runspider 当前文件runspider 命令适合调试单个 Spider 文件(不要求位于项目包内);如果 Spider 在项目内且需要项目级设置,也可将 args 换成 ["crawl", "<spider-name>"]
  • "console": "integratedTerminal" 让爬虫输出直接显示在集成终端。
  • 文档特别提醒:务必在调试设置中启用 “User Uncaught Exceptions”(用户未捕获异常中断),否则 Spider 回调里抛出的异常会被 Scrapy 引擎正常捕获并记录为 error 日志,调试器不会像普通 Python 程序那样在异常处停下来。

小结:按问题类型选择调试手段

问题类型 首选手段 关键入口
某一层回调的 Items/Requests 输出不对 parse 命令(-c 指定回调、-d 控制深度、-v 逐层看) scrapy/commands/parse.py
回调内部逻辑异常(如 cb_kwargs 丢失) inspect_response 现场 Shell / VS Code 断点 scrapy/shell.py
响应内容疑似被降级/反爬 open_in_browser 肉眼比对 scrapy/utils/response.py
需要事后追溯的运行时线索 self.logger 埋点 docs/topics/logging.rst
需要确认真实上线的请求头/TLS 握手 mitmproxy(可修改)或 Wireshark + SSLKEYLOGFILE(被动) scrapy/downloadermiddlewares/httpproxy.pyscrapy/utils/ssl.py

以上所有命令与代码路径均基于当前仓库实现,parse 选项列表、inspect_response 机制与 SSLKEYLOGFILE 行为均可在对应源码文件中直接核对。

登录后查看全文
热门项目推荐
相关项目推荐