在爬虫开发中,最棘手的不是基础的页面请求,而是多层嵌套数据的抓取与处理——比如电商商品的“分类→商品→SKU→规格属性”嵌套JSON、论坛的“主帖→楼层→子评论→回复”多层HTML结构、API返回的“分页数据+嵌套详情”组合。这类数据层级深、结构不固定,新手容易出现“数据遗漏”“解析崩溃”“代码冗余”等问题。
我之前做过电商平台商品全量数据抓取项目,仅商品规格就有“商品→SKU→颜色→尺寸→库存”5层嵌套,初期用原生字典索引解析,代码写了200多行还频繁报错;后来用“路径提取+递归解析”重构,核心代码缩减到50行,数据提取成功率从80%提升至99%。
这篇文章会聚焦JSON嵌套和HTML嵌套两大核心场景,手把手教你用“jsonpath+XPath+递归解析”搞定复杂多层数据,从“抓取→解析→扁平化→存储”全程实战,所有代码可直接运行,还会分享4个高效处理技巧和6个避坑指南。
一、先看核心亮点(没时间看全文可直接拿)
场景全覆盖:搞定JSON多层嵌套(API数据)、HTML多层嵌套(网页结构)、分页+嵌套(列表+详情)三大高频场景;解析效率高:用jsonpath/XPath替代原生索引,1行代码提取深层数据,避免“dict[‘a’][‘b’][‘c’]”的冗余写法;适配动态结构:递归解析适配“层级不固定”数据(如无限级评论回复),无需手动适配每一层;数据可落地:嵌套数据自动扁平化,直接存入Excel/MySQL,避免存储后难以查询;代码极简:核心解析逻辑仅30行,含详细注释,新手也能看懂。
二、技术方案:为什么是“jsonpath+XPath+递归”?
2.1 多层嵌套数据的3大痛点
传统解析方式(原生字典索引、嵌套for循环)面对多层数据时,痛点明显:
代码冗余:提取3层以上数据需写“data[‘level1’][‘level2’][‘level3’]”,层级越深代码越臃肿;容错性差:若某一层数据缺失(如部分商品无SKU),直接抛出KeyError,导致程序崩溃;适配性弱:面对动态层级(如有的评论有3层回复,有的有5层),固定嵌套循环完全失效。
2.2 最优技术组合(针对性解决痛点)
| 工具/方法 | 核心作用 | 选型理由 |
|---|---|---|
| jsonpath | 解析多层JSON数据 | 支持“$.a.b.c”路径语法,1行提取深层数据,支持模糊匹配、容错查询 |
| XPath | 解析多层HTML嵌套结构 | 支持“//div[@class=‘post’]//span[@class=‘reply’]”,穿透多层标签定位数据 |
| 递归解析 | 适配动态层级数据(如无限级评论) | 自动遍历所有层级,无需手动写嵌套循环,适配结构不固定的数据 |
| pandas | 数据扁平化+存储 | 一键将嵌套字典/列表转为二维表,直接存入Excel/MySQL |
| requests | 抓取API/HTML页面 | 简洁高效,支持分页请求、Cookie/代理配置 |
| BeautifulSoup | 辅助HTML解析 | 配合XPath使用,处理HTML标签更灵活 |
核心逻辑:用jsonpath/XPath“穿透”多层结构,直接定位目标数据;用递归解析处理“层级不固定”场景;最后用pandas将嵌套数据扁平化,方便存储和查询。
三、全场景实战:抓取并处理多层嵌套数据
下面通过3个实战案例,覆盖JSON、HTML、分页+嵌套三大场景,从抓取到存储全程落地。
3.1 场景1:JSON多层嵌套(电商商品规格数据)
3.1.1 数据特点
目标:抓取某电商API的商品数据,包含“商品基本信息→SKU列表→规格属性(颜色/尺寸)→库存”4层嵌套,部分商品无SKU(需容错)。
API示例(简化版):
https://api.example.com/product/123
返回数据结构(嵌套4层):
{
"product_id": "123",
"name": "2025新款运动鞋",
"price": 399,
"sku_list": [
{
"sku_id": "123-01",
"spec": {
"color": "黑色",
"size": "42",
"material": "网面"
},
"stock": 156
},
{
"sku_id": "123-02",
"spec": {
"color": "白色",
"size": "43",
"material": "网面"
},
"stock": 89
}
],
"category": {
"level1": "鞋靴",
"level2": "运动鞋",
"level3": "跑步鞋"
}
}
3.1.2 实战步骤:抓取+解析+扁平化
第一步:安装依赖
pip install requests jsonpath-ng pandas # jsonpath-ng支持更灵活的路径查询
第二步:核心代码(1行提取深层数据)
创建:
json_nested_crawler.py
import requests
from jsonpath_ng import parse
import pandas as pd
def crawl_product_sku(product_id):
"""抓取商品多层嵌套SKU数据"""
url = f"https://api.example.com/product/{product_id}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0.0.0 Safari/537.36"
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
return data
except Exception as e:
print(f"抓取商品{product_id}失败:{str(e)}")
return None
def parse_nested_json(data):
"""用jsonpath解析多层嵌套JSON,自动扁平化"""
if not data:
return []
# 1. 提取商品基本信息(1层数据)
product_id = parse("$.product_id").find(data)[0].value if parse("$.product_id").find(data) else ""
product_name = parse("$.name").find(data)[0].value if parse("$.name").find(data) else ""
category = parse("$.category.level1").find(data)[0].value if parse("$.category.level1").find(data) else ""
# 2. 提取SKU列表(3-4层数据,用jsonpath直接穿透)
sku_expr = parse("$.sku_list[*]") # 匹配所有SKU
sku_list = sku_expr.find(data)
result = []
for sku in sku_list:
sku_data = sku.value
# 提取SKU深层数据(无需嵌套索引,直接用路径)
sku_id = parse("$.sku_id").find(sku_data)[0].value if parse("$.sku_id").find(sku_data) else ""
color = parse("$.spec.color").find(sku_data)[0].value if parse("$.spec.color").find(sku_data) else "未知"
size = parse("$.spec.size").find(sku_data)[0].value if parse("$.spec.size").find(sku_data) else "未知"
stock = parse("$.stock").find(sku_data)[0].value if parse("$.stock").find(sku_data) else 0
# 3. 数据扁平化:将多层数据合并为一维字典
result.append({
"商品ID": product_id,
"商品名称": product_name,
"分类": category,
"SKU ID": sku_id,
"颜色": color,
"尺寸": size,
"库存": stock
})
# 处理无SKU的商品(容错)
if not result:
result.append({
"商品ID": product_id,
"商品名称": product_name,
"分类": category,
"SKU ID": "无",
"颜色": "无",
"尺寸": "无",
"库存": 0
})
return result
if __name__ == "__main__":
# 批量抓取3个商品(可扩展为多商品ID列表)
product_ids = ["123", "456", "789"]
all_data = []
for pid in product_ids:
product_data = crawl_product_sku(pid)
parsed_data = parse_nested_json(product_data)
all_data.extend(parsed_data)
# 4. 存储到Excel(扁平化后的数据直接可用)
df = pd.DataFrame(all_data)
df.to_excel("商品SKU嵌套数据.xlsx", index=False, encoding="utf-8-sig")
print(f"抓取完成!共{len(all_data)}条SKU数据,已保存到Excel")
关键解析技巧:jsonpath路径语法
| 需求 | jsonpath路径 | 效果 |
|---|---|---|
| 提取商品名称 | |
直接获取1层数据 |
| 提取分类(2层嵌套) | |
穿透2层,无需 |
| 提取所有SKU | |
匹配数组中所有元素(忽略索引) |
| 提取所有SKU的颜色 | |
穿透4层,1行提取所有SKU的颜色 |
| 容错查询(无颜色时返回默认值) | |
无数据时不报错,返回None |
3.1.3 运行结果
Excel中数据自动扁平化,每行对应1个SKU,多层嵌套数据转为直观的二维表,可直接用于数据分析或入库:
| 商品ID | 商品名称 | 分类 | SKU ID | 颜色 | 尺寸 | 库存 |
|---|---|---|---|---|---|---|
| 123 | 2025新款运动鞋 | 鞋靴 | 123-01 | 黑色 | 42 | 156 |
| 123 | 2025新款运动鞋 | 鞋靴 | 123-02 | 白色 | 43 | 89 |
| 456 | 休闲T恤 | 服饰 | 无 | 无 | 无 | 0 |
3.2 场景2:HTML多层嵌套(论坛无限级评论回复)
3.2.1 数据特点
目标:抓取某论坛帖子的“主评论→子评论→子子评论”多层回复数据,评论层级不固定(部分有3层,部分有5层),需完整提取所有层级的评论内容、作者、时间。
页面结构(HTML嵌套示例):
<div class="post">
<div class="main-comment">
<span class="author">用户A</span>
<span class="time">2025-10-30 10:00</span>
<div class="content">主评论内容</div>
<!-- 子评论(1层嵌套) -->
<div class="sub-comment">
<span class="author">用户B</span>
<span class="time">2025-10-30 10:10</span>
<div class="content">回复用户A的评论</div>
<!-- 子子评论(2层嵌套) -->
<div class="sub-comment">
<span class="author">用户C</span>
<span class="time">2025-10-30 10:20</span>
<div class="content">回复用户B的评论</div>
</div>
</div>
</div>
<div class="main-comment">
<span class="author">用户D</span>
<span class="time">2025-10-30 11:00</span>
<div class="content">另一条主评论</div>
</div>
</div>
3.2.2 实战步骤:递归解析无限级嵌套
第一步:安装依赖
pip install requests beautifulsoup4 lxml pandas # lxml支持XPath解析
第二步:核心代码(递归遍历所有层级)
创建:
html_nested_crawler.py
import requests
from bs4 import BeautifulSoup
import pandas as pd
def crawl_forum_post(post_url):
"""抓取论坛帖子HTML页面"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0.0.0 Safari/537.36"
}
try:
response = requests.get(post_url, headers=headers, timeout=10)
response.raise_for_status()
response.encoding = response.apparent_encoding # 自动识别编码
return response.text
except Exception as e:
print(f"抓取帖子失败:{str(e)}")
return None
def parse_comment(comment_tag, parent_author="", parent_level=0):
"""递归解析评论(适配无限级嵌套)
:param comment_tag: 评论对应的HTML标签
:param parent_author: 父评论作者(用于关联层级)
:param parent_level: 父评论层级(0=主评论,1=子评论,2=子子评论...)
:return: 解析后的评论列表
"""
comments = []
# 提取当前评论的核心数据(用XPath穿透多层标签)
author = comment_tag.select_one(".author").get_text(strip=True) if comment_tag.select_one(".author") else "匿名用户"
time_str = comment_tag.select_one(".time").get_text(strip=True) if comment_tag.select_one(".time") else "未知时间"
content = comment_tag.select_one(".content").get_text(strip=True) if comment_tag.select_one(".content") else "无内容"
current_level = parent_level + 1 # 当前评论层级
# 存储当前评论数据(关联父评论信息)
comments.append({
"评论层级": current_level,
"作者": author,
"父评论作者": parent_author,
"发布时间": time_str,
"评论内容": content
})
# 递归解析子评论(如果有)
sub_comments = comment_tag.select(".sub-comment")
for sub_comment in sub_comments:
# 递归调用,传入当前作者和层级
sub_comments_data = parse_comment(sub_comment, parent_author=author, parent_level=current_level)
comments.extend(sub_comments_data)
return comments
def parse_nested_html(html):
"""解析HTML多层嵌套评论"""
if not html:
return []
soup = BeautifulSoup(html, "lxml")
# 先定位所有主评论(顶层评论)
main_comments = soup.select(".main-comment")
all_comments = []
for main_comment in main_comments:
# 主评论的父作者为空,层级为0
comment_data = parse_comment(main_comment, parent_author="", parent_level=0)
all_comments.extend(comment_data)
return all_comments
if __name__ == "__main__":
# 目标论坛帖子URL(替换为实际可访问的论坛帖子)
post_url = "https://bbs.example.com/post/12345"
html_content = crawl_forum_post(post_url)
comments_data = parse_nested_html(html_content)
# 存储到Excel
df = pd.DataFrame(comments_data)
df.to_excel("论坛多层评论数据.xlsx", index=False, encoding="utf-8-sig")
print(f"抓取完成!共{len(comments_data)}条评论(含所有层级回复),已保存到Excel")
关键解析技巧:递归适配动态层级
递归函数接收3个参数:当前评论标签、父评论作者、父评论层级;解析当前评论后,自动查找子评论标签,递归调用自身,直到没有子评论为止;用
parse_comment标记评论层级,用
current_level关联父评论,解决“多层回复不知谁回复谁”的问题。
parent_author
3.2.3 运行结果
Excel中完整保留所有层级评论的关联关系,即使是5层嵌套的回复也能正确提取:
| 评论层级 | 作者 | 父评论作者 | 发布时间 | 评论内容 |
|---|---|---|---|---|
| 1 | 用户A | 2025-10-30 10:00 | 主评论内容 | |
| 2 | 用户B | 用户A | 2025-10-30 10:10 | 回复用户A的评论 |
| 3 | 用户C | 用户B | 2025-10-30 10:20 | 回复用户B的评论 |
| 1 | 用户D | 2025-10-30 11:00 | 另一条主评论 |
3.3 场景3:分页+嵌套(知乎问题列表+回答详情)
3.3.1 数据特点
目标:先分页抓取知乎问题列表(1层),再对每个问题抓取嵌套的回答数据(问题→回答→作者→点赞数),属于“外层分页+内层嵌套”的组合场景。
3.3.2 核心代码(分页请求+嵌套解析)
创建:
pagination_nested_crawler.py
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
def crawl_zhihu_questions(keyword, page_count=2):
"""分页抓取知乎问题列表(外层分页)"""
questions = []
for page in range(page_count):
# 知乎搜索分页URL(offset=page*20,每页20个问题)
url = f"https://www.zhihu.com/search?q={keyword}&type=question&offset={page*20}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0.0.0 Safari/537.36",
"Cookie": "你的Cookie" # 替换为自己的知乎Cookie(步骤见之前文章)
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
# 提取当前页所有问题(1层数据)
question_tags = soup.select(".List-item")
for tag in question_tags:
question_title = tag.select_one(".ContentItem-title").get_text(strip=True) if tag.select_one(".ContentItem-title") else ""
question_url = tag.select_one(".ContentItem-title a")["href"] if tag.select_one(".ContentItem-title a") else ""
# 补全URL(知乎相对路径转绝对路径)
question_url = f"https://www.zhihu.com{question_url}" if question_url.startswith("/") else question_url
if question_title and question_url:
questions.append({
"问题标题": question_title,
"问题URL": question_url
})
print(f"第{page+1}页问题抓取完成,共{len(question_tags)}个问题")
time.sleep(2) # 分页延迟,避免反爬
except Exception as e:
print(f"第{page+1}页问题抓取失败:{str(e)}")
time.sleep(5)
return questions
def crawl_question_answers(question_url):
"""抓取单个问题的嵌套回答(内层嵌套)"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0.0.0 Safari/537.36",
"Cookie": "你的Cookie"
}
answers = []
try:
response = requests.get(question_url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
# 提取问题标题(用于关联回答)
question_title = soup.select_one("h1").get_text(strip=True) if soup.select_one("h1") else ""
# 提取所有回答(内层嵌套数据)
answer_tags = soup.select(".List-item")
for answer in answer_tags:
author = answer.select_one(".AuthorInfo-name span").get_text(strip=True) if answer.select_one(".AuthorInfo-name span") else "匿名用户"
vote_count = answer.select_one(".VoteButton--up").get_text(strip=True) if answer.select_one(".VoteButton--up") else "0"
content_tags = answer.select(".RichContent-inner p")
content = "
".join([tag.get_text(strip=True) for tag in content_tags]) if content_tags else "无内容"
answers.append({
"问题标题": question_title,
"问题URL": question_url,
"回答作者": author,
"点赞数": vote_count,
"回答内容": content
})
except Exception as e:
print(f"抓取问题{question_url}的回答失败:{str(e)}")
return answers
if __name__ == "__main__":
# 搜索关键词(如“AI行业发展”)
keyword = "AI行业发展"
# 1. 分页抓取问题列表
questions = crawl_zhihu_questions(keyword, page_count=2)
# 2. 抓取每个问题的嵌套回答
all_answers = []
for q in questions:
print(f"正在抓取问题:{q['问题标题']}")
answers = crawl_question_answers(q["问题URL"])
all_answers.extend(answers)
time.sleep(3) # 单个问题延迟,避免反爬
# 3. 存储到Excel
df = pd.DataFrame(all_answers)
df.to_excel("知乎分页嵌套回答数据.xlsx", index=False, encoding="utf-8-sig")
print(f"抓取完成!共{len(all_answers)}条回答,涉及{len(questions)}个问题")
3.3.3 核心逻辑
外层分页:通过参数控制分页,循环抓取多页问题列表;内层嵌套:对每个问题URL,单独请求并提取嵌套的回答数据;数据关联:用“问题标题”“问题URL”关联问题和回答,避免数据混乱;反爬处理:添加分页延迟、Cookie配置,避免被知乎封禁IP。
offset
四、处理多层嵌套数据的4个核心技巧
4.1 用路径语法替代原生索引(效率翻倍)
JSON数据:用jsonpath的替代
$.a.b.c,1行代码提取深层数据,且支持容错查询;HTML数据:用XPath的
data['a']['b']['c']穿透多层标签,无需逐层查找父标签。
//div[@class='a']//span[@class='b']
4.2 递归解析适配动态层级(万能方案)
面对“层级不固定”的数据(如无限级评论),用递归函数自动遍历所有层级:
核心逻辑:解析当前层级→查找子层级→递归调用自身→合并结果;关键参数:保留“父层级标识”(如父评论作者、父分类名称),便于数据关联。
4.3 数据扁平化(存储必备)
嵌套数据直接存储会导致查询困难,需转为二维表:
方法1:手动拼接字段(如+
商品ID+
SKU ID);方法2:用
颜色自动扁平化JSON数据(适合复杂嵌套):
pandas.json_normalize()
# 自动扁平化JSON嵌套数据
df = pd.json_normalize(data, record_path="sku_list", meta=["product_id", "name", ["category", "level1"]])
4.4 容错处理(避免程序崩溃)
JSON数据:用的
jsonpath-ng语法或
?捕获KeyError;HTML数据:用
try-except判断标签是否存在,避免AttributeError;示例:
if tag.select_one(selector) else 默认值
# JSON容错查询(无stock时返回0)
stock = parse("$.stock?").find(sku_data)[0].value if parse("$.stock?").find(sku_data) else 0
# HTML容错查询(无作者时返回“匿名用户”)
author = tag.select_one(".author").get_text() if tag.select_one(".author") else "匿名用户"
五、避坑指南(6个实测踩过的坑)
坑1:嵌套层级缺失导致程序崩溃
症状:部分数据缺少某一层(如无SKU、无评论),代码抛出KeyError/AttributeError;解决:用路径语法的容错查询+判断,给缺失数据设置默认值。
if-else
坑2:递归深度过大导致栈溢出
症状:解析超深层数据(如100层评论回复)时,程序报错“maximum recursion depth exceeded”;解决:① 用迭代替代递归(循环遍历所有层级);② 限制最大递归深度()。
sys.setrecursionlimit(1000)
坑3:数据关联丢失(分页+嵌套场景)
症状:回答与问题、子评论与主评论无法关联;解决:保留“父层级标识”(如问题URL、父评论作者),作为关联字段存入数据。
坑4:JSONPath/XPath选择器失效(页面/API更新)
症状:选择器匹配不到数据,返回空值;解决:重新抓包/查看HTML源码,用大模型快速生成新的选择器(参考之前AI辅助爬虫文章)。
坑5:分页抓取时重复数据
症状:多页数据出现重复(如同一问题被多次抓取);解决:用“唯一标识”(如商品ID、问题URL)去重:
# 用set去重(基于问题URL)
unique_questions = list({q["问题URL"]: q for q in questions}.values())
坑6:数据量过大导致内存溢出
症状:抓取10万+条嵌套数据时,内存占用过高;解决:① 分批次存储(每1000条数据写入一次Excel/MySQL);② 用生成器替代列表,逐个处理数据。
六、总结与进阶方向
处理多层嵌套数据的核心逻辑是:用路径语法穿透层级→用递归适配动态结构→用扁平化便于存储→用容错保证稳定。无论是JSON API还是HTML网页,这套方法都能高效应对。
进阶扩展方向
异步爬取:用替代
aiohttp,同时抓取多个嵌套数据(如同时请求10个问题的回答),速度提升5-10倍;分布式处理:结合之前的“Scrapy+RabbitMQ”架构,处理百万级嵌套数据(如全量电商商品SKU);数据入库:将扁平化后的数据存入MySQL(分表存储父子数据)或MongoDB(保留原始嵌套结构,适合非结构化数据);复杂嵌套自动解析:用大模型生成jsonpath/XPath选择器,甚至自动识别嵌套结构并解析(参考AI辅助爬虫文章)。
requests




















暂无评论内容