Scrapy 官方 FAQ 精解:从代理配置、内存优化到流式解析的实战答案与源码依据
本篇技术指南完整覆盖 Scrapy 官方文档 FAQ 中的所有高频问题:Scrapy 与 BeautifulSoup/lxml 的定位差异、HTTP/SOCKS 代理与 Basic Auth 的接入方式、OffsiteMiddleware 的域名过滤机制与内存优化、爬取顺序、Cookie 调试、流式解析大型 XML/CSV 数据源、runspider 免项目运行、数据导出命令,以及状态码 999 限速、StopDownload、IPv6 支持等进阶话题。读完之后,你可以带着源码级依据(每个结论都标注了对应的实现文件路径)回答生产环境中遇到的绝大多数 Scrapy 疑问。
Scrapy 与 BeautifulSoup、lxml 是什么关系?
FAQ 的第一个经典问题就是:Scrapy 相比 BeautifulSoup 或 lxml 有什么优势?官方的答案是——二者不在同一个层次上,没有可比性:
BeautifulSoup和lxml是 HTML/XML 解析库;- Scrapy 是编写 Web 爬虫、执行网站爬取并提取数据的应用框架。
因此,把 BeautifulSoup 与 Scrapy 相比,就像把 jinja2 与 Django 相比:一个是底层组件,一个是完整框架。Scrapy 内置了自己的数据提取机制——selectors(选择器);但如果你更习惯 BeautifulSoup 或 lxml,完全可以在 Scrapy 回调里直接导入使用——它们只是普通的 Python 解析库。
官方给出的示例 spider(见 docs/faq.rst):
from bs4 import BeautifulSoup
import scrapy
class ExampleSpider(scrapy.Spider):
name = "example"
allowed_domains = ["example.com"]
start_urls = ("http://www.example.com/",)
def parse(self, response):
# use lxml to get decent HTML parsing speed
soup = BeautifulSoup(response.text, "lxml")
yield {"url": response.url, "title": soup.h1.string}
注意示例中指定 "lxml" 作为解析后端,目的是获得更接近 lxml 的解析速度。需要提醒的是,BeautifulSoup 支持多种 HTML/XML 解析器,具体可用哪些取决于你在运行环境中安装了哪些解析器包,选择哪种解析器请以 BeautifulSoup 官方文档为准。
一个顺带的说明:Scrapy 借鉴了 Django 的设计
FAQ 中“Did Scrapy steal X from Django?”一节的立场是:如果某个问题已经有人做得很好,就没必要重新发明轮子。Django 是 Scrapy 的设计灵感来源之一,这种“复用已被验证的开源设计”的思路同样适用于文档、流程与规范。
代理配置实战:HTTP 代理、Basic Auth 与 SOCKS
HTTP 代理
FAQ 确认 Scrapy 支持 HTTP 代理,能力由 HTTP Proxy 下载器中间件提供,实现位于 HttpProxyMiddleware。从源码可以看出它的完整行为:
- 中间件在初始化时通过
urllib.request.getproxies()读取操作系统级环境变量代理(如http_proxy),并跳过无法解析的值(源码注释提到像/var/run/docker.sock这类值会被静默忽略); - 支持按请求指定代理:只要在
request.meta中放入"proxy"键即可覆盖系统代理; - 代理 URL 中可以内嵌
user:password,中间件会将其编码为 Base64 的 Proxy-Authorization 头,即天然支持 HTTP Basic Authentication(_basic_auth_header方法)。FAQ 中“Can I use Basic HTTP Authentication in my spiders?”一节的正解就是HttpAuthMiddleware与这里对代理自身鉴权的处理; - 可通过设置
HTTPPROXY_ENABLED整体禁用该中间件,HTTPPROXY_AUTH_ENCODING可调整代理凭据的编码方式(默认latin-1)。
SOCKS 代理
FAQ 明确:SOCKS 代理支持仅在 HttpxDownloadHandler 下可用。从源码看,当代理以 socks 开头而运行环境缺少 SOCKS 支持时,处理器会直接报错,提示需要安装 httpx2[socks] 扩展。因此实际使用 SOCKS 的前置条件是:切换到 httpx 下载处理器 + 安装 httpx2[socks] 依赖。
域名过滤:"Filtered offsite request" 到底意味着什么?
消息机制
日志里出现 "Filtered offsite request" 消息不一定代表出了问题——这些消息以 DEBUG 级别记录,来自默认启用的 OffsiteMiddleware。它的作用是过滤掉 spider 覆盖范围之外的域名请求。
源码揭示了几个 FAQ 没有明说的细节(见 scrapy/downloadermiddlewares/offsite.py):
- 匹配规则:若 spider 定义了
allowed_domains,中间件会把域名列表拼接成正则^(.*\.)?(domain1|domain2)$。也就是说www.example.org会放行其子域名bob.www.example.org,但不会放行www2.example.org或裸域example.org;若 spider 未定义allowed_domains,则所有请求都放行; - 日志去重:每个被过滤的域名只记录第一条,避免日志被刷屏;被过滤请求会计入统计项
offsite/filtered与offsite/domains; - 豁免机制:设置了
request.dont_filter = True或request.meta["allow_offsite"] = True的请求不受过滤; - 子类可覆写
should_follow方法实现自定义策略(如只允许根域、不允许子域)。
allowed_domains 过长导致内存告警?
FAQ 专门解答:当 allowed_domains 列表非常长(例如 5 万+ 个域名)时,默认中间件把它们拼接成复杂正则再编译,会占用可观内存。官方建议替换为自定义下载器中间件:
- 如果域名形态相似,直接自己维护一个正则表达式;
- 如果允许额外依赖,可以考虑用
pyre2这类第三方正则引擎替代标准库re来编译 URL 过滤正则。
替换时必须显式禁用默认中间件,配置写法:
DOWNLOADER_MIDDLEWARES = {
"scrapy.downloadermiddlewares.offsite.OffsiteMiddleware": None,
"myproject.middlewares.CustomOffsiteMiddleware": 50,
}
爬取顺序、登录模拟与项目内数据拼接
- 爬取顺序:Scrapy 默认按深度优先(depth-first) 方式爬取,如需其他顺序(如广度优先),需要借助扩展或自定义调度器/队列实现。
- 模拟用户登录:官方指引见 request-response 文档中的登录小节,核心思路是通过
FormRequest提交登录表单并利用 Cookie 自动维护保持会话。 - 跨页面属性拼接(item 拆到多页):FAQ 指向 callback-data 一节,通用做法是利用请求回调参数(
cb_kwargs)或Request.meta携带已采集的部分数据,在后续页面回调中合并。
在 item pipeline 里把一个 item 拆成多个?
FAQ 明确指出:item pipeline 不能对单个输入 item 产出多个 item,正确做法是编写一个 spider middleware,利用其 process_spider_output 方法(自定义中间件的写法见 spider-middleware 文档)。官方示例:
from copy import deepcopy
from itemadapter import ItemAdapter
from scrapy import Request
class MultiplyItemsMiddleware:
def process_spider_output(self, response, result):
for item_or_request in result:
if isinstance(item_or_request, Request):
yield item_or_request
continue
adapter = ItemAdapter(item_or_request)
for _ in range(adapter["multiply_by"]):
yield deepcopy(item_or_request)
这个例子按 item 中的 multiply_by 字段把一条记录复制成多条,展示了 process_spider_output 对 Request 与 item 混合流的统一处理模式——请求原样放行,item 则复制后逐个产出。
控制下载行为:状态码 999、提前取消下载与空白请求
状态码 999:站点在对你限速
部分站点(如早期的某些新闻站)会返回自定义状态码 999 来限流。FAQ 建议:对受影响域名把下载延迟提高到 2 秒或更高。两种方式:
# 按域名差异化延迟
DOWNLOAD_SLOTS = {
"example.com": {"delay": 2},
}
或者用全局设置 DOWNLOAD_DELAY 统一设置项目延迟。
在收到完整响应前取消下载
某些场景下,光看响应头或响应体前几个字节就能判断是否需要完整内容,此时可以注册 bytes_received 或 headers_received 信号处理器,在其中抛出 StopDownload 异常来中止下载,节省带宽和内存。详见 request-response 文档中的 stop-response-download 小节。
构造一个"空白请求"
from scrapy import Request
blank_request = Request("data:,")
这里使用 data: URI 方案、内容为空,等价于一个不带具体内容的内联数据请求。该方案不发起真实网络连接,在需要占位请求或测试数据流时很实用。
大型 XML/CSV 数据源的流式解析
FAQ 提醒:用 XPath 选择器解析大型 feed 有问题,因为选择器需要把整个 feed 构建进内存 DOM,既慢又吃内存。正确做法是使用 Scrapy 提供的流式迭代器:
scrapy.utils.iterators.xmliter_lxmlscrapy.utils.iterators.csviter
事实上,XMLFeedSpider 和 CSVFeedSpider 底层用的就是这两个函数。从 scrapy/utils/iterators.py 的源码可以看到实现要点:
xmliter_lxml基于lxml.etree.iterparse,以end事件驱动逐节点产出,并对每个处理完的节点调用node.clear()释放内存,实现真正的流式处理;csviter基于标准库csv.reader逐行迭代,未提供表头时默认取第一行作为键;行长度与表头不一致的数据行会被跳过并记录告警。
函数签名(可直接照抄使用):
xmliter_lxml(obj, nodename, namespace=None, prefix="x") # 产出 Selector 迭代器
csviter(obj, delimiter=None, headers=None, encoding=None, quotechar=None) # 产出 dict 迭代器
obj 都可以是 Response 对象、Unicode 字符串或 utf-8 编码的字节串。
Cookie:自动管理 + 一行设置打开调试
Scrapy 像普通浏览器一样自动接收并跟踪服务器下发的 Cookie,并在后续请求中带回。想观察 Cookie 的收发过程,只需打开设置:
COOKIES_DEBUG = True
对应实现见 CookiesMiddleware:from_crawler 从 COOKIES_DEBUG 读取开关,之后 _debug_cookie 与 _debug_set_cookie 两个方法分别以 DEBUG 级别记录请求携带的 Cookie 与响应下发的 Set-Cookie。更多机制详见 cookies 文档。
命令行实战:runspider、数据导出与调试
不建项目直接跑 spider
FAQ 确认可以用 runspider 命令免项目运行。若 spider 写在 my_spider.py 中:
scrapy runspider my_spider.py
runspider 命令 的实现把 spider 文件所在目录临时插入 sys.path 后导入模块,再用 DummySpiderLoader 启动,无需 scrapy.cfg。
FAQ 同时提醒一个已知陷阱:出现 error: No spider found in file: <filename> 时,通常是你的 spider 模块名与 Python 标准库或已安装包重名(如 csv.py、os.py),导致导入的不是你的文件。
最简单的数据导出方式
crawl 命令的 -O 参数可以直接把抓取结果导出为指定格式,按文件扩展名自动选择导出器:
scrapy crawl myspider -O items.json # JSON
scrapy crawl myspider -O items.csv # CSV
scrapy crawl myspider -O items.xml # XML
完整的 Feed 导出机制(存储方案、批处理、后处理等)见 feed-exports 文档。关于"能否用 JSON 导出超大结果",FAQ 的答案是视数据规模而定,并指向 exporters 文档中的大体积 JSON 警告。
调试:pdb 还是 Scrapy shell?
可以在 spider 里调用 pdb.set_trace(),但官方更推荐 Scrapy shell:它能快速检查(甚至修改)spider 正在处理的响应,通常比裸 pdb 更实用。用法与"检查特定响应"的完整流程见 shell 文档。
让 spider 自己停下来
在回调中抛出 CloseSpider 异常即可优雅关闭 spider,例如按条件主动结束爬取。
其他高频问题的准确答案
语言不是母语却总是拿到英文页面?
服务器按请求的 Accept-Language 头返回对应语言页面。覆盖默认请求头即可:
DEFAULT_REQUEST_HEADERS = {
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.5",
}
spider 参数还是 settings?
两者都能配置 spider,没有硬性规定,但分工上:settings 适合基本不变的参数(如登录凭据),spider 参数适合频繁变化、甚至每次运行必变的参数(如本次要抓的栏目 URL)。官方示例:需要登录的站点,凭据放 settings,目标栏目 URL 放 spider 参数。
XML 的 XPath 取不到东西?
很可能是命名空间的问题,需要按 selectors 文档中的 removing-namespaces 小节 先注册/剥离命名空间再匹配。
IPv6 支持
Scrapy 支持 IPv6,但当使用 HTTP11DownloadHandler 或 H2DownloadHandler 时,需要把 TWISTED_DNS_RESOLVER 设为 scrapy.resolver.CachingHostnameResolver(见 scrapy/resolver.py)。注意代价:这样做后 DNS_TIMEOUT 设置会被忽略,即失去对 DNS 请求单独设置超时的能力。
macOS 上大规模爬取报 filedescriptor out of range in select()?
该问题历史上出现在 macOS 宽范围爬取、且 Twisted 默认 reactor 为 SelectReactor 的场景。如果你用 TWISTED_REACTOR 设置显式选择了该 reactor,换用其他 reactor 即可规避。
如何避免被目标站点封禁?
FAQ 指向 practices 文档中的 bans 小节,核心手段包括合理延迟、限制并发、遵守站点规则等。
生产环境部署与内存
- 生产部署:见 deploy 文档;
- 内存泄漏:FAQ 建议参考 leaks 文档,其中同时解释了"没有泄漏却看似泄漏"的 Python 解释器内置内存管理机制(对象池不归还 OS 的现象)。减少内存占用的思路与泄漏排查是同一套方法。
写在最后
FAQ 的价值不在于答案本身——多数答案只是一两句话加一个文档跳转——而在于它集中定义了 Scrapy 的能力边界与官方推荐路径:解析交给选择器或 lxml/BeautifulSoup、代理与鉴权交给下载器中间件、超大数据源交给流式迭代器、数据膨胀交给 spider middleware 而非 pipeline。本文给出的所有行为描述均可在 scrapy/downloadermiddlewares/offsite.py、scrapy/downloadermiddlewares/httpproxy.py、scrapy/core/downloader/handlers/_httpx.py、scrapy/utils/iterators.py、scrapy/commands/runspider.py 等文件中逐行验证,配合 docs/faq.rst 原文即可作为生产排障时的可靠参照。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00