漂亮汤 - 获取所有文本,但保留链接HTML?

6

我需要处理一个非常混乱的HTML归档文件,其中包含大量不必要的表格、跨度和内联样式,并将其转换为Markdown。

我尝试使用Beautiful Soup来完成这个任务,我的目标基本上是get_text()函数的输出,除了保留带有完整href的锚标签。

例如,我想要将以下内容转换为Markdown:

<td>
    <font><span>Hello</span><span>World</span></font><br>
    <span>Foo Bar <span>Baz</span></span><br>
    <span>Example Link: <a href="https://google.com" target="_blank" style="mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #395c99;font-weight: normal;text-decoration: underline;">Google</a></span>
</td>

Into:

Hello World
Foo Bar Baz
Example Link: <a href="https://google.com">Google</a>

目前我的思路是获取所有标记,并在它们不是锚点时将它们全部解包,但这会导致文本重复多次,因为soup.find_all(True) 递归地返回嵌套的标记作为单独的元素:

#!/usr/bin/env python

from bs4 import BeautifulSoup

example_html = '<td><font><span>Hello</span><span>World</span></font><br><span>Foo Bar <span>Baz</span></span><br><span>Example Link: <a href="https://google.com" target="_blank" style="mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #395c99;font-weight: normal;text-decoration: underline;">Google</a></span></td>'

soup = BeautifulSoup(example_html, 'lxml')
tags = soup.find_all(True)

for tag in tags:
    if (tag.name == 'a'):
        print("<a href='{}'>{}</a>".format(tag['href'], tag.get_text()))
    else:
        print(tag.get_text())

当解析器向下遍历树时,会返回多个片段/重复的内容:

HelloWorldFoo Bar BazExample Link: Google
HelloWorldFoo Bar BazExample Link: Google
HelloWorldFoo Bar BazExample Link: Google
HelloWorld
Hello
World

Foo Bar Baz
Baz

Example Link: Google
<a href='https://google.com'>Google</a>

你想同时删除样式和其他链接属性吗?因为你的输入和输出都涉及到了这些。 - Gahan
3个回答

6

解决这个问题的一种可能方法是在打印元素文本时,为 a 元素引入特殊处理。

您可以通过覆盖 _all_strings() 方法并返回 a 后代元素的字符串表示形式来完成此操作,并跳过 a 元素内部的可导航字符串。大致如下:

from bs4 import BeautifulSoup, NavigableString, CData, Tag


class MyBeautifulSoup(BeautifulSoup):
    def _all_strings(self, strip=False, types=(NavigableString, CData)):
        for descendant in self.descendants:
            # return "a" string representation if we encounter it
            if isinstance(descendant, Tag) and descendant.name == 'a':
                yield str(descendant)

            # skip an inner text node inside "a"
            if isinstance(descendant, NavigableString) and descendant.parent.name == 'a':
                continue

            # default behavior
            if (
                (types is None and not isinstance(descendant, NavigableString))
                or
                (types is not None and type(descendant) not in types)):
                continue

            if strip:
                descendant = descendant.strip()
                if len(descendant) == 0:
                    continue
            yield descendant

示例:

In [1]: data = """
   ...: <td>
   ...:     <font><span>Hello</span><span>World</span></font><br>
   ...:     <span>Foo Bar <span>Baz</span></span><br>
   ...:     <span>Example Link: <a href="https://google.com" target="_blank" style="mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #395c99;font-weight: normal;tex
   ...: t-decoration: underline;">Google</a></span>
   ...: </td>
   ...: """

In [2]: soup = MyBeautifulSoup(data, "lxml")

In [3]: print(soup.get_text())

HelloWorld
Foo Bar Baz
Example Link: <a href="https://google.com" style="mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #395c99;font-weight: normal;text-decoration: underline;" target="_blank">Google</a>

太棒了,谢谢你,这是一个灵活、优雅的解决方案,我从未想过。我对a标签的处理进行了小调整,输出效果正好符合我的要求,非常完美。 - waffl
如何仅返回href属性?例如链接:https://google.com - Gabriel
回答自己的问题,只需要更改以下内容: if isinstance(descendant, Tag) and descendant.name == 'a': yield str(descendant) 为: if isinstance(descendant, Tag) and descendant.name == 'a': yield str('<{}> '.format(descendant.get('href', ''))) - Gabriel
2
不知道是否有版本更新,我运行示例代码时出现了错误 文件“/Users/xhuang9/pros/station1/t1.py”,第20行_all_strings (types不为None且descendant的类型不在types中): TypeError:'object'类型的参数不可迭代 - alextre
我和@alextre有同样的问题。如果有人能分享一下更新,那就太好了。 - VV75

2

如果只考虑直接子级,而不是所有后代,则需要逐个处理每个“td”并分别提取文本和锚链接。

#!/usr/bin/env python
from bs4 import BeautifulSoup

example_html = '<td><font><span>Some Example Text</span></font><br><span>Another Example Text</span><br><span>Example Link: <a href="https://google.com" target="_blank" style="mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #395c99;font-weight: normal;text-decoration: underline;">Google</a></span></td>'

soup = BeautifulSoup(example_html, 'lxml')
tags = soup.find_all(recursive=False)
for tag in tags:
    print(tag.text)
    print(tag.find('a'))

如果您希望将文本分别打印在不同行上,您需要逐个处理每个“span”元素。
for tag in tags:
    spans = tag.find_all('span')
    for span in spans:
        print(span.text)
print(tag.find('a'))

0
解决方案对我来说不起作用(我遇到了与@alextre相同的问题,可能是由于版本更改)。 然而,我通过进行修改并覆盖get_text()方法而不是all_string()方法来解决了这个问题。
from bs4 import BeautifulSoup, NavigableString, CData, Tag
class MyBeautifulSoup(BeautifulSoup):
    def get_text(self, separator='', strip=False, types=(NavigableString,)):
        text_parts = []

        for element in self.descendants:
            if isinstance(element, NavigableString):
                text_parts.append(str(element))
            elif isinstance(element, Tag):
                if element.name == 'a' and 'href' in element.attrs:
                    text_parts.append(element.get_text(separator=separator, strip=strip))
                    text_parts.append('(' + element['href'] + ')')
                elif isinstance(element, types):
                    text_parts.append(element.get_text(separator=separator, strip=strip))

        return separator.join(text_parts)```

你最后的elif似乎是无法到达的,因为在进入最后的if/elif块之前,你已经检查过它的类型是Tag了。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接