使用BeautifulSoup解析分层HTML为指定结构JSON的错位重复问题
问题根因
原代码的实现逻辑存在几个核心偏差,直接导致输出错位:
- 跨区块遍历:遇到下一个h2时没有立即终止当前区块的循环,后续所有h2对应的内容都会被并入上一个区块,出现内容重复、串位
- 字段漏存:切换h3节点、到达区块边界时,没有把上一个h3收集完成的p文本写入内容字典,经常丢失最后一个字段的内容
- 过滤逻辑失效:遇到Bar、Baz这类需要跳过的h3时,没有暂停对应p文本的收集流程,导致后续文本拼接混乱
- 冗余文本未过滤:h2之后、第一个h3之前的孤立p文本不属于任何目标字段,原代码没有做拦截
核心实现思路
完全匹配“h2划区块、下一个h2截断、按h3分组收集文本”的规则,步骤如下:
- 定义目标字段白名单,仅保留
Prio、Description、Foo三类h3对应的内容 - 提取文档中首个h1标签的文本,作为
Category字段值 - 遍历所有h2标签,每个h2作为独立内容区块的起点
- 对单个h2区块,遍历其后续兄弟节点,一旦遇到下一个h2立刻停止遍历,完成当前区块的内容收集
- 区块内遍历过程中维护两个状态:当前正在收集的目标字段名、该字段下拼接的p文本列表
- 遇到h3节点时,先把上一个收集到的字段内容写入字典;再判断当前h3是否在白名单内,是则更新当前收集字段,否则清空收集状态
- 遇到p节点时,仅当处于目标字段收集状态时,才把p的文本追加到对应字段的文本列表中
可直接运行的修正代码
import json from bs4 import BeautifulSoup html = """<h1>Heading</h1> <h1>More heading</h1> <p>test</p> <h2>Section</h2> <p>a.b.c</p> <h3>Prio</h3> <p>Medium</p> <h3>Description</h3> <p>Description 1</p> <p>Description 2</p> <h3>Foo</h3> <p>Foo 1</p> <p>Foo 2</p> <h3>Bar</h3> <p>Bar 1</p> <p>Bar 2</p> <p>Bar 3</p> <h3>Baz</h3> <p>Baz 1</p> <h2>Section</h2> <h3>Prio</h3> <p>High</p> <h3>Description</h3> <p>Description 3</p> <p>Description 4</p> <h3>Foo</h3> <p>Foo 3</p> <h2>Section</h2> <h3>Prio</h3> <p>Low</p> <h3>Description</h3> <p>Description 5</p> <p>Description 6</p> <h3>Foo</h3> <p>Foo 4</p> <p>Foo 5</p> <h3>Bar</h3> <p>Bar 4</p> <p>Bar 5</p> <h3>Baz</h3> <p>Baz 2</p> <h2>Section</h2> <h3>Prio</h3> <p>Medium</p> <h3>Description</h3> <p>Description 7</p> <h3>Foo</h3> <p>Foo 6</p> <h3>Bar</h3> <p>Bar 6</p> <h3>Baz</h3> <p>Baz 3</p>""" # 定义需要提取的目标h3字段白名单 TARGET_FIELDS = {"Prio", "Description", "Foo"} result = {} soup = BeautifulSoup(html, 'lxml') # 提取首个h1的文本作为Category result['Category'] = soup.find('h1').get_text(strip=True) data = [] all_h2 = soup.find_all('h2') # 遍历每个h2划分的独立区块 for h2 in all_h2: current_content = {} current_field = None current_text_list = [] # 遍历当前h2的后续兄弟节点,直到遇到下一个h2就截断 for sibling in h2.next_siblings: # 区块边界:遇到下一个h2,先完成当前区块的收尾 if sibling.name == 'h2': if current_field: current_content[current_field] = '\n'.join(current_text_list) current_field = None current_text_list = [] break # 遇到h3节点,切换收集状态 if sibling.name == 'h3': # 先把上一个h3收集的内容存入字典 if current_field: current_content[current_field] = '\n'.join(current_text_list) current_text_list = [] # 判断当前h3是否为目标字段 h3_text = sibling.get_text(strip=True) if h3_text in TARGET_FIELDS: current_field = h3_text else: # 非目标字段(Bar/Baz),清空收集状态 current_field = None # 遇到p节点,仅在目标字段收集状态下拼接文本 elif sibling.name == 'p': if current_field: p_text = sibling.get_text(strip=True) current_text_list.append(p_text) data.append(current_content) result['Data'] = data # 格式化输出结果 print(json.dumps(result, indent=2, ensure_ascii=False))
运行效果
执行上述代码后,输出结构与预期完全匹配:
Category字段为首个h1的文本HeadingData数组共4个元素,每个元素对应单个h2区块,仅包含Prio、Description、Foo三个字段,Bar、Baz的内容全部过滤- 同一h3下的多段p文本自动用
\n拼接
内容的提问来源于stack exchange,提问作者beautifulsouper
相关产品推荐
相关产品推荐

