使用Beautiful Soup模块将标签替换为纯文本

3 投票
2 回答
7849 浏览
提问于 2025-04-15 18:00

我正在使用Beautiful Soup从网页中提取“内容”。我知道之前有些人问过这个问题,他们都被推荐使用Beautiful Soup,这也是我开始使用它的原因。

我成功提取了大部分内容,但在处理一些属于内容的标签时遇到了一些挑战。(我开始时的基本策略是:如果一个节点的字符数超过x,那么它就是内容。)我们来看下面的HTML代码作为例子:

<div id="abc">
    some long text goes <a href="/"> here </a> and hopefully it 
    will get picked up by the parser as content
</div>

results = soup.findAll(text=lambda(x): len(x) > 20)

当我使用上面的代码来获取长文本时,它在标签处断开(识别的文本从'and hopefully..'开始)。所以我尝试用纯文本替换这个标签,如下所示:

anchors = soup.findAll('a')

for a in anchors:
  a.replaceWith('plain text')

上面的做法不奏效,因为Beautiful Soup将字符串插入为NavigableString,这在我使用findAll时会造成同样的问题,条件是len(x) > 20。我可以先用正则表达式解析HTML为纯文本,清除所有不需要的标签,然后再调用Beautiful Soup。但我想避免对同一内容进行两次处理——我试图解析这些页面,以便为给定链接显示一小段内容(很像Facebook分享)——如果一切都用Beautiful Soup完成,我想这会更快。

所以我的问题是:有没有办法用Beautiful Soup“清除标签”并用“纯文本”替换它们?如果没有,最好的做法是什么呢?

谢谢你的建议!

更新:Alex的代码在示例中效果很好。我也尝试了各种边缘情况,它们都运行良好(根据下面的修改)。所以我在一个真实的网站上试了一下,遇到了一些让我困惑的问题。

import urllib
from BeautifulSoup import BeautifulSoup

page = urllib.urlopen('http://www.engadget.com/2010/01/12/kingston-ssdnow-v-dips-to-30gb-size-lower-price/')

anchors = soup.findAll('a')
i = 0
for a in anchors:
    print str(i) + ":" + str(a)
    for a in anchors:
        if (a.string is None): a.string = ''
        if (a.previousSibling is None and a.nextSibling is None):
            a.previousSibling = a.string
        elif (a.previousSibling is None and a.nextSibling is not None):
            a.nextSibling.replaceWith(a.string + a.nextSibling)
        elif (a.previousSibling is not None and a.nextSibling is None):
            a.previousSibling.replaceWith(a.previousSibling + a.string)
        else:
            a.previousSibling.replaceWith(a.previousSibling + a.string + a.nextSibling)
            a.nextSibling.extract()
    i = i+1

当我运行上面的代码时,出现了以下错误:

0:<a href="http://www.switched.com/category/ces-2010">Stay up to date with 
Switched's CES 2010 coverage</a>
Traceback (most recent call last):
  File "parselink.py", line 44, in <module>
  a.previousSibling.replaceWith(a.previousSibling + a.string + a.nextSibling)
 TypeError: unsupported operand type(s) for +: 'Tag' and 'NavigableString'

当我查看HTML代码时,'Stay up to date..'没有任何前一个兄弟节点(在看到Alex的代码之前,我不知道前一个兄弟节点是如何工作的,根据我的测试,它似乎是在寻找标签之前的'text')。所以,如果没有前一个兄弟节点,我很惊讶它没有通过if逻辑,即a.previousSibling是None且a.nextSibling是None。

你能告诉我我哪里做错了吗?

-ecognium

2 个回答

1

当我尝试在文档中“扁平化”标签时,我想把标签里的所有内容都提到它的父节点上。比如,我想减少一个p标签里的内容,包括所有的子段落、列表、divspan等,但想去掉一些stylefont标签,还有一些糟糕的从文字转成HTML的工具留下的东西。我发现用BeautifulSoup来做这件事挺复杂的,因为extract()方法会把内容也删除,而replaceWith()不幸的是不接受None作为参数。在经过一些疯狂的递归实验后,我最终决定在用BeautifulSoup处理文档之前或之后使用正则表达式,方法如下:

import re
def flatten_tags(s, tags):
   pattern = re.compile(r"<(( )*|/?)(%s)(([^<>]*=\\\".*\\\")*|[^<>]*)/?>"%(isinstance(tags, basestring) and tags or "|".join(tags)))
   return pattern.sub("", s)

这里的tags参数可以是一个单独的标签,也可以是一个标签列表,用来进行扁平化处理。

4

一个适合你这个具体例子的做法是:

from BeautifulSoup import BeautifulSoup

ht = '''
<div id="abc">
    some long text goes <a href="/"> here </a> and hopefully it 
    will get picked up by the parser as content
</div>
'''
soup = BeautifulSoup(ht)

anchors = soup.findAll('a')
for a in anchors:
  a.previousSibling.replaceWith(a.previousSibling + a.string)

results = soup.findAll(text=lambda(x): len(x) > 20)

print results

这个方法会产生

$ python bs.py
[u'\n    some long text goes  here ', u' and hopefully it \n    will get picked up by the parser as content\n']

当然,你可能还需要多加注意,比如说,如果没有 a.string,或者 a.previousSiblingNone,你就需要加一些合适的 if 语句来处理这些特殊情况。不过我希望这个大致的思路能对你有帮助。(实际上,你可能还想把 下一个 兄弟节点合并起来,如果它是一个字符串的话——我不太确定这和你用的规则 len(x) > 20 有什么关系,但比如说你有两个9个字符的字符串,中间夹着一个包含5个字符的 <a> 标签,或许你想把它们一起当作一个“23个字符的字符串”呢?我无法判断,因为我不太理解你这个规则的动机)。

我想除了 <a> 标签,你可能还想去掉其他标签,比如 <b><strong>,也许还有 <p> 和/或 <br> 等等?我想这也得看你这个规则背后的实际想法是什么!

撰写回答