Какой самый быстрый и безошибочный метод извлечения и очистки основного текста HTML в Python?

В настоящее время у меня есть две функции для извлечения текста HTML <body> из Python и возврата его в виде набора слов. Они дают эквивалентный результат. Я также очищаю различные теги, которые в противном случае давали бы мне мусорный текст (например, код <script>).

def html_to_bow_bs(text):
    if text is None or len(text)==0:
        return []

    soup = BeautifulSoup(text, "lxml",parse_only=SoupStrainer('body'))

    # Remove all irrelevant tags
    for elem in soup.findAll(['script','style','a']):
        elem.extract()
    body_text = soup.findAll("body")
    if len(body_text) == 0:
        return []

    # Encoding.  Remove extra whitespace and unprintable characters
    the_text = body_text[0].get_text().encode('utf-8')
    the_text = str(the_text)
    the_text = the_text.strip()
    the_text = re.sub(r'[^\x00-\x7F]+',' ',the_text)
    return [w.lower() for w in the_text.split()]




def html_to_bow_bs_lxml(text):
    if text is None or len(text)==0:
        return []
    body_re = re.findall('<body(.*?)</body>', text, flags=re.DOTALL)
    if len(body_re) == 0:
        return []
    fragment = body_re[0]

    # Remove irrelevant tags
    fragment = re.sub(r'<script.*?</script>', ' ', fragment, flags=re.DOTALL)
    fragment = re.sub(r'<style.*?</style>', ' ', fragment, flags=re.DOTALL)
    text = "<body" + fragment + "</body>"
    soup = BeautifulSoup(text, "lxml")

    if soup is None:
        return []

    # Remote more irrelevant tags
    for elem in soup.findAll(['a']):
        elem.extract()

    # Encoding.  Remove extra whitespace and unprintable characters
    the_text = body_text[0].get_text().encode('utf-8')
    the_text = str(the_text)
    the_text = the_text.strip()
    the_text = re.sub(r'[^\x00-\x7F]+',' ',the_text)
    return [w.lower() for w in the_text.split()]

Моим основным требованием является соответствие вывода: чтобы набор слов из html_to_bow_bs_lxml(text) соответствовал html_to_bow_bs(text). В настоящее время оба находятся на одном уровне по времени работы; для 330 страниц они выполняются около 20 секунд (медленно!). Если я удалю и заменю последние soup.findAll(['a'])...extract() во второй функции регулярными выражениями, я смогу сократить свое время на 6 секунд. Полная замена BeautifulSoup на lxml.etree может сократить дополнительные 10 секунд, в результате чего общее время выполнения составит около 3-4 секунд. Однако при замене регулярными выражениями

  1. вывод не всегда совпадает. При замене BeautifulSoup либо вывод не совпадает, либо
  2. моя программа падает во время обработки из-за плохо сформированного HTML. Как увеличить скорость при сохранении правильности?

Я видел различные рекомендации по извлечению HTML с помощью Python, как правило, на StackOverflow, но они датируются несколькими годами ранее (например, 2012). Понятно, что с тех пор в библиотеках было много обновлений.

(Я также пробовал pyquery, но он не всегда правильно извлекает тело.)


person Matt    schedule 03.03.2016    source источник


Ответы (2)


Вы много сделали для того, чтобы сделать его быстрым: фильтр для супа и анализатор lxml обычно являются первыми вещами, которые нужно попробовать при оптимизации анализа с помощью BeautifulSoup.

Вот некоторые улучшения для этого конкретного кода.

Убираем проверку существования тела:

body_text = soup.findAll("body")
if len(body_text) == 0:
    return []

и используйте вместо этого find().

Замените if text is None or len(text)==0: только на if not text:.

Разделите через get_text(strip=True).


Улучшенный код:

def html_to_bow_bs(text):
    if not text:
        return []

    soup = BeautifulSoup(text, "lxml", parse_only=SoupStrainer('body'))

    # Remove all irrelevant tags
    for elem in soup.find_all(['script','style','a']):
        elem.extract()

    body = soup.find("body")
    if not body:
        return []

    the_text = body.get_text(strip=True).encode('utf-8')
    the_text = re.sub(r'[^\x00-\x7F]+', ' ', the_text)
    return [w.lower() for w in the_text.split()]

Это всего лишь микроулучшения, и я не думаю, что они изменят общую картину производительности. На что я бы еще обратил внимание:

  • запуск скрипта через pypy (beautifulsoup4 совместим, но вы не сможете использовать lxml парсер - попробуйте с html.parser или html5lib). Вы можете выиграть много, даже не изменяя код вообще.
person alecxe    schedule 03.03.2016
comment
Спасибо! Похоже, были некоторые постепенные улучшения. Однако body.get_text(strip=True) дал гораздо меньше слов, чем разделение get_text() и strip() на две разные функции. Я заменил это на ' '.join([text for text in body.stripped_strings]).encode('utf-8'), что дает больше слов, которые на самом деле были пропущены в моих подходах. Однако это редактирование, наряду с указанным выше, дает почти одинаковую производительность во время выполнения. - person Matt; 03.03.2016
comment
@ Мэтт, да, спасибо, я не ожидал особого прироста производительности от этих изменений. Что с пипи? - person alecxe; 03.03.2016
comment
Сейчас пробую сейчас. Извините, не хватило места для комментариев. :) - person Matt; 03.03.2016
comment
Ничего себе, pypy дал мне очень впечатляющий прирост производительности, вдвое. К сожалению, он не совместим с некоторыми другими моими библиотеками (pandas для анализа данных), но я отмечу это как решение. Спасибо! - person Matt; 03.03.2016

Использование модуля запросов и bs4

Это самый простой способ напечатать основной текст.

import requests
from bs4 import BeautifulSoup
url = "yourUrl"
r = requests.get(url)
soup = BeautifulSoup(r.content, 'lxml')
items = soup.find_all('body')
for item in items:
    print item.text

Примечание. Если вы печатаете все тело, оно также будет печатать функции jquery и javascript, если они там есть.

person estebanpdl    schedule 03.03.2016
comment
К сожалению, поскольку в вывод будут включены jQuery и JavaScript, это не соответствует приведенным выше спецификациям. Мы также можем предположить, что текст HTML является входом для функций, поэтому import requests не требуется. - person Matt; 03.03.2016