Python爬虫实战:复杂多层嵌套数据抓取与处理(JSON/HTML双场景)

在爬虫开发中,最棘手的不是基础的页面请求,而是多层嵌套数据的抓取与处理——比如电商商品的“分类→商品→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路径 效果
提取商品名称
$.name
直接获取1层数据
提取分类(2层嵌套)
$.category.level1
穿透2层,无需
data['category']['level1']
提取所有SKU
$.sku_list[*]
匹配数组中所有元素(忽略索引)
提取所有SKU的颜色
$.sku_list[*].spec.color
穿透4层,1行提取所有SKU的颜色
容错查询(无颜色时返回默认值)
$.sku_list[*].spec.color?
无数据时不报错,返回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")
关键解析技巧:递归适配动态层级

递归函数
parse_comment
接收3个参数:当前评论标签、父评论作者、父评论层级;解析当前评论后,自动查找子评论标签,递归调用自身,直到没有子评论为止;用
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 核心逻辑

外层分页:通过
offset
参数控制分页,循环抓取多页问题列表;内层嵌套:对每个问题URL,单独请求并提取嵌套的回答数据;数据关联:用“问题标题”“问题URL”关联问题和回答,避免数据混乱;反爬处理:添加分页延迟、Cookie配置,避免被知乎封禁IP。

四、处理多层嵌套数据的4个核心技巧

4.1 用路径语法替代原生索引(效率翻倍)

JSON数据:用jsonpath的
$.a.b.c
替代
data['a']['b']['c']
,1行代码提取深层数据,且支持容错查询;HTML数据:用XPath的
//div[@class='a']//span[@class='b']
穿透多层标签,无需逐层查找父标签。

4.2 递归解析适配动态层级(万能方案)

面对“层级不固定”的数据(如无限级评论),用递归函数自动遍历所有层级:

核心逻辑:解析当前层级→查找子层级→递归调用自身→合并结果;关键参数:保留“父层级标识”(如父评论作者、父分类名称),便于数据关联。

4.3 数据扁平化(存储必备)

嵌套数据直接存储会导致查询困难,需转为二维表:

方法1:手动拼接字段(如
商品ID
+
SKU ID
+
颜色
);方法2:用
pandas.json_normalize()
自动扁平化JSON数据(适合复杂嵌套):


# 自动扁平化JSON嵌套数据
df = pd.json_normalize(data, record_path="sku_list", meta=["product_id", "name", ["category", "level1"]])

4.4 容错处理(避免程序崩溃)

JSON数据:用
jsonpath-ng

?
语法或
try-except
捕获KeyError;HTML数据:用
if tag.select_one(selector) else 默认值
判断标签是否存在,避免AttributeError;示例:


# 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
替代
requests
,同时抓取多个嵌套数据(如同时请求10个问题的回答),速度提升5-10倍;分布式处理:结合之前的“Scrapy+RabbitMQ”架构,处理百万级嵌套数据(如全量电商商品SKU);数据入库:将扁平化后的数据存入MySQL(分表存储父子数据)或MongoDB(保留原始嵌套结构,适合非结构化数据);复杂嵌套自动解析:用大模型生成jsonpath/XPath选择器,甚至自动识别嵌套结构并解析(参考AI辅助爬虫文章)。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容