Python 高效提取 HTML 文本的方法

picture.image

在解决自然语言处理问题时,有时你需要获得大量的文本集。互联网是文本的最大来源,但是从任意HTML页面提取文本是一项艰巨而痛苦的任务。

假设我们需要从各种网页中提取全文,并且要剥离所有HTML标记。通常,默认解决方案是使用 BeautifulSoup 软件包中的 get\_text 方法,该方法内部使用 lxml 。这是一个经过充分测试的解决方案,但是在处理成千上万个HTML文档时可能会非常慢。

通过用 selectolax 替换 BeautifulSoup ,您几乎可以免费获得5-30倍的加速!

这是一个简单的基准测试,可分析commoncrawl(`处理NLP问题时,有时您需要获得大量的文本集。互联网是文本的最大来源,但是不幸的是,从任意HTML页面提取文本是一项艰巨而痛苦的任务。

假设我们需要从各种网页中提取全文,并且要剥离所有HTML标记。通常,默认解决方案是使用 BeautifulSoup 软件包中的 get\_text 方法,该方法内部使用 lxml 。这是一个经过充分测试的解决方案,但是在处理成千上万个HTML文档时可能会非常慢。

通过用 selectolax 替换 BeautifulSoup ,您几乎可以免费获得5-30倍的加速!这是一个简单的基准测试,可分析commoncrawl( https://commoncrawl.org/ )的10,000个HTML页面:


          
# coding: utf-8  
  
from time import time  
  
import warc  
from bs4 import BeautifulSoup  
from selectolax.parser import HTMLParser  
  
  
def get\_text\_bs(html):  
    tree = BeautifulSoup(html, 'lxml')  
  
    body = tree.body  
    if body is None:  
        return None  
  
    for tag in body.select('script'):  
        tag.decompose()  
    for tag in body.select('style'):  
        tag.decompose()  
  
    text = body.get_text(separator='\n')  
    return text  
  
  
def get\_text\_selectolax(html):  
    tree = HTMLParser(html)  
  
    if tree.body is None:  
        return None  
  
    for tag in tree.css('script'):  
        tag.decompose()  
    for tag in tree.css('style'):  
        tag.decompose()  
  
    text = tree.body.text(separator='\n')  
    return text  
  
  
def read\_doc(record, parser=get\_text\_selectolax):  
    url = record.url  
    text = None  
  
    if url:  
        payload = record.payload.read()  
        header, html = payload.split(b'\r\n\r\n', maxsplit=1)  
        html = html.strip()  
  
        if len(html) > 0:  
            text = parser(html)  
  
    return url, text  
  
  
def process\_warc(file\_name, parser, limit=10000):  
    warc_file = warc.open(file_name, 'rb')  
    t0 = time()  
    n_documents = 0  
    for i, record in enumerate(warc_file):  
        url, doc = read_doc(record, parser)  
  
        if not doc or not url:  
            continue  
  
        n_documents += 1  
  
        if i > limit:  
            break  
  
    warc_file.close()  
    print('Parser: %s' % parser.__name__)  
    print('Parsing took %s seconds and produced %s documents\n' % (time() - t0, n_documents))  

      

          
>>> ! wget https://commoncrawl.s3.amazonaws.com/crawl-data/CC-MAIN-2018-05/segments/1516084886237.6/warc/CC-MAIN-20180116070444-20180116090444-00000.warc.gz  
>>> file\_name = "CC-MAIN-20180116070444-20180116090444-00000.warc.gz"  
>>> process\_warc(file\_name, get\_text\_selectolax, 10000)  
Parser: get_text_selectolax  
Parsing took 16.170367002487183 seconds and produced 3317 documents  
>>> process\_warc(file\_name, get\_text\_bs, 10000)  
Parser: get_text_bs  
Parsing took 432.6902508735657 seconds and produced 3283 documents  

      

显然,这并不是对某些事物进行基准测试的最佳方法,但是它提供了一个想法,即 selectolax 有时比 lxml 快30倍。

selectolax 最适合将HTML剥离为纯文本。如果我有10,000多个HTML片段,需要将它们作为纯文本索引到Elasticsearch中。(Elasticsearch有一个 html\_strip 文本过滤器,但这不是我想要/不需要在此上下文中使用的过滤器)。事实证明,以这种规模将HTML剥离为纯文本实际上是非常低效的。那么,最有效的方法是什么?

PyQuery


          
from pyquery import PyQuery as pq  
  
text = pq(html).text()  

      

selectolax


          
from selectolax.parser import HTMLParser  
  
text = HTMLParser(html).text()  

      

正则表达式


          
import re  
  
regex = re.compile(r'<.*?>')  
text = clean_regex.sub('', html)  

      

结果

我编写了一个脚本来计算时间,该脚本遍历包含HTML片段的10,000个文件。注意!这些片段不是完整的 <html> 文档(带有 <head><body> 等),只是HTML的一小部分。平均大小为10,314字节(中位数为5138字节)。结果如下:


          
pyquery  
  SUM:    18.61 seconds  
  MEAN:   1.8633 ms  
  MEDIAN: 1.0554 ms  
selectolax  
  SUM:    3.08 seconds  
  MEAN:   0.3149 ms  
  MEDIAN: 0.1621 ms  
regex  
  SUM:    1.64 seconds  
  MEAN:   0.1613 ms  
  MEDIAN: 0.0881 ms  

      

我已经运行了很多次,结果非常稳定。重点是: selectolaxPyQuery 快7倍。

正则表达式好用?真的吗?

对于最基本的 HTML Blob ,它可能工作得很好。实际上,如果HTML是 <p> Foo&amp; Bar </ p> ,我希望纯文本转换应该是 Foo&Bar ,而不是 Foo&amp; bar

更重要的一点是,PyQuery和selectolax支持非常特定但对我的用例很重要的内容。在继续之前,我需要删除某些标签(及其内容)。例如:


          
<h4 class="warning">This should get stripped.</h4>  
<p>Please keep.</p>  
<div style="display: none">This should also get stripped.</div>  

      

正则表达式永远无法做到这一点。

2.0 版本

因此,我的要求可能会发生变化,但基本上,我想删除某些标签。例如: <div class =“ warning”><div class =“ hidden”><div style =“ display:none”> 。因此,让我们实现一下:

PyQuery


          
from pyquery import PyQuery as pq  
  
_display_none_regex = re.compile(r'display:\s*none')  
  
doc = pq(html)  
doc.remove('div.warning, div.hidden')  
for div in doc('div[style]').items():  
    style_value = div.attr('style')  
    if _display_none_regex.search(style_value):  
        div.remove()  
text = doc.text()  

      

selectolax


          
from selectolax.parser import HTMLParser  
  
_display_none_regex = re.compile(r'display:\s*none')  
  
tree = HTMLParser(html)  
for tag in tree.css('div.warning, div.hidden'):  
    tag.decompose()  
for tag in tree.css('div[style]'):  
    style_value = tag.attributes['style']  
    if style_value and _display_none_regex.search(style_value):  
        tag.decompose()  
text = tree.body.text()  

      

这实际上有效。当我现在为10,000个片段运行相同的基准时,新结果如下:


          
pyquery  
  SUM:    21.70 seconds  
  MEAN:   2.1701 ms  
  MEDIAN: 1.3989 ms  
selectolax  
  SUM:    3.59 seconds  
  MEAN:   0.3589 ms  
  MEDIAN: 0.2184 ms  
regex  
  Skip  

      

同样,selectolax击败PyQuery约6倍。

结论

正则表达式速度快,但功能弱。 selectolax 的效率令人印象深刻。

更多阅读

2020 年最佳流行 Python 库 Top 10

2020 Python中文社区热门文章 Top 10

Top 10 沙雕又有趣的 GitHub 程序

特别推荐

picture.image

picture.image

点击下方阅读原文加入 社区会员

0
0
0
0
评论
未登录
暂无评论