
做跨境选品的朋友大概都遇到过这种局面想系统性地摸一遍某个站点的商品结构结果打开页面一看分类导航只给你展示最上面那几层再往下要么是懒加载要么是折叠起来要么干脆藏在某个接口里。手动一层层点下去几十上百个末级类目点完人已经麻了还容易漏。我前后在几个不同站点上折腾过这件事踩过的坑不算少今天就把“把一个站点全部商品分类完整扒下来”这件事从头到尾讲清楚包括为什么要这么设计、接口怎么找、递归怎么控、数据怎么存、以及那些只有真跑过才会遇到的坑。这篇内容适合两类人一类是做选品、做市场结构分析、做竞品类目覆盖度盘点的运营或分析师需要一份干净完整的类目树另一类是想练手数据采集的开发者拿类目树当练手项目结构清晰、层级明确、数据量适中非常适合把“递归抓取去重落库”这套流程跑通。下面所有内容都围绕“如何稳定、完整、可复现地拿到全站商品分类”展开不涉及任何账号登录、不涉及任何绕过正常访问限制的手段纯粹从公开页面与公开接口的角度讲方法。1. 先搞清楚“全部商品分类”到底藏在哪很多人一上来就写爬虫去扒首页导航扒完发现只有二三十个大类心里还挺美觉得“分类也不多嘛”。结果一做选品分析就露馅了——某个大类下面明明有几百个细分品类你手里只有它的名字没有任何子节点。问题出在站点展示给你的分类和它实际拥有的分类根本不是一回事。1.1 页面导航只是分类树的“冰山一角”首页或者频道页上的分类导航本质上是给普通消费者用的“快捷入口”它只展示高频访问的顶层类目以及运营手动配置的一部分推荐位。真正的完整分类体系通常存在两套东西一套是面向展示的导航结构另一套是面向检索和归类的底层类目体系。后者才是“全部商品分类”的真身它的层级往往更深、节点更多而且很多末级节点在页面上压根不会出现入口。我一般把这两者比作超市导航是门口那块“生鲜区、日化区、零食区”的指示牌而底层类目体系是仓库里的货架编号细到“膨化食品-薯片-原味”这种粒度。你要做的是拿到货架编号表而不是拍一张指示牌的照片。所以第一步不是写代码而是判断目标站点把完整类目体系暴露在了哪里。常见的有三个位置页面里内嵌的初始化数据一段 JSON、前端异步请求的分类接口、以及站点地图里的分类页链接。这三个位置的可获取性和完整度差别很大下面逐个说。1.2 三种常见暴露位置的可获取性对比暴露位置完整度获取难度稳定性适用场景页面内嵌初始化 JSON中到高低中随页面改版变动快速拿顶层部分子类前端异步分类接口高中较高接口相对稳定递归拿全量类目树站点地图分类链接高低高补全末级类目、校验遗漏页面内嵌 JSON 的好处是打开页面源码就能看到通常是一个大的初始化对象里面带着当前页相关的类目数据。缺点是它往往只包含“当前上下文需要”的那部分比如你打开某个大类页面它只给你这个大类及其子类不会给你全站。想靠它拼出全站树得把每个大类页面都打开一遍成本不低。前端异步接口是我最推荐的主路径。分类这种数据天然适合做成接口因为前端要渲染多级菜单、要做搜索联想、要做筛选都依赖它。这类接口一般返回结构化的层级数据一次请求能拿到一个父节点下的全部子节点递归下去就能拼出整棵树。站点地图里的分类链接则是最好的“校验工具”。它通常把站内所有可访问的分类页都列出来了你可以拿它和你递归抓到的结果做比对看看有没有漏掉的末级节点。这个思路后面会专门讲。1.3 判断依据打开开发者工具看请求具体怎么判断打开目标页面的开发者工具切到网络面板筛选出 XHR/Fetch 类型的请求然后刷新页面重点看那些返回内容里带类目名称、类目 ID、层级字段的请求。判断一个请求是不是分类接口看三个特征返回体里有明显的层级结构children、subCategories 之类的字段、有稳定的类目标识id、nodeId、categoryId、返回的数据量和页面展示的分类数量对得上。这里有个经验分类接口往往在页面加载早期就被调用因为导航要先渲染出来。所以刷新后按时间排序往前面找命中率更高。另外有些站点会把分类数据做成一个独立的、带版本号的静态 JSON 文件这种更好办直接请求就行连参数都不用拼。提示找接口的时候别只盯着“category”这个词。有些站点用的是“node”“taxonomy”“browse”“department”这类命名光搜 category 会漏。我一般会把返回体里出现类目名称的请求都过一遍。2. 递归抓取类目树的核心逻辑与参数设计找到接口只是开始真正决定你能不能拿到“全部”的是递归逻辑设计得对不对。类目树本质是一棵多叉树抓取过程就是一次树的遍历。听起来简单但实际跑起来节点去重、层级控制、请求节奏这三件事没处理好结果要么重复要么缺失要么被封。2.1 从根节点出发的广度优先遍历我倾向用广度优先BFS而不是深度优先DFS。原因很实际类目树的深度一般不会太夸张常见 4 到 6 层但宽度可能很大某个顶层类目下挂几百个子类很正常。BFS 的好处是每一层抓完再进下一层进度可控中途断了也容易续DFS 一头扎到底万一某个分支特别深容易在一条路上耗太久而且断点续传不好做。BFS 的实现思路用一个队列就够了先把根节点或者顶层类目列表入队然后循环出队对每个节点请求它的子类接口把返回的子节点入队同时记录当前节点已经处理过。伪代码大概是这样from collections import deque def crawl_category_tree(root_ids, fetch_children): queue deque(root_ids) visited set() tree {} while queue: node_id queue.popleft() if node_id in visited: continue visited.add(node_id) children fetch_children(node_id) # 请求接口拿子节点 tree[node_id] children for child in children: if child[id] not in visited: queue.append(child[id]) return tree这段代码的关键在visited集合。类目树理论上不该有环但实际数据里偶尔会出现“某个子类又被挂到另一个父类下”的情况多归属或者接口返回里带了指回父级的引用。没有visited去重轻则重复请求重则死循环。我见过有人跑了一晚上发现请求量爆炸最后查出来是某个节点被反复入队。2.2 层级深度与请求量的估算在动手前先估算一下请求量心里有个数。假设类目树平均每个父节点有 15 个子节点深度 5 层那么总节点数大约是 1 15 225 3375 50625五万多个节点。如果每个节点都要单独请求一次子类接口那就是五万多次请求。这个量级必须提前想好节奏否则要么跑太久要么触发站点的频率限制。实际中很多接口支持“一次返回某个父节点下的全部子节点”甚至有的接口一次能返回两三层这样请求量会大幅下降。所以先摸清接口的返回粒度再决定递归策略。如果接口一次只返回一层那就老老实实按节点数估如果一次返回多层可以少递归几层把返回结果直接展开。我一般会先手动请求几个不同层级的节点看看返回体里 children 的深度确认接口是“单层返回”还是“多层返回”。这个动作花不了几分钟但能帮你把请求量估准避免跑到一半发现量级不对。2.3 请求节奏与并发控制请求节奏这块我的原则是宁可慢一点也别把路走死。类目数据是静态的今天抓和明天抓没区别没必要追求速度。具体参数上单线程加固定间隔是最稳的间隔设在 0.5 到 1 秒之间具体看站点响应速度。如果确认站点对频率不敏感可以上小并发但并发数别超过 3 到 5而且要做失败重试和退避。这里有个容易被忽略的点递归抓取会产生大量“短请求”每个请求返回的数据量很小但请求数很多。这种模式对目标服务的压力其实不小也更容易被识别为异常流量。所以间隔不能省重试要有上限比如最多重试 3 次每次退避时间翻倍失败超过上限就记录下来跳过别死磕。import time import random def fetch_with_retry(url, params, max_retry3): for attempt in range(max_retry): try: resp request_get(url, params, timeout10) if resp.status_code 200: return resp.json() except Exception: pass time.sleep((2 ** attempt) random.random()) return None退避里加一点随机抖动jitter是常规做法避免多个请求在同一时刻集中重试。这个细节在单线程下影响不大但一旦上了并发就很关键。3. 数据落库与类目树的存储结构抓到数据之后怎么存直接决定了你后面能不能方便地用它做分析。很多人图省事把接口返回的原始 JSON 一股脑塞进一个文件或者一张表结果要用的时候发现根本没法查——“给我某个末级类目的完整路径”这种需求面对一坨嵌套 JSON 直接抓瞎。3.1 扁平表与嵌套 JSON 的取舍我的建议是两份都存原始 JSON 存一份做备份和回溯扁平化的节点表存一份做查询。原始 JSON 的好处是保真接口返回什么就是什么将来发现解析逻辑有问题还能重新处理扁平表的好处是查询方便每个节点一行父节点 ID、层级、路径都拆成独立字段。扁平表的结构大概是这样字段类型说明node_idstring类目唯一标识parent_idstring父节点标识根节点为空namestring类目名称levelint层级根为 1pathstring完整路径如“A B C”path_idsstring路径上的 ID 串便于反查is_leafbool是否末级rawtext原始节点 JSONpath和path_ids这两个字段是精髓。有了它们“查某个末级类目的完整归属链”就是一次简单的字符串查询不用递归。is_leaf则方便你快速筛出所有末级类目——做选品分析时末级类目往往才是真正对应具体商品的那一层。3.2 用路径字段解决“反查归属”的高频需求为什么强调路径字段因为实际分析里最高频的需求就是“这个商品属于哪条类目链”。比如你拿到一批商品每个商品带一个末级类目 ID你想知道它们分别归属到哪些顶层大类好做汇总。如果只有父子关系表你得写递归查询往上追有了path字段直接按末级 ID 查一行路径就出来了。生成路径的时机是在 BFS 遍历过程中出队一个节点时它的路径 父节点路径 当前节点名。所以队列里存的不能只是节点 ID得带上父路径信息。改造一下前面的伪代码queue deque([(rid, , 1) for rid in root_ids]) # (id, parent_path, level) while queue: node_id, parent_path, level queue.popleft() node get_node_info(node_id) current_path f{parent_path} {node[name]} if parent_path else node[name] save_node(node_id, node[parent_id], node[name], level, current_path) for child in fetch_children(node_id): queue.append((child[id], current_path, level 1))这样每个节点落库时路径就是现成的不用事后补算。3.3 增量更新与全量重抓的判断类目树不是一成不变的站点会新增类目、下线类目、调整归属。所以要有更新机制。我的做法是定期全量重抓 差异比对而不是做复杂的增量。原因很简单类目数据量不大几万个节点全量重抓成本可控而增量逻辑要处理“新增、删除、移动”三种情况容易出错。全量抓完和上一版做 diff把变化的部分单独输出人工确认一下就行。diff 的粒度按node_id比对新版本有、旧版本没有的是新增旧版本有、新版本没有的是下线两边都有但parent_id或path变了的是移动。这三种变化分别打标输出成一张变更表比闷头覆盖要靠谱得多。4. 实测中那些让人抓狂的坑前面讲的是“应该怎么做”这一节讲“实际做的时候会怎么翻车”。这些坑我在不同站点上基本都踩过一遍有些是设计问题有些纯粹是数据脏。4.1 末级类目判定没有 children 不等于末级最典型的一个坑判断末级类目时很多人直接看接口返回的 children 是不是空。逻辑上没错但实际数据里有些非末级节点的 children 字段是缺失的而不是空数组。你按“children 为空就是末级”去判会把这些节点误判成末级导致它们的子类永远抓不到。正确的做法是区分三种情况children 是空数组真末级、children 字段不存在未知需要再确认、children 有内容非末级。对于“字段不存在”的节点要么再单独请求一次它的详情接口确认要么标记为“待定”最后用站点地图的链接做交叉校验。我一般会把这些待定节点单独存一张表跑完之后人工抽查几个确认是接口特性还是数据问题。4.2 多归属节点导致的重复与环前面提过visited去重这里展开说。多归属指的是同一个类目节点同时挂在多个父节点下这在电商类目体系里其实挺常见——一个商品可能既属于“厨房用品”又属于“礼品”。反映到数据上就是同一个node_id会从不同父节点被遍历到。如果只按node_id去重你会丢掉其中一条归属路径如果不去重就会重复抓取甚至成环。我的处理方式是按“父节点 ID 子节点 ID”的组合去重允许同一个节点在不同父节点下各存一条记录但同一个父节点下不重复。这样既保留了多归属信息又避免了环。落库时主键用parent_id node_id的联合主键。4.3 接口分页分类接口也会分页这个坑比较隐蔽。大部分分类接口一次返回全部子节点但有些站点子节点特别多的时候会分页。如果你没注意分页参数只拿了第一页就会漏掉后面的子类。判断方法很简单看返回体里有没有 total、page、pageSize、hasMore 这类字段。有的话就必须循环翻页直到拿全。我遇到过一次某个大类下有 400 多个子类接口默认每页 50 条我第一版代码没处理分页结果那个大类只抓到 50 个子类后面做完整性校验时才发现少了 350 多个。这种漏是“静默”的不报错不校验根本发现不了。4.4 名称里的特殊字符与编码问题类目名称里带特殊字符是常态比如、、/、各种语言的重音符号。这些字符在拼接路径、写数据库、导出 CSV 时都可能出问题。最稳妥的做法是存储时保留原始字符只在展示和导出时做转义。路径分隔符别用/用或者|避免和类目名里的斜杠冲突。编码上统一用 UTF-8请求时注意响应头的 charset有些站点返回的 Content-Type 里没写编码实际是 GBK 或者别的直接按 UTF-8 解会乱码。遇到乱码先看响应头再看返回体开头的 meta 声明两个都对不上就试常见编码。5. 完整性校验怎么确认你真的抓“全”了抓完之后最怕的就是“以为抓全了其实漏了”。完整性校验这一步不能省它是区分“能跑”和“靠谱”的关键。我一般用两个手段交叉验证站点地图比对和抽样人工核对。5.1 用站点地图做交叉比对站点地图sitemap里通常包含站内所有分类页的链接每个链接对应一个类目。把 sitemap 里的分类 URL 全部提取出来和数据库里的节点做比对看有没有 sitemap 里有、数据库里没有的。有的话说明你的递归漏了得回去查是哪个父节点没展开。比对时要注意 URL 和 node_id 的映射关系。有些站点的分类页 URL 里直接带类目 ID好办有些是带类目名的 slug得做一次名称到 ID 的映射或者反过来。映射不上的先单独列出来人工看几个判断是 slug 规则问题还是真的漏抓。5.2 抽样核对末级类目的商品数另一个校验角度是看末级类目下有没有商品。理论上每个末级类目都应该对应一批商品如果你抓到的某个“末级类目”点进去一个商品都没有那它很可能不是真末级下面还有子类没展开。这个校验不需要全量做抽几十个末级类目各打开看一下商品列表是否非空就能发现系统性问题。我一般会抽三类节点重点看层级最深的那些、名称里带“其他/更多/全部”字样的这类往往是兜底类目结构特殊、以及 children 字段缺失被标为待定的那些。这三类是最容易出问题的。5.3 节点总数与层级分布的合理性检查最后一个快速检查看节点总数和层级分布是否符合常识。一个正常的电商站点类目总数通常在几千到几万之间层级集中在 3 到 5 层。如果你抓出来只有几百个节点或者某一层突然断崖式减少比如第 3 层有几千个第 4 层只有十几个那基本可以判定中间有环节漏了。把层级分布打成一张表每层的节点数、末级节点数、平均子节点数都列出来一眼就能看出异常。这个检查花不了几分钟但能拦住大部分“静默漏抓”。层级节点数末级节点数平均子节点数异常判断130012正常236059正常332008006正常4480046002需关注骤降512120异常疑似漏抓上面这张表里第 5 层只有 12 个节点明显不对劲正常应该有几百个。这种情况多半是第 4 层某些节点的子类没展开回去查第 4 层里 children 字段缺失的那些节点就对了。6. 把类目树用起来几个真实的分析场景数据抓全只是第一步真正体现价值的是怎么用。这里分享几个我实际做过的分析场景都是围绕类目树展开的操作不复杂但很实用。6.1 类目覆盖度盘点最直接的用法拿一份目标站点的类目树和你自己店铺/产品的类目覆盖做对比看哪些类目你有商品、哪些没有。把类目树按末级节点展开每个末级节点标记“已覆盖/未覆盖”然后按顶层大类汇总就能看出你的品类布局在哪些大方向上存在空白。这个分析对选品和品类拓展很有参考价值比拍脑袋决定做哪个类目靠谱得多。具体操作上把你自己的商品类目 ID 和抓下来的类目树做左连接未匹配上的末级节点就是空白。注意两边类目体系可能不完全一致需要做一次映射映射不上的单独处理。6.2 类目深度与商品分布的关系另一个有意思的分析看商品在不同层级的分布。有些站点商品大量集中在较浅的层级比如第 3 层就挂商品有些则要求必须挂到末级。这个分布能反映站点的类目管理风格也能帮你判断“挂到哪一层更容易被搜到”。把每个层级的商品数统计出来做成分布图规律一目了然。6.3 类目名称的词频分析类目名称本身也是数据。把所有末级类目名称拿出来做分词和词频统计能看出这个站点在哪些品类上划分得特别细。比如“手机壳”相关末级类目有几十个按材质、按型号、按风格分说明这个品类竞争充分、细分需求明确。这种词频分析对判断品类成熟度挺有用操作也简单导出名称列跑一下分词就行。7. 几个能省下大量时间的实操建议最后这部分是我个人在反复折腾中攒下来的经验都是些小点但每一个都能帮你少走弯路。第一先手动跑通一个分支再写全量代码。挑一个子类多的顶层类目手动请求它的接口把返回结构看清楚确认层级字段、分页字段、末级判定逻辑都对再写递归。我见过太多人直接上全量代码跑到一半发现字段理解错了几万条数据白抓。第二把请求日志和结果分开存。每次请求记一条日志节点 ID、请求时间、返回状态、返回条数结果单独存。出问题的时候看日志能快速定位是哪个节点、哪次请求出的问题比翻结果数据高效得多。第三给递归加一个最大深度保护。虽然正常类目树不会太深但万一数据有问题导致无限递归有个深度上限比如 10 层能兜底超过就报警跳过不至于把程序跑死。第四末级类目单独导一份清单。做分析时最常用的就是末级类目单独导一份node_id name path的清单随用随取不用每次从全量树里筛。第五变更比对结果一定要人工过一遍。自动 diff 出来的新增/下线/移动别直接信尤其是“移动”这一类有时候是接口返回顺序变了导致的误判人工扫一眼能避免很多乌龙。这套流程我在几个不同规模的站点上都跑过节点数从几千到几万不等整体是稳的。真正花时间的从来不是写代码而是前期摸接口结构和后期做完整性校验——这两头做扎实了中间那段递归反而是最省心的。如果非要给个优先级我会说先把“怎么判断抓全了”想清楚再动手写抓取这个顺序反过来返工的概率很高。