Scrapling 平台型 Spider 模板详解:用 ShopifySpider 通过 JSON API 直接抽取任意 Shopify 商店的产品数据
在 Scrapling 的爬虫体系中,通用模板(如 CrawlSpider、SitemapSpider)解决的是"按什么模式爬",而平台模板解决的是"数据藏在哪里":对于 Shopify 这类在成千上万个独立站点上暴露同一套机器可读结构的建站平台,Spider 已经内置了数据结构知识,你只需要告诉它一个域名。读完本文,你将理解 ShopifySpider 的完整工作流程、12 个标准 item 字段的来源与取值规则、变体去重机制的实现细节,以及如何通过覆写 _process_product() 自定义输出结构。
平台模板 vs 通用模板:定位的区别
Scrapling 的 Spider 模板分为两类(可参见 平台模板文档 与 通用模板文档):
- 通用模板覆盖爬取模式:跟随链接、遍历 sitemap、解析 XML/CSV 数据源等。
- 平台模板覆盖平台:站点构建工具在大量独立网站上暴露相同的机器可读结构,Spider 已经知道数据在哪里,你只需要把目标指向一个域名。
判断标准在文档中写得很明确:只有当某个平台在多个独立网站上暴露统一、机器可读的结构(如 Shopify 的 JSON 端点)时,才配得上进入库内模板。针对某个具体网站的 Spider——无论该网站多么流行——都不属于库的收录范围。
ShopifySpider 快速上手
ShopifySpider 从任意 Shopify 商店的 JSON API 中抽取全部产品,完全不接触网站 HTML。最小可用示例:
from scrapling.spiders import ShopifySpider
class MyStore(ShopifySpider):
target_website = "example.com"
result = MyStore().start()
print(result.items[0])
ShopifySpider 通过 scrapling.spiders 包顶层直接导出,见 scrapling/spiders/init.py,因此 from scrapling.spiders import ShopifySpider 与 from scrapling.spiders.templates import ShopifySpider 等价。
目标域名的解析规则
将 target_website 设置为商店域名。当它为空时,Spider 按如下回退链确定目标(源码见 scrapling/spiders/templates/shopify.py):
target_website(优先级最高);start_urls的第一项;allowed_domains的第一项;- 三者皆空时抛出
ValueError,错误信息形如MyStore must set target_website, start_urls, or allowed_domains。
解析逻辑的关键一行:
source = self.target_website or next(iter(self.start_urls or ()), "") or next(iter(self.allowed_domains), "")
if not source:
raise ValueError(f"{self.__class__.__name__} must set `target_website`, `start_urls`, or `allowed_domains`")
self.target_website = urlparse(source if "://" in source else f"https://{source}").netloc
即:完整 URL 会被自动归一化为纯域名(https://example.com/collections/all 归一化为 example.com),缺少 scheme 的输入会先补上 https:// 再取 netloc。tests/spiders/test_shopify.py 中的 TestDomainResolution 对以上四条路径(target_website、URL 归一化、start_urls 回退、allowed_domains 回退、三者皆空抛错)逐一做了断言验证。
工作原理:两级分页 + 变体去重
ShopifySpider 的爬取流程共三步(源码 scrapling/spiders/templates/shopify.py):
-
翻页
https://<store>/collections.json。URL 模板由类属性collections_url定义,每页固定limit=250(Shopify 平台对该端点的上限):collections_url = "https://{website}/collections.json?page={page}&limit=250" -
对每个报告有商品的 collection,翻页
/collections/<handle>/products.json,模板为:products_url = "https://{website}/collections/{handle}/products.json?page={page}&limit=250"只有
products_count非零的 collection 才会被请求。 -
每个产品变体(variant)产出一个 item,对出现在多个 collection 中的同一变体做去重。
启动请求与 collection 分发
start_requests() 只发出第一个请求:https://<store>/collections.json?page=1&limit=250,回调指向 parse(),并在 meta 中携带 {"page": 1} 用于后续翻页(见 shopify.py L47-L52)。
parse() 处理每页 collections 的逻辑(见 shopify.py L54-L70):
- 遍历
response.json()["collections"],对每个products_count为真的 collection,发出products.json?page=1请求,回调parse_collection,meta携带{"handle": ..., "page": 1}; - 只要本页还有 collection,就发出下一页
collections.json?page=N+1请求; - 翻页终止条件:某页返回空
collections列表时不再发出任何请求。测试用例 test_parse_stops_on_empty_page 验证了这一行为。
产品页解析与终止条件
parse_collection()(见 shopify.py L96-L112):
- 遍历
response.json()["products"],把每个产品交给_process_product()产出 item; - 只要本页有产品就发出
page+1请求;返回空products时记录一条 debug 日志Extracted all products from collection <handle>并停止。
[tests/spiders/test_shopify.py](https://gitcode.com/GitHub_Trending/sc/Scrapling/blob/9af7e2796ac2d06be2d9e52fa2dccd767c883537/tests/spiders/test_shopify.py?utm_source=gitcode_repo_files#L140-L165) 中的 test_parse_collection_yields_items_and_next_page 与 test_parse_collection_stops_on_empty_page 分别覆盖了"产出 item + 发下一页请求"和"空页终止"两个分支。
Item 字段说明
每个 item 的 12 个字段及其来源(对照 _process_product() 实现):
| 字段 | 来源与取值规则 |
|---|---|
name |
产品标题;当变体标题不是 Default Title 时追加 - <变体标题> |
price |
变体价格(字符串,Shopify 原样返回,如 "9.00") |
category |
collection handle 转为 title-case(summer-sale 变 Summer Sale,由 handle.replace("-", " ").title().strip() 实现) |
brand |
产品的 vendor |
identifier |
变体 id(整数) |
sku |
变体 SKU;商店未设置时为 "" |
stock |
available 为真时 None,缺货时 0 |
image_url |
第一张产品图的 src;无图时 "" |
url |
产品在所属 collection 内的页面地址(由 product_url 模板拼出) |
description |
产品 body_html 经 w3lib.html.remove_tags 去掉 HTML 标签后的纯文本 |
old_price |
compare_at_price 为真实原价时保留,否则 ""(判定条件是"非空且 float() 后非零") |
barcode |
变体条形码,或 ""(多数商店不在这些端点暴露该字段) |
几个值得注意的实现细节(均有测试断言佐证,见 test_shopify.py L168-L190):
- 价格保持字符串:
price直接透传 Shopify 的原始字符串,方便你按自身业务决定如何解析,避免浮点误伤; stock的二值语义:None表示有货、0表示缺货,而非真实库存数量——这些端点不提供库存深度;old_price的过滤:"0.00"或空值会被归一为"",只有真正的预售价(比较价)才保留;Default Title变体不追加后缀:测试用例test_default_title_variant_keeps_product_name确认了单变体产品的name就是产品标题本身。
变体去重机制
同一产品会出现在多个 collection 中(例如 lipsticks 和 bestsellers 同时包含同一支口红),每个变体在多个 collection 下都会出现。Spider 在实例属性 self.collected_ids: Set[int](在 __init__ 中初始化)记录已产出的变体 id:_process_product() 遇到 variant["id"] 已在集合中的变体时直接 continue,否则先加入集合并产出 item。
这带来两个可验证的行为(见 test_shopify.py L202-L212):
- 同一产品先以
lipsticks处理产出 2 个 item,再以bestsellers处理时产出[]; - 去重状态是每实例独立的:两个新实例各自处理同一产品,都完整产出 2 个 item。
也就是说,一次爬取(一个 Spider 实例)内保证变体不重复产出;跨实例不复用去重状态。若你启用了 checkpoint 恢复,恢复后的新实例去重集合是空的,这一点在长爬取场景下值得留意。
自定义输出:覆写 _process_product()
上述 12 个字段不是强制的。文档明确指出:在子类中覆写 _process_product() 即可改变 item 结构、或从产品数据中提取不同字段。该方法签名是:
def _process_product(self, product: Dict, collection_handle: str) -> Generator[Dict[str, Any], None, None]:
它接收完整的 Shopify 产品 JSON(含 id、title、handle、vendor、body_html、images、variants 等字段)与当前 collection handle,以生成器方式 yield item。一个典型的覆写示例:
from scrapling.spiders import ShopifySpider
class MyStore(ShopifySpider):
target_website = "example.com"
def _process_product(self, product, collection_handle):
# 每个产品只产出一条,取最便宜的变体
cheapest = min(product["variants"], key=lambda v: float(v["price"]))
yield {
"name": product["title"],
"price": cheapest["price"],
"handle": product["handle"],
"category": collection_handle,
}
覆写时请注意:如果你仍希望保留"跨 collection 变体去重"的语义,需要自己在方法内维护类似 collected_ids 的状态(父类的去重逻辑是内联在 _process_product() 里的,覆写即被替换)。
限制与注意事项
文档列出的三点限制,逐条对照源码确认:
-
products_count不是可信的总量。JSON 端点只暴露已发布到 online-store 渠道的产品,collection 的products_count可能大于实际可抓取数。Spider 源码中只有一处用到它:if collection["products_count"]: yield Request(...)即"非零就抓",从不将其当作预期总数。
-
受额外防护或密码保护的商店大概率不可用。这些端点依赖匿名可访问的
*.json路径;若目标商店有此类防护,模板即便可用也需要你从会话、请求头、回调等多个层面做大量覆写。 -
Spider基类的全部能力继续生效。ShopifySpider继承自 Spider,因此以下配置项原样可用:- 并发与限速:
concurrent_requests(默认 4)、concurrent_requests_per_domain、download_delay、max_blocked_retries,以及 AutoThrottle 系列(autothrottle_enabled、autothrottle_start_delay等,见 spider.py L82-L93); - robots.txt 合规:
robots_txt_obey(默认False,需显式开启); - checkpoint 断点续爬:构造时传入
crawldir即启用,interval参数(默认 300 秒)控制定期保存间隔,见 Spider 构造函数; - 生命周期钩子:
on_start、on_close、on_error、on_scraped_item(返回None可静默丢弃某个 item)等,见 spider.py L178-L212。
start()返回的CrawlResult携带items(产出的 item 列表)、stats(统计)与paused状态,示例中result.items[0]即取第一个产品变体。 - 并发与限速:
小结
ShopifySpider 展示了平台模板的设计范式:把"平台数据结构知识"固化为模板(URL 模板、两级分页、字段映射、变体去重),把"目标是谁"留给使用者的一行 target_website。它的行为边界(250 条/页、空页终止、products_count 仅作触发条件、每实例去重)都能直接在 scrapling/spiders/templates/shopify.py 与 tests/spiders/test_shopify.py 中找到对应实现与断言。对于其他满足"统一、机器可读结构"条件的平台,按同样的模式扩展模板——覆写 URL 模板属性与解析回调即可复用 Spider 基类提供的并发、限速、合规与断点续爬全套基础设施。
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 StartedRust0624
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