2025, Sep 27 11:21

Надежное извлечение строк в BeautifulSoup: делим только по br

Как избежать дробления строк в BeautifulSoup: делим текст только по br, разворачиваем font (unwrap), применяем smooth и получаем предсказуемый парсинг HTML.

При извлечении структурированного текста с помощью BeautifulSoup легко ненароком разбить содержимое по границам разметки, которые вам не важны. Типичный сценарий: вы хотите делить текст только по HTML‑переносам строк br, но оформительские теги вроде font дробят содержимое и засоряют вывод лишними разбиениями. Ниже — краткий разбор проблемы и аккуратный способ получить надежное построчное извлечение.

Суть проблемы

Предположим, вы проходите по секциям blockquote и хотите, чтобы каждая строка делилась только там, где стоит br. Если посреди строки встречается тег font, BeautifulSoup stripped_strings может разбить эту строку на несколько фрагментов, хотя визуально это одна строка. Задача — сохранить строки целыми и делить их исключительно по br.

Воспроизводимый HTML-пример

Ниже упрощённый фрагмент, на котором видно, в чём дело. Тег font прерывает одну и ту же логическую строку и вызывает нежелательное разбиение:

<blockquote>
<p>RT CLOAK<a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesc3.htm#1057"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a><br><font color="FFFFFF">RT </font>COAT<a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesc4.htm#1082"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a><br>BT GARMENT<a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesg1.htm#2099"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a><br><i>NP ABBA</i><a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesa1.htm#3"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a></p>
<hr width="90%"></blockquote>
<p><a name="3"></a><i>ABBA NP</i></p>
<blockquote>

Код, который проявляет проблему

Скрипт ниже проходит по странице, находит блоки blockquote и печатает связанные с ними строки текста. Однако вывод дробится внутри одной логической строки из‑за тега font. Итерация идёт через stripped_strings — именно там и происходит фрагментация.

from bs4 import BeautifulSoup
import requests
sources = ['https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesa1.htm']
counter = 1
for url in sources:
    resp = requests.get(url)
    markup = resp.text
    dom = BeautifulSoup(markup, "lxml")
    blocks = dom.find_all('blockquote')
    for blk in blocks:
        header = blk.find_previous('p')
        for chunk in blk.stripped_strings:
            print(counter, header.text, chunk)
        counter = counter + 1

Типичный нежелательный результат — когда «RT» и «COAT» идут как два отдельных элемента, хотя они принадлежат одной строке и разделены только br.

Почему так происходит

stripped_strings возвращает текст, разбитый по структуре DOM. Оформительские элементы вроде font могут делить строку на разные текстовые узлы. Даже если визуально это сплошная строка, узлы уже разделены — и stripped_strings отдаёт их по отдельности.

Подход к решению

Если нужно делить только по br и игнорировать оформление, уберите промежуточные оформительские узлы, чтобы они не резали текст. Прямой способ — развернуть теги font (unwrap), оставив их текст на месте, а затем слить соседние текстовые узлы перед обходом с stripped_strings.

Разворачивание выполняется методом .unwrap(). Слияние соседних текстовых узлов после разворачивания — методом .smooth(). Важно вызвать .smooth() уже после .unwrap(), чтобы новые соседние строки действительно объединились.

Минимальный рабочий пример

Ниже компактный пример, который работает с образцом HTML выше и возвращает ожидаемый список строк:

from bs4 import BeautifulSoup
sample = """
<blockquote>
<p>RT CLOAK<a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesc3.htm#1057"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a><br><font color="FFFFFF">RT </font>COAT<a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesc4.htm#1082"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a><br>BT GARMENT<a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesg1.htm#2099"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a><br><i>NP ABBA</i><a href="https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesa1.htm#3"><img src="./British Museum Object Names Theasarus_ Terms AB-AM_files/link.gif" border="0"></a></p>
<hr width="90%"></blockquote>
<p><a name="3"></a><i>ABBA NP</i></p>
</blockquote>
"""
doc = BeautifulSoup(sample, "lxml")
for node in doc.find_all("font"):
    node.unwrap()
doc.smooth()
for bq in doc.find_all("blockquote"):
    print(list(bq.stripped_strings))

Результат — аккуратный список строк:

['RT CLOAK', 'RT COAT', 'BT GARMENT', 'NP ABBA']

Исправленный скрипт целиком

Применяя ту же идею к предыдущему обходу страницы, разворачивание и последующее сглаживание перед итерацией заставляет stripped_strings учитывать только реальные переносы строк. Логика остаётся прежней; просто в DOM меньше оформительских границ, по которым можно непреднамеренно разделить текст.

from bs4 import BeautifulSoup
import requests
sources = ['https://terminology.collectionstrust.org.uk/British-Museum-objects/Obthesa1.htm']
counter = 1
for url in sources:
    resp = requests.get(url)
    markup = resp.text
    dom = BeautifulSoup(markup, "lxml")
    for f in dom.find_all("font"):
        f.unwrap()
    dom.smooth()
    quotes = dom.find_all('blockquote')
    for quote in quotes:
        title = quote.find_previous('p')
        for line in quote.stripped_strings:
            print(counter, title.text, line)
        counter += 1

Почему это важно

Когда вы нормализуете HTML для последующей обработки, последовательное разбиение на строки критично. Если хотите делить только по br, позволять встроенному оформлению ломать текст — значит дробить токены и нарушать любую логику, зависящую от непрерывных строк. Убирая несущественные структурные границы перед извлечением текста, вы получаете предсказуемый и удобный для обработки результат.

Если нужно делить строго по br, ещё один практичный приём — заменить br в HTML на уникальный разделитель, собрать сплошной текст, а затем разделить по этому маркеру. Это особенно удобно, когда нельзя или нежелательно удалять другие теги.

Выводы

Пользуйтесь stripped_strings для обхода «чистого» текста, но помните: он следует границам DOM. Если встроенное оформление рвёт ваши строки, сначала разверните мешающие теги и вызовите smooth, чтобы слить соседние текстовые узлы. Тогда разбиение будет определяться реальными переносами строк, а не артефактами стилизации.

Статья основана на вопросе с StackOverflow от James Brian и ответе от jqurious.