Scrapling 与 Scrapy 集成实战:用 scrapling_response 装饰器在现有 Scrapy 项目中直接获得 Scrapling 解析能力
如果你维护着一个既有的 Scrapy 项目,又不想推翻重写,Scrapling 的 Scrapy 集成(docs/integrations/scrapy.md)提供了一条平滑路径:通过 scrapling_response 装饰器,Scrapy 回调函数里的 response 参数会被就地转换为 Scrapling 的 Response 对象,爬虫调度、请求队列仍由 Scrapy 全权负责,而解析环节则无缝切换到 Scrapling 的完整选择器 API。读完本文,你将掌握三种接入方式(装饰器、参数化装饰器、直接调用 convert_response)、字段映射与 Cookie 解析等底层实现细节,以及四条必须知道的使用注意事项。
安装要求:默认安装即可,无需额外 extras
该集成基于 Scrapling 的默认安装(pip install scrapling)即可工作,不需要安装 fetchers、shell 等任何可选依赖组,唯一要求是环境中已安装 Scrapy——对于既有 Scrapy 项目来说这自然满足。这一结论可以从 依赖声明 得到印证:核心依赖只有 lxml、cssselect、orjson、tld、w3lib 和 typing_extensions,Scrapy 并不在其中。
集成模块的导入方式也体现了"不强制依赖"的设计。scrapling/integrations/init.py 明确说明每个第三方框架集成都是独立模块、显式导入:
from scrapling.integrations.scrapy import scrapling_response
而 scrapling/integrations/scrapy.py 在模块顶层用 try/except 捕获 Scrapy 的导入:
try:
from scrapy.http import Response as ScrapyResponse
except (ImportError, ModuleNotFoundError) as e:
raise ModuleNotFoundError(
"This integration requires Scrapy installed, please install it first with `pip install scrapy`"
) from e
也就是说,只有真正 import 这个集成模块时才要求 Scrapy 存在,未安装时会抛出带明确提示的 ModuleNotFoundError,Scrapy 永远不会成为 Scrapling 的硬性依赖。适用前提方面,pyproject.toml 声明 requires-python = ">=3.10",因此该集成运行在 Python 3.10 及以上环境。
核心用法:用 scrapling_response 装饰回调
最典型的接入方式是把 @scrapling_response 装饰器加在任意 spider 回调上,回调收到的 response 就变成了 Scrapling Response。官方文档给出的完整示例是一个引用语(quotes)爬虫:
import scrapy
from scrapling.integrations.scrapy import scrapling_response
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = ["https://quotes.toscrape.com"]
@scrapling_response
def parse(self, response): # `response` is now a Scrapling Response
first_quote = response.find_by_text("The world as we have created it", partial=True)
for quote in [first_quote, *first_quote.find_similar()]:
card = quote.parent
yield {
"text": quote.get_all_text(strip=True),
"author": card.find("small", class_="author").text,
"tags": [tag.text for tag in card.find_all("a", class_="tag")],
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield scrapy.Request(response.urljoin(next_page), callback=self.parse)
示例中用到的几个 Scrapling 解析 API 都定义在 Selector 类 中,值得逐个对照源码确认其行为:
find_by_text(text, partial=True):按文本内容查找元素,partial=True表示包含式匹配而非精确匹配;实现 还支持first_match(默认True,返回单个元素)、case_sensitive和clean_match(忽略空白差异)参数。find_similar():基于"相同树深度 + 相同标签名 + 相同父/祖父标签 + 属性相似度阈值(默认 0.2)"寻找相似元素,实现 默认忽略href、src这类易变的 URL 属性。这也是示例中能从一个 quote 卡片批量定位其余卡片的关键。get_all_text(strip=True):收集元素下所有可见文本并用换行符拼接(默认忽略script/style标签),实现 见Separator="\n"、ignore_tags=("script", "style")等默认值。find_all("a", class_="tag"):按标签名与属性查找,class_是class的合法别名(源码注释 解释了为何要白名单处理 Python 保留字)。css(...)与urljoin(...):Scrapling 的Response继承自Selector,因此保留了对熟悉 Scrapy CSS 语法的开发者的兼容性,示例末尾仍用response.css("li.next a::attr(href)")取下一页链接。
Response 类的定义位于 scrapling/engines/toolbelt/custom.py,它的文档字符串说明这是"所有引擎统一返回的响应类型",额外携带 status、reason、cookies、headers、request_headers、history、meta 等 HTTP 元信息,并在构造时记录一条便于调试的日志(Fetched ({status}) <{method} {url}>)。
支持全部四种回调形态,且保留函数内省信息
装饰器能覆盖 Scrapy 支持的所有回调类型:普通函数、生成器、协程和异步生成器。关键在于 scrapling/integrations/scrapy.py 中的分发逻辑——Scrapy 会内省回调函数本身(而不是只看它的返回值)来决定如何处理 yield,因此装饰器必须"以同种形态包裹":
# Each callback kind gets a wrapper of the same kind because Scrapy inspects the
# callback function itself, not just what it returns.
if isasyncgenfunction(func):
@wraps(func)
async def async_gen_wrapper(*args, **kwargs):
args, kwargs = _convert_arguments(args, kwargs)
async for result in func(*args, **kwargs):
yield result
return async_gen_wrapper
elif iscoroutinefunction(func):
@wraps(func)
async def async_wrapper(*args, **kwargs):
args, kwargs = _convert_arguments(args, kwargs)
return await func(*args, **kwargs)
return async_wrapper
elif isgeneratorfunction(func):
@wraps(func)
def gen_wrapper(*args, **kwargs):
args, kwargs = _convert_arguments(args, kwargs)
yield from func(*args, **kwargs)
return wrapper
同时用 functools.wraps 保留原回调的名称与 docstring,所以 Scrapy 的回调内省与 contracts 测试机制不受影响。测试用例 明确验证了这一点:
spider = TestSpider()
assert inspect.isgeneratorfunction(spider.parse)
assert spider.parse.__name__ == "parse"
assert spider.parse.__doc__ == "Parse the page title"
另外两个值得注意的运行时细节,同样有测试背书:
- 响应参数位置灵活:
_convert_arguments会先扫描位置参数、再扫描关键字参数来定位ScrapyResponse实例(实现)。测试验证了parse(response=...)关键字传参同样生效(test_response_passed_as_keyword),且位置参数优先于关键字参数(test_positional_response_wins_over_keyword);参数中完全找不到 Scrapy 响应时会抛出TypeError("No Scrapy response found ...")。 - 额外 kwargs 透传:回调上声明的
cb_kwargs(如parse(response, category=None))会原样传入,不会被装饰器吞掉(test_cb_kwargs_passthrough)。
参数化形式:向 Response 透传 Selector 配置
装饰器支持带参形式,参数会作为 **selector_config 转发给 Scrapling Response(进而传给底层 Selector)的构造函数:
@scrapling_response(adaptive=True, keep_comments=True)
def parse_product(self, response):
...
可透传的配置项在 convert_response 的文档字符串 中列明,与 Selector 构造函数 的参数一一对应:
| 参数 | 类型 | 默认值 | 作用 |
|---|---|---|---|
huge_tree |
bool |
True |
启用 libxml2 的 huge_tree,解析超大文档时应保持开启 |
keep_comments |
bool |
False |
解析时是否保留 HTML 注释 |
keep_cdata |
bool |
False |
是否保留 CDATA 段 |
adaptive |
bool |
False |
开启自适应(adaptive)选择功能,优先级高于其他 adaptive 相关参数 |
storage |
类 | SQLiteStorageSystem |
adaptive 功能使用的存储类(须为带 lru_cache 的 StorageSystemMixin 子类) |
storage_args |
dict |
None |
传给存储类的参数;缺省时自动填充默认的 storage_file 与 url |
adaptive_domain |
str |
"" |
覆盖记录用的 URL 域名(Response 构造 会用它替代原始 url 传给 Selector) |
adaptive=True 的语义可以在 Selector.init 中确认:开启后若未显式传入 _storage,则使用默认的 SQLiteStorageSystem(元素存储数据库位于 scrapling/elements_storage.db 对应的默认路径),并在调用存储类前校验其是否为被 lru_cache 包装的 StorageSystemMixin 子类。测试用例 test_parameterized_form 直接验证了 keep_comments=True 生效后 xpath("//comment()") 能够查到注释节点。
回调之外:直接调用 convert_response
在中间件、管道等非回调场景中拿到 Scrapy 响应时,可以绕过装饰器直接转换:
from scrapling.integrations.scrapy import convert_response
scrapling_response = convert_response(scrapy_response, keep_comments=False, keep_cdata=False)
convert_response 的完整实现在 scrapling/integrations/scrapy.py,它做的是逐字段映射:
request = response.request
cookies: Dict[str, str] = {}
for line in response.headers.getlist(b"Set-Cookie"):
pair = line.split(b";", 1)[0]
if b"=" in pair:
name, _, value = pair.decode("latin-1").partition("=")
cookies[name.strip()] = value.strip()
return Response(
url=response.url,
content=response.body,
status=response.status,
reason=StatusText.get(response.status),
cookies=cookies,
headers=dict(response.headers.to_unicode_dict()),
request_headers=dict(request.headers.to_unicode_dict()) if request else {},
encoding=getattr(response, "encoding", "utf-8"),
method=request.method if request else "GET",
meta=dict(response.meta) if request else {},
**selector_config,
)
其中几个设计决策都对应着具体的坑:
- Cookie 从原始
Set-Cookie头逐行解析:Scrapy 的to_unicode_dict()会用逗号拼接重复头,而Set-Cookie的值(如Expires=Wed, 09 Jun 2027 ...)本身含逗号,拼接后会把一条 Cookie 拆成乱码。因此实现直接遍历response.headers.getlist(b"Set-Cookie")原始头行。测试 用两条带Expires日期的 Set-Cookie 头验证了解析结果{"sid": "abc123", "lang": "en"}的完整性。 - 无 request 的响应同样可转换:
meta属性在响应未绑定 request 时会抛AttributeError,实现里用if request else {}做了防御;编码则用getattr(response, "encoding", "utf-8")兜底——基类二进制响应(如scrapy.http.Response)没有encoding属性,测试 验证此时回退为utf-8且原始字节body原样保留。 meta是浅拷贝:dict(response.meta)复制的是字典本身而非各值,其他中间件放进 meta 的对象(比如scrapy-playwright放在response.meta["playwright_page"]的页面对象)依旧可达。测试 断言了response.meta is not scrapy_response.meta且内容一致。- 状态码 reason 映射:
StatusText.get(status)内置了完整的 HTTP 状态短语表(实现),未知状态码(如测试中的 599)会回退为"Unknown Status Code"。
转换完成后,对象即拥有 Scrapling Response 的全部能力:测试 断言了 url、body、status、reason、encoding、headers、request_headers、meta 的映射,以及 css("h1::text") 选择与 urljoin("/next") 链接拼接均正常工作。
使用注意事项
官方文档在 Notes 一节列出的三条限制,结合源码看都指向同一个核心事实:转换后的对象是 Scrapling 的 Response,但请求调度仍属于 Scrapy 的世界。
-
翻页要 yield
scrapy.Request,不要用response.follow()。Response.follow()(实现)生成的是 Scrapling 自带爬虫系统的scrapling.spiders.Request对象(它还会合并会话参数、自动带 referer),Scrapy 的引擎并不认识这个类型。而且follow()依赖self.request是 Scrapling 的Request实例,在 Scrapy 集成场景中该属性为None,直接调用会抛TypeError("This response has no request set yet.")。正确做法如主示例所示:yield scrapy.Request(response.urljoin(next_page), callback=self.parse)。 -
meta浅拷贝,跨中间件对象可达:如前述,scrapy-playwright等浏览器中间件写入response.meta的页面对象在转换后仍然可以从response.meta["playwright_page"]取出。 -
Cookies 来自原始
Set-Cookie头:如前所述逐行解析,放入 Scrapling 响应的cookies字典,可被find_similar、adaptive 等下游逻辑直接消费。
从源码结构还可以推断一条隐含边界:Scrapy 回调中拿到的 Scrapling Response 主要用于解析,它不会把 self.request 指向任何对象(该字段由 Scrapling 自己的爬虫框架设置,见 构造代码),因此依赖 response.request 的 Scrapling 专有逻辑(如前述 follow)在该集成中不可用。
验证与回归:集成测试覆盖了什么
整个集成的行为边界由 tests/integrations/test_scrapy.py 完整覆盖,分两个测试类:
TestConvertResponse:字段映射、Set-Cookie 解析、无 request 响应、二进制响应编码兜底、未知状态码 reason、selector 配置透传(keep_comments=True对//comment()查询的影响);TestScraplingResponseDecorator:四种回调形态(普通/生成器/协程/异步生成器)各一条用例,外加参数化形式、cb_kwargs透传、关键字传参、位置参数优先、无响应抛TypeError等边界。
测试中构造 Scrapy 响应的方式也值得一提——make_response 直接构造 HtmlResponse 并附带 Request(实现),全程不触碰网络,说明"转换"这一步与网络抓取彻底解耦:无论 Scrapy 用 httpbin、httpx 还是其他下载器拿到的响应,接入 Scrapling 解析都不需要改动。
小结
Scrapling 的 Scrapy 集成是一个刻意做"薄"的适配层:约 130 行的 scrapling/integrations/scrapy.py 只做两件事——把 ScrapyResponse 逐字段映射为 Scrapling Response(处理 Cookie、meta、编码等易错点),以及按回调形态包裹转换逻辑并保持内省信息。爬虫、调度、去重、重试继续由 Scrapy 承担,而 find_by_text、find_similar、adaptive 选择器等 Scrapling 解析能力则通过一个装饰器或一次函数调用即可在既有项目中落地。如果你正在评估是否迁移,最低成本的验证路径就是:在一个既有 spider 上加上 @scrapling_response,把解析逻辑换成 Scrapling API 跑一轮,再决定后续范围。
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 StartedRust0623
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