Python中如何从字符串中删除HTML标签

363
from mechanize import Browser
br = Browser()
br.open('http://somewebpage')
html = br.response().readlines()
for line in html:
  print line
当我在 HTML 文件中打印一行时,我想找到一种只显示每个 HTML 元素的内容而不是格式本身的方法。如果它发现 '<a href="whatever.example">some text</a>',它只会打印 'some text','<b>hello</b>' 打印 'hello' 等等。如何实现这样的功能?

18
处理HTML实体(例如&amp;)是一个重要的考虑因素。你可以选择:1)删除它们和标记(通常不可取,因为它们等同于纯文本),2)保持它们不变(如果被剥离的文本将要回到HTML环境中,则是一种合适的解决方案),或 3)将它们解码为纯文本(如果被剥离的文本将要进入数据库或其他非HTML环境中,或者如果你的网页框架自动对文本进行HTML转义)。 - Søren Løvborg
2
针对@SørenLøvborg的第二点建议,请参考以下链接:https://dev59.com/onRA5IYBdhLWcg3w_DLF - Robert
5
这里的最佳答案曾被 Django 项目使用直至 2014 年 3 月,但已经发现存在跨站脚本攻击漏洞。点击链接可查看一个能够成功攻击的例子。我建议使用 Bleach.clean()、Markupsafe 的 striptags 或 RECENT Django 的 strip_tags。 - rescdsk
28个回答

529
我经常使用这个函数来去除HTML标签,因为它只需要Python标准库:
对于Python 3:
from io import StringIO
from html.parser import HTMLParser

class MLStripper(HTMLParser):
    def __init__(self):
        super().__init__()
        self.reset()
        self.strict = False
        self.convert_charrefs= True
        self.text = StringIO()
    def handle_data(self, d):
        self.text.write(d)
    def get_data(self):
        return self.text.getvalue()

def strip_tags(html):
    s = MLStripper()
    s.feed(html)
    return s.get_data()

对于 Python 2:

from HTMLParser import HTMLParser
from StringIO import StringIO

class MLStripper(HTMLParser):
    def __init__(self):
        self.reset()
        self.text = StringIO()
    def handle_data(self, d):
        self.text.write(d)
    def get_data(self):
        return self.text.getvalue()

def strip_tags(html):
    s = MLStripper()
    s.feed(html)
    return s.get_data()

53
请注意,这个操作会去除HTML实体(例如&amp;)和标签。 - Søren Løvborg
31
@surya 我相信你已经看过这个 - tkone
9
谢谢您提供的很好的答案。对于那些使用较新版本的Python(3.2+)的人,需要调用父类的__init__函数。请参见此处:https://dev59.com/42gu5IYBdhLWcg3w0aGg。 - pseudoramble
12
为了保留HTML实体(转换为Unicode),我在strip_tags函数的开头添加了两行代码:parser = HTMLParser()html = parser.unescape(html)。这样可以使文本更易读,但不会改变原意。 - James Doepp - pihentagyu
4
我的其中一位同事发现了<<sc<script>script>alert(1)<</sc</script>/script>。如果你通过这段代码,输出结果将是<script>alert(1)</script>。为确保结果正确,我使用了html.escape()对你的解决方案进行包装以确保输出中没有留下任何标签。 - AliBZ
显示剩余13条评论

195
如果您需要去除HTML标签以进行文本处理,一个简单的正则表达式就可以做到。但是,如果您想要消毒用户生成的HTML以防止XSS攻击,请不要使用此方法。这不是一种安全的方式来删除所有

11
这几乎就是 Django 的 strip_tags 函数的实现方式。 - Bluu
13
请注意,这将使 HTML 实体(例如 &amp;)在输出中保持不变。 - Søren Løvborg
40
你仍然可以用这样的方法欺骗这个技巧:<script<script>>alert("Hi!")<</script>/script> - user822159
23
不要这样做!正如@Julio Garcia所说,这是不安全的! - rescdsk
24
请勿混淆HTML剥离和HTML消毒。是的,对于损坏或恶意输入,此答案可能会产生带有HTML标记的输出。剥离HTML标记仍然是一种完全有效的方法。但是,剥离HTML标记不是适当的HTML消毒替代品。规则很简单:每当您将纯文本字符串插入HTML输出中时,即使您“知道”它不包含HTML(例如因为您剥离了HTML内容),您也应始终使用HTML转义它(使用cgi.escape(s, True))。然而,这不是OP提出的问题。 - Søren Løvborg
显示剩余6条评论

123

你可以使用BeautifulSoup get_text()特性。

from bs4 import BeautifulSoup

html_str = '''
<td><a href="http://www.fakewebsite.example">Please can you strip me?</a>
<br/><a href="http://www.fakewebsite.example">I am waiting....</a>
</td>
'''
soup = BeautifulSoup(html_str)

print(soup.get_text())
#or via attribute of Soup Object: print(soup.text)

建议明确指定解析器,例如BeautifulSoup(html_str, features="html.parser"),以便输出可重现。


现在必须设置解析器。 - c24b

43

简短版!

import re, html
tag_re = re.compile(r'(<!--.*?-->|<[^>]*>)')

# Remove well-formed tags, fixing mistakes by legitimate users
no_tags = tag_re.sub('', user_input)

# Clean up anything else by escaping
ready_for_web = html.escape(no_tags)

正则表达式来源:MarkupSafe。他们的版本还可以处理 HTML 实体,而这个快速版本不行。

为什么不能只删除标签并保留文本?

防止人们在文本中使用 <i>斜体</i> 这样的标签容易,但是使输入内容完全无害化又是另外一回事了。本页面上的大多数技术将保留未闭合的注释(<!--)和非标签角括号(blah <<<><blah)等。如果它们在未关闭的注释内部,则 HTMLParser 版本甚至可以保留完整的标记。

如果您的模板是{{ firstname }} {{ lastname }}firstname='<a'lastname='href="http://evil.example/">' 将被本页中每个标签剥离器放过(除了@Medeiros!),因为它们本身不是完整的标签。单纯地剥离正常的HTML标签是不够的。

Django 的strip_tags是本问题答案的改进版本(请参见下一标题),其提供以下警告:

绝对不保证生成的字符串是 HTML 安全的。因此,永远不要标记 strip_tags 调用的结果为安全的,例如使用 escape() 进行转义。

遵循他们的建议!

要使用 HTMLParser 剥离标签,您必须多次运行它。

绕过本问题的最佳答案非常简单。

看这个字符串(来源和讨论):

<img<!-- --> src=x onerror=alert(1);//><!-- -->
第一次HTML解析器看到它时,无法识别<img...>为标签。它看起来有问题,因此HTML解析器不会将其删除。它只会取出<!-- 注释 -->,留下以下内容:
<img src=x onerror=alert(1);//>

该问题于2014年三月向Django项目披露。他们旧的strip_tags本质上与此问题的最佳答案相同。他们的新版本基本上是在循环中运行它,直到再次运行不改变字符串为止:

# _strip_once runs HTMLParser once, pulling out just the text of all the nodes.

def strip_tags(value):
    """Returns the given HTML with all tags stripped."""
    # Note: in typical case this loop executes _strip_once once. Loop condition
    # is redundant, but helps to reduce number of executions of _strip_once.
    while '<' in value and '>' in value:
        new_value = _strip_once(value)
        if len(new_value) >= len(value):
            # _strip_once was not able to detect more tags
            break
        value = new_value
    return value
当然,如果您始终转义strip_tags()的结果,则不会出现任何问题。

更新于2015年3月19日: 在1.4.20、1.6.11、1.7.7和1.8c1之前的Django版本中存在一个错误。这些版本可能会在strip_tags()函数中进入无限循环。修复版本如上所示。更多详情请见此处.

好的东西可以复制或使用

我的示例代码不处理HTML实体 - Django和MarkupSafe打包版本可以处理。

我的示例代码摘自优秀的MarkupSafe库,用于防止跨站脚本攻击。它方便快捷(具有其本地Python版本的C加速)。它被包含在Google App Engine中,并被Jinja2(2.7及以上版本)、Mako、Pylons等使用。它在Django 1.7的模板中很容易使用。

Django的strip_tags和其他HTML实用程序来自最新版本非常好,但我发现它们比MarkupSafe不太方便。它们非常自包含,您可以从这个文件中复制您所需的内容。

如果您需要删除几乎所有标记,则Bleach库很好。您可以强制执行规则,例如“我的用户可以使事物成为斜体,但他们不能制作iframes。”

了解您的标记剥离器的属性!对其进行模糊测试!这是我用于为此答案进行研究的代码。

羞怯的说明——问题本身是关于打印到控制台的,但这是“Python从字符串中删除HTML”的顶级Google搜索结果,因此此答案99%涉及网络。


我的“备用最后一行”示例代码无法处理HTML实体 - 这有多糟糕? - rescdsk
我只解析了一小部分没有特殊标签的HTML,你的简短版本做得非常好。谢谢分享! - tbolender
1
回复:ready_for_web = cgi.escape(no_tags) -- cgi.escape已经被弃用,自3.2版本起:“此函数默认情况下不安全,因为引号是错误的,因此已被弃用。请改用html.escape()。”在3.8中已删除。 - JeremyDouglass

36

我需要一种方法来去除标签将HTML实体解码为纯文本。以下解决方案基于Eloff的答案(但我不能使用它,因为它会剥离实体)。

import html.parser

class HTMLTextExtractor(html.parser.HTMLParser):
    def __init__(self):
        super(HTMLTextExtractor, self).__init__()
        self.result = [ ]

    def handle_data(self, d):
        self.result.append(d)

    def get_text(self):
        return ''.join(self.result)

def html_to_text(html):
    """Converts HTML to plain text (stripping tags and converting entities).
    >>> html_to_text('<a href="#">Demo<!--...--> <em>(&not; \u0394&#x03b7;&#956;&#x03CE;)</em></a>')
    'Demo (\xac \u0394\u03b7\u03bc\u03ce)'

    "Plain text" doesn't mean result can safely be used as-is in HTML.
    >>> html_to_text('&lt;script&gt;alert("Hello");&lt;/script&gt;')
    '<script>alert("Hello");</script>'

    Always use html.escape to sanitize text before using in an HTML context!

    HTMLParser will do its best to make sense of invalid HTML.
    >>> html_to_text('x < y &lt z <!--b')
    'x < y < z '

    Named entities are handled as per HTML 5.
    >>> html_to_text('&nosuchentity; &apos; ')
    "&nosuchentity; ' "
    """
    s = HTMLTextExtractor()
    s.feed(html)
    return s.get_text()

一个快速测试:

html = '<a href="#">Demo <em>(&not; \u0394&#x03b7;&#956;&#x03CE;)</em></a>'
print(repr(html_to_text(html)))

结果:

'Demo (¬ Δημώ)'

安全提示:不要将HTML 剥离(将HTML转换为纯文本)与HTML 净化(将纯文本转换为HTML)混淆。本答案将删除HTML并将实体解码为纯文本 - 这并不能使结果在HTML上下文中使用时安全。

例如:&lt;script&gt;alert("Hello");&lt;/script&gt; 将被转换为<script>alert("Hello");</script>,这是100%正确的行为,但如果将结果纯文本直接插入HTML页面中,则显然不足够安全。

规则很简单:每次将纯文本字符串插入HTML输出时,始终对其进行HTML转义(使用html.escape(s)),即使您“知道”它不包含HTML(例如,因为您已经剥离了HTML内容)。

但是,OP询问如何将结果打印到控制台,在这种情况下不需要进行HTML转义。相反,您可能希望剥离ASCII控制字符,因为它们可能会触发不必要的行为(特别是在Unix系统上):

import re
text = html_to_text(untrusted_html_input)
clean_text = re.sub(r'[\0-\x1f\x7f]+', '', text)
# Alternatively, if you want to allow newlines:
# clean_text = re.sub(r'[\0-\x09\x0b-\x1f\x7f]+', '', text)
print(clean_text)

24
有一个简单的方法:

这很简单:

def remove_html_markup(s):
    tag = False
    quote = False
    out = ""

    for c in s:
        if c == '<' and not quote:
            tag = True
        elif c == '>' and not quote:
            tag = False
        elif (c == '"' or c == "'") and tag:
            quote = not quote
        elif not tag:
            out = out + c

    return out

这里解释了这个想法:http://youtu.be/2tu9LTDujbw 你可以在这里看到它的运行情况:http://youtu.be/HPkNPcYed9M?t=35s PS-如果您对我提供的关于Python智能调试课程感兴趣,这是一个链接:http://www.udacity.com/overview/Course/cs259/CourseRev/1。 它是免费的!
欢迎! :)

4
我不知道为什么这个回答被踩了。它是一个简单的解决问题的方式,没有使用任何库,只用纯Python,而且它的有效性已经在链接中得到展示。 - Medeiros
6
人们可能更喜欢使用库来提供安全性。我测试了你的代码并通过了,我总是更喜欢理解的小代码,而不是使用一个库并假设它没问题,直到出现漏洞。对我来说,这就是我在寻找的,再次感谢。至于投票,不要陷入那种心态。人们应该关心质量而不是得分。最近,SO已经成为一个每个人都想得分而非知识的地方。 - Jimmy Kane
2
这个解决方案的问题在于错误处理。例如,如果您将<b class="o'>x</b>作为输入,则函数输出x。但实际上,这个输入是无效的。我认为这就是人们更喜欢使用库的原因。 - laltin
1
它也可以处理那个输入。我刚刚测试过了。只是要意识到,在这些库中,你会发现类似的代码。我知道它不太符合Python的风格。看起来更像C或Java代码。但我认为它很高效,并且可以轻松地移植到另一种语言。 - Medeiros
2
简单、Pythonic,似乎比讨论过的其他方法都要好用。可能无法处理一些格式不正确的HTML,但这是无法克服的。 - denson
如果你关心性能,那么 out = out + c 将是你最可怕的噩梦。相反,你可以使用一个列表,在最后使用 "".join - bfontaine

23

一种基于lxml.html的解决方案(lxml是一个本地库,比纯python解决方案更具性能)。

要安装lxml模块,请使用pip install lxml

删除所有标签

from lxml import html


## from file-like object or URL
tree = html.parse(file_like_object_or_url)

## from string
tree = html.fromstring('safe <script>unsafe</script> safe')

print(tree.text_content().strip())

### OUTPUT: 'safe unsafe safe'

删除所有预处理HTML标签(删除一些标签)

from lxml import html
from lxml.html.clean import clean_html

tree = html.fromstring("""<script>dangerous</script><span class="item-summary">
                            Detailed answers to any questions you might have
                        </span>""")

## text only
print(clean_html(tree).text_content().strip())

### OUTPUT: 'Detailed answers to any questions you might have'

请查看http://lxml.de/lxmlhtml.html#cleaning-up-html,了解lxml.cleaner的具体作用。

如果您需要更多控制权来决定在转换为文本之前应删除哪些特定标记,请使用所需选项创建自定义lxml Cleaner,例如:

cleaner = Cleaner(page_structure=True,
                  meta=True,
                  embedded=True,
                  links=True,
                  style=True,
                  processing_instructions=True,
                  inline_style=True,
                  scripts=True,
                  javascript=True,
                  comments=True,
                  frames=True,
                  forms=True,
                  annoying_tags=True,
                  remove_unknown_tags=True,
                  safe_attrs_only=True,
                  safe_attrs=frozenset(['src','color', 'href', 'title', 'class', 'name', 'id']),
                  remove_tags=('span', 'font', 'div')
                  )
sanitized_html = cleaner.clean_html(unsafe_html)

为了定制生成纯文本的方式,您可以使用lxml.etree.tostring而不是text_content():
from lxml.etree import tostring

print(tostring(tree, method='text', encoding=str))


1
我得到了AttributeError: 'HtmlElement'对象没有'strip'属性。 - aris
@aris:那是针对较旧版本的Python和lxml,已更新。 - ccpizza
有没有选项可以用空字符串(例如“”)替换已删除的标签? - user742736

21

这里有一个简单的解决方案,基于极快速的lxml库,它可以去除HTML标签并解码HTML实体:

from lxml import html

def strip_html(s):
    return str(html.fromstring(s).text_content())

strip_html('Ein <a href="">sch&ouml;ner</a> Text.')  # Output: Ein schöner Text.

4
截至2020年,这是剥离HTML内容的最快、最好的方法。此外还具有处理解码的额外优势。非常适合语言检测! - dfabiano
2
text_content() 返回 lxml.etree._ElementUnicodeResult,因此您可能需要先将其转换为字符串。 - Suzana
1
@Suzana 不错的观点。似乎对于像+和索引[]这样的字符串操作,它会自动转换为str。无论如何,为了保险起见,我还是添加了一个转换。 - Robin Dinse

16

如果您需要保留HTML实体(例如&amp;),我已经在Eloff的答案中添加了“handle_entityref”方法。

from HTMLParser import HTMLParser

class MLStripper(HTMLParser):
    def __init__(self):
        self.reset()
        self.fed = []
    def handle_data(self, d):
        self.fed.append(d)
    def handle_entityref(self, name):
        self.fed.append('&%s;' % name)
    def get_data(self):
        return ''.join(self.fed)

def html_to_text(html):
    s = MLStripper()
    s.feed(html)
    return s.get_data()

14
如果您想去除所有的 HTML 标签,我发现使用 BeautifulSoup 是最简单的方法:
from bs4 import BeautifulSoup  # Or from BeautifulSoup import BeautifulSoup

def stripHtmlTags(htmlTxt):
    if htmlTxt is None:
            return None
        else:
            return ''.join(BeautifulSoup(htmlTxt).findAll(text=True)) 

我尝试了被接受的答案的代码,但是出现了“RuntimeError: maximum recursion depth exceeded”的错误,而使用上面的代码块则没有发生此情况。


1
我刚刚尝试了你的方法,因为它看起来更简洁,它起作用了,但是...它没有去除输入标签! - kustomrtr
我发现使用BeautifulSoup的简单应用在空格方面有问题:''.join(BeautifulSoup('<em>he</em>llo<br>world').find_all(text=True))。这里输出是"helloworld",而你可能想要它变成"hello world"。' '.join(BeautifulSoup('<em>he</em>llo<br>world').find_all(text=True))也不起作用,因为它会变成"he llo world"。 - Finn Årup Nielsen
@kustomrtr,抱歉我的无知,请问我应该将什么放入self参数中? NameError:名称'self'未定义。 - Ian_De_Oliveira
@Ian_De_Oliveira 您可以将其删除,我假设它在类内部,但不需要。我还编辑了答案以将其删除。 - Vasilis
@Ian_De_Oliveira 您可以将其删除,我假设它在一个类中,但不需要它。我还编辑了答案以将其删除。 - Vasilis

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