Scrapling 平台爬虫模板详解:ShopifySpider 如何免写 HTML 解析抓取整店商品
【免费下载链接】Scrapling🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling
本文围绕 Scrapling 的"平台型爬虫模板"(Platform Spider Templates)展开:它面向 Shopify 这类"统一数据结构、跨大量独立站点"的平台,只需用ShopifySpider指定一个店铺域名,即可通过平台的 JSON API 抓取全部商品与变体数据。读完本文,你将掌握平台模板与通用模板的本质区别、ShopifySpider的分页抓取流程、全部 item 字段的来源与取值规则、target_website的域名解析逻辑,以及如何通过覆写_process_product()定制输出结构,并能结合仓库源码验证每一个行为细节。
平台模板 vs 通用模板:按"平台"而非按"模式"复用
Scrapling 内置的爬虫模板分为两类,理解两者的定位是理解本文的前提:
- 通用模板(CrawlSpider、SitemapSpider、XMLFeedSpider、CSVFeedSpider)覆盖的是爬取的模式:跟踪匹配某模式的链接、遍历站点 sitemap、迭代 XML/CSV 数据源。它们需要你针对具体站点的 HTML 结构自己写解析逻辑。
- 平台模板覆盖的是平台:网站构建器(site builder)会在它托管的众多独立网站上暴露出相同的机器可读结构,所以爬虫天生知道"数据在哪里"——你只需把爬虫指向某个具体域名。
从源码结构看,这一划分直接体现在模板包的组织方式上:scrapling/spiders/templates/init.py 将通用模板(CrawlSpider、SitemapSpider、XMLFeedSpider、CSVFeedSpider)与平台模板(ShopifySpider)统一导出,而 scrapling/spiders/init.py 再将其提升到scrapling.spiders顶层,因此可以直接from scrapling.spiders import ShopifySpider。
当前仓库中唯一的平台模板就是ShopifySpider,它是本文的绝对主角。
ShopifySpider:最小可用示例
ShopifySpider通过 Shopify 店铺的 JSON API 提取任意 Shopify 驱动商店的全部商品,全程不接触网站的 HTML。最小用法如下:
from scrapling.spiders import ShopifySpider class MyStore(ShopifySpider): target_website = "example.com" result = MyStore().start() print(result.items[0])将target_website设置为商店域名即可,爬虫会自行完成 collections 与 products 两层的分页遍历,result.items中是逐变体展开的商品条目。Spider基类的其余能力全部继承可用:并发配置、下载延迟、robots.txt 遵循、检查点(checkpoint)以及生命周期钩子——这套异步调度、并发控制与断点续爬的完整机制,可参见 Spider 架构文档。
目标域名解析:三级回退与自动归一化
target_website并非唯一指定目标的方式。从 shopify.py 的__init__可以看到,域名的确定遵循明确的三级回退顺序:
- 优先使用
target_website; - 为空时,回退到
start_urls的第一个条目; - 仍为空时,回退到
allowed_domains的第一个条目; - 三者全部为空则抛出
ValueError,错误信息明确要求设置其中任意一项。
无论来源是裸域名(example.com)还是完整 URL(https://example.com/collections/all),最终都会被归一化为纯域名(netloc):没有://的字符串会先补上https://前缀再交给urlparse取netloc。
这套行为在 tests/spiders/test_shopify.py 的TestDomainResolution中有逐项验证:从target_website取域名、从完整 URL 归一化出域名、从start_urls回退(含www.子域保留)、从allowed_domains回退,以及三源皆空时抛出ValueError,五个用例与源码实现一一对应。
抓取流程:collections.json → products.json → 变体级条目
ShopifySpider的抓取逻辑由三个类属性 URL 模板驱动(shopify.py#L33-L37):
name = "shopify" target_website = "" collections_url = "https://{website}/collections.json?page={page}&limit=250" products_url = "https://{website}/collections/{handle}/products.json?page={page}&limit=250" product_url = "https://{website}/collections/{handle}/products/{product_handle}"其中limit=250是 Shopify 平台的单页上限。完整流程分三步:
第一步:遍历 collections.json 分页
start_requests()只产出第一个请求:https://<store>/collections.json?page=1&limit=250,并携带meta={"page": 1}。parse()处理该响应时做两件事(shopify.py#L54-L70):
- 对当前页中每一个
products_count非零的 collection,派发一个指向/collections/<handle>/products.json?page=1的请求,meta中记录handle与page; - 只要当前页的
collections列表非空,就继续派发下一页(page + 1)的 collections.json 请求。空页即终止,不再产生后续请求。
products_count在这里只被当作"非零就抓这个 collection"的信号,从不作为期望总数使用——原因见下文"注意事项与限制"。
第二步:遍历每个 collection 的 products.json 分页
parse_collection()(shopify.py#L96-L112)对每个 collection 的响应执行:
- 对
products列表中的每个产品,调用_process_product()逐变体产出条目; - 只要当前页
products非空,就派发该 collection 的下一页产品请求; - 空页表示该 collection 已抓完,记录一条 debug 日志("Extracted all products from collection ")并停止。
测试用例test_parse_dispatches_collections_and_next_page、test_parse_stops_on_empty_page、test_parse_collection_stops_on_empty_page(test_shopify.py)精确覆盖了这套分页边界:2 个 collection(含一个空 collection 被跳过)时派发 2 个请求、空 collections 页返回 0 个请求、空 products 页返回 0 个请求。
第三步:按变体产出条目并跨 collection 去重
去重状态存放在实例属性self.collected_ids(一个set,在__init__中初始化)。每个变体以variant["id"]作为键:已见过的变体 id 直接跳过。由于同一个产品变体可能出现在多个 collection 中,这一机制保证每个变体只产出一次条目。测试用例test_variants_deduplicated_across_collections验证了同一产品在lipsticks与bestsellers两个 collection 中只产出一次,test_dedup_state_is_per_instance则确认去重状态是实例级的——不同 Spider 实例之间互不干扰。
条目字段详解:每个字段的来源与取值规则
ShopifySpider默认每个变体产出一个 dict 条目,字段结构与底层数据来源如下(逐项对应 _process_product() 的实现):
| 字段 | 来源与规则 |
|---|---|
name | 产品标题;当变体标题不是Default Title时,追加" - <变体标题>" |
price | 变体价格,字符串,保持 Shopify 返回的原样(如"9.00") |
category | 所属 collection 的 handle,做title-case化(summer-sale变成Summer Sale) |
brand | 产品的vendor字段 |
identifier | 变体 id(variant["id"]) |
sku | 变体 SKU;店铺未设置时为""(None也被归一为空串) |
stock | 变体有货为None,缺货为0 |
image_url | 产品第一张图的src;无图为"" |
url | 产品在所属 collection 内的商品页 URL(product_url模板拼出) |
description | 产品body_html经remove_tags(来自w3lib.html)剥离 HTML 标签后的纯文本 |
old_price | compare_at_price,但仅当它是真实预售价(非零)时才保留,否则为"" |
barcode | 变体barcode,无则为""(大多数店铺不在这两个端点暴露该字段) |
测试数据(test_shopify.py#L19-L59 的PRODUCTS_PAGE)恰好覆盖了所有边界分支,TestItemProcessing的断言可以直接当作"字段规则 → 期望输出"的验收清单,例如:
- 变体标题为
Red(非默认)→name为"Matte Lipstick - Red",标题为Default Title→name保持产品原标题; sku: None→ 归一为"";available: False→stock为0;compare_at_price: "0.00"→old_price为"","12.00"→ 原样保留;body_html: None→description为"",images: []→image_url为""。
自定义输出结构:覆写_process_product()
上述字段不是强制契约。如果你想改变条目结构、或从产品数据中提取其他字段,只需在子类中覆写_process_product()——它的签名是_process_product(self, product: Dict, collection_handle: str) -> Generator[Dict, None, None],接收单个产品 dict 与所属 collection 的 handle,以生成器形式逐条yield结果。覆写后,parse_collection()的分页、去重之外的调用链保持不变(注意:跨 collection 的变体去重发生在_process_product()内部,如果你完全重写该方法,需要自行决定是否保留self.collected_ids去重逻辑)。
注意事项与限制
使用ShopifySpider前必须了解以下边界(来自 platform-templates.md 与源码行为):
- JSON 端点的可见性限制:
/collections.json与/products.json只暴露已发布到 online-store 渠道的商品,因此一个 collection 报告的products_count可能大于实际能抓到的数量。源码中(shopify.py#L58)if collection["products_count"]:的判断印证了这一点——该计数只被用作"是否抓取"的开关,而不是断点校验或完整性预期。 - 受保护店铺大概率不可用:位于额外防护或密码保护(Shopify 店铺密码页)后面的商店,该模板很可能无法工作;即使勉强可用,也需要你进行大量覆写。
- Spider 基类能力照常生效:
robots_txt_obey、concurrent_requests、下载延迟、crawldir检查点(暂停/恢复)与生命周期钩子等 Spider 系统 提供的一切机制对平台模板同样适用。
什么样的平台有资格成为平台模板
仓库文档给出了明确的准入标准:平台模板只接受"在大量独立网站上暴露统一、机器可读结构"的平台,Shopify 的 collections/products JSON 端点即典型例子。反过来,针对某一个特定网站的爬虫不属于库的收录范围——无论该网站有多受欢迎。从源码结构看,这一约束也反映在实现方式上:ShopifySpider的全部逻辑都建立在平台级 URL 模板(collections_url/products_url/product_url类属性)之上,没有任何站点专属的 HTML 选择器或硬编码路径——这正是"平台模板"与"单站爬虫"的结构性差异。
小结
Scrapling 的平台模板把"数据在哪里"的知识封装进了库:ShopifySpider用约 110 行源码(scrapling/spiders/templates/shopify.py)实现了三级域名回退、双层分页遍历、变体级去重和 12 个规范化字段,并留出_process_product()作为唯一的定制点。对任意一家 Shopify 店铺,你只需要定义一个两行的子类并调用start();而docs/spiders/generic-templates.md中的通用模板则继续负责链接跟踪、sitemap 与数据源迭代这些"模式级"的复用。两者叠加,覆盖了从单页抓取到整站乃至整平台规模抓取的大部分场景。
【免费下载链接】Scrapling🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考