使用 lxml 提取两个 HTML 标题之间的所有文本
Posted
技术标签:
【中文标题】使用 lxml 提取两个 HTML 标题之间的所有文本【英文标题】:Extracting all text between two HTML headings with lxml 【发布时间】:2019-09-10 21:43:48 【问题描述】:我正在尝试在 Python 中使用 lxml 解析 html 页面。
在 HTML 中有这样的结构:
<html>
<h5>Title</h5>
<p>Some text <b>with</b> <i>other tags</i>.</p>
<p>More text.</p>
<p>More text[2].</p>
<h5>Title[2]</h5>
<p>Description.</p>
<h5>Title[3]</h5>
<p>Description[1].</p>
<p>Description[2].</p>
***
and so on...
***
</html>
我需要将此 HTML 解析为以下 JSON:
[
"title": "Title",
"text": "Some text with other tags.\nMore text.\nMore text[2].",
,
"title": "Title[2]",
"text": "Description.",
,
"title": "Title[3]",
"text": "Description[1].\nDescription[2]",
]
我可以读取所有带有标题的 h5 标签并使用以下代码将它们写入 JSON:
array = []
for title in tree.xpath('//h5/text()'):
data =
"title" : title,
"text" : ""
array.append(data)
with io.open('data.json', 'w', encoding='utf8') as outfile:
str_ = json.dumps(array,
indent=4, sort_keys=True,
separators=(',', ' : '), ensure_ascii=False)
outfile.write(to_unicode(str_))
问题是,我不知道如何阅读<h5>
标题之间的所有这些段落内容并将它们放入text
JSON字段中。
【问题讨论】:
我唯一能想到的就是逐个标签解析所有内容并构建它的 JSON... 【参考方案1】:要获取两个元素“之间”的所有文本,例如两个标题之间,没有其他方法:
遍历整个tree
(我们将使用.iterwalk()
,因为我们必须区分元素的开始和结束)
为遇到的每个标题创建一个数据项(我们称之为current_heading
)
将来自的任何其他元素的所有单个文本位收集到一个列表中
每次遇到新航向时,存储迄今为止收集的数据并开始新的数据项
ElementTree 元素中的每个元素都可以有一个.text
和一个.tail
:
<b>This will be the .text</b> and this will be the .tail
我们必须同时收集两者,否则输出中会丢失文本。
以下使用堆栈跟踪我们在 HTML 树中的位置,因此嵌套元素的 .head
和 .tail
以正确的顺序收集。
collected_text = []
data = []
stack = []
current_heading =
'title': '',
'text': []
html_headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
def normalize(strings):
return ''.join(strings)
for event, elem in ET.iterwalk(tree, events=('start', 'end')):
# when an element starts, collect its .text
if event == 'start':
stack.append(elem)
if elem.tag in html_headings:
# reset any collected text, b/c now we're starting to collect
# the heading's text. There might be nested elements in it.
collected_text = []
if elem.text:
collected_text.append(elem.text)
# ...and when it ends, collect its .tail
elif event == 'end' and elem == stack[-1]:
# headings mark the border between data items
if elem.tag in html_headings:
# normalize text in the previous data item
current_heading['text'] = normalize(current_heading['text'])
# start new data item
current_heading =
'title': normalize(collected_text),
'text': []
data.append(current_heading)
# reset any collected text, b/c now we're starting to collect
# the text after the the heading
collected_text = []
if elem.tail:
collected_text.append(elem.tail)
current_heading['text'] = collected_text
stack.pop()
# normalize text in final data item
current_heading['text'] = normalize(current_heading['text'])
当我对您的示例 HTML 运行它时,我得到了这个输出(JSON 格式):
[
"text" : "\n Some text with other tags.\n More text.\n More text[2].\n\n ",
"title" : "Title"
,
"text" : "\n Description.\n\n ",
"title" : "Title[2]"
,
"text" : "\n Description[1].\n Description[2].\n\n ***\n and so on...\n ***\n",
"title" : "Title[3]"
]
我的normalize()
函数非常简单,它保留了作为 HTML 源代码一部分的所有换行符和其他空格。如果您想要更好的结果,请编写更复杂的函数。
【讨论】:
【参考方案2】:有一种更简单的方法可以做到这一点,只需跟踪下一个 h5 的位置,并确保选择位置较低的 p:
data = []
for h5 in doc.xpath('//h5'):
more_h5s = h5.xpath('./following-sibling::h5')
position = int(more_h5s[0].xpath('count(preceding-sibling::*)')) if len(more_h5s) > 0 else 999
ps = h5.xpath('./following-sibling::p[position()<' + str(position) + ']')
data.append(
"title": h5.text,
"text": "\n".join(map(lambda p: p.text_content(), ps))
)
“关注”following-sibling::*
直到它不再是 p
可能更简单
【讨论】:
这仅适用于<p>
s,并且仅当所有文本确实在 <p>
中时。实际文档的结构可能不够优化。
您可以将该 p 更改为您想要的任何内容。通常我会在一个while循环中跟随“next()”,但这些在python中很笨拙
是的,但这仍然不会涵盖<p>
(或其他)之外的文本。另外,preceding-sibling
不会停在前一个<h5>
,所以在最后一个<h5>
,count(preceding-sibling::*)
将是<h5>
之前的所有 元素,所以我什至没有确定这整个position
应该如何工作? (除此之外:'AttributeError: lxml.etree._Element' object has no attribute 'text_content'
)
p 之外的文本或可以使用 text() 选择的任何内容。 count(preceding-sibling::*)
将给出可以在 position() xpath 表达式中使用的元素的位置。我正在使用来自 lxml 的 html。【参考方案3】:
首先,根据传递的标签将元素的子元素拆分为单独的部分。
def split(element, tag):
sections = [[]]
for element in element:
if element.tag == tag:
sections.append([])
sections[-1].append(element)
return sections
从那里,它可以重新塑造成字典。应该执行以下操作:
data = []
for section in split(html, "h5"):
if section and section[0].tag == "h5":
data.append(
"title": section[0].text_content(),
"text": "\n".join(q.text_content() for q in section[1:]),
)
【讨论】:
以上是关于使用 lxml 提取两个 HTML 标题之间的所有文本的主要内容,如果未能解决你的问题,请参考以下文章