Вы имеете дело с HTML, отформатированным в Microsoft Word . Не извлекайте текст и не пытайтесь обрабатывать его без этого контекста.
Раздел, который вы хотите обработать, четко очерчен тегами <a name="...">
, давайте начнем с выбора всех элементов с маркером <a name="ITEM_1A_RISK_FACTORS">
, вплоть до, но не включая маркер <a name="ITEM2_UNREGISTERED_SALES">
:
def sec_section(soup, item_name):
"""iterate over SEC document paragraphs for the section named item_name
Item name must be a link target, starting with ITEM
"""
# ask BS4 to find the section
elem = soup.select_one('a[name={}]'.format(item_name))
# scan up to the parent text element
# html.parser does not support <text> but lxml does
while elem.parent is not soup and elem.parent.name != 'text':
elem = elem.parent
yield elem
# now we can yield all next siblings until we find one that
# is also contains a[name^=ITEM] element:
for elem in elem.next_siblings:
if not isinstance(elem, str) and elem.select_one('a[name^=ITEM]'):
return
yield elem
Эта функция дает нам все дочерние узлы от узла <text>
в документе HTML, которые начинаются с абзаца, содержащего конкретную цель ссылки, вплоть до следующей цели ссылки, которая называет ITEM
.
Далее обычная задача очистки Word - удалить теги <font>
, атрибуты style
:
def clean_word(elem):
if isinstance(elem, str):
return elem
# remove last-rendered break markers, non-rendering but messy
for lastbrk in elem.select('a[name^=_AEIOULastRenderedPageBreakAEIOU]'):
lastbrk.decompose()
# remove font tags and styling from the document, leaving only the contents
if 'style' in elem.attrs:
del elem.attrs['style']
for e in elem: # recursively do the same for all child nodes
clean_word(e)
if elem.name == 'font':
elem = elem.unwrap()
return elem
Метод Tag.unwrap()
- это то, что больше всего поможет вашему делу, так как текст почти произвольно разделен на <font>
теги.
Теперь неожиданно аккуратно извлечь текст:
for elem in sec_section(soup, 'ITEM_1A_RISK_FACTORS'):
clean_word(elem)
if not isinstance(elem, str):
elem = elem.get_text(strip=True)
print(elem)
Выводит среди остального текста:
•that the equipment and processes which we have selected for Model 3 production will be able to accurately manufacture high volumes of Model 3 vehicles within specified design tolerances and with high quality;
Текст теперь правильно соединен, повторное объединение больше не требуется.
Весь раздел все еще находится в таблице, но clean_word()
исправил это до гораздо более разумного:
<div align="left">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td valign="top">
<p> </p></td>
<td valign="top">
<p>•</p></td>
<td valign="top">
<p>that the equipment and processes which we have selected for Model 3 production will be able to accurately manufacture high volumes of Model 3 vehicles within specified design tolerances and with high quality;</p></td></tr></table></div>
так что вы можете использовать более умные методы извлечения текста для дальнейшего обеспечения чистого преобразования текста здесь; Вы можете преобразовать такие таблицы маркеров в префикс *
, например:
def convert_word_bullets(soup, text_bullet="*"):
for table in soup.select('div[align=left] > table'):
div = table.parent
bullet = div.find(string='\u2022')
if bullet is None:
# not a bullet table, skip
continue
text_cell = bullet.find_next('td')
div.clear()
div.append(text_bullet + ' ')
for i, elem in enumerate(text_cell.contents[:]):
if i == 0 and elem == '\n':
continue # no need to include the first linebreak
div.append(elem.extract())
Кроме того, вы, вероятно, захотите пропустить разрывы страниц (комбинация элементов <p>[page number]</p>
и <hr/>
), если вы запустите
for pagebrk in soup.select('p ~ hr[style^=page-break-after]'):
pagebrk.find_previous_sibling('p').decompose()
pagebrk.decompose()
Это более явно, чем ваша собственная версия, где вы удаляете все элементы <hr/>
и предшествующий элемент <p>
независимо от того, являются ли они на самом деле братьями и сестрами.
Выполните оба перед очисткой Word Word. В сочетании с вашей функцией это вместе становится:
def get_text(url, item_name):
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
for pagebrk in soup.select('p ~ hr[style^=page-break-after]'):
pagebrk.find_previous_sibling('p').decompose()
pagebrk.decompose()
convert_word_bullets(soup)
cleaned_section = map(clean_word, sec_section(soup, item_name))
return ''.join([
elem.get_text(strip=True) if elem.name else elem
for elem in cleaned_section])
text = get_text(url, 'ITEM_1A_RISK_FACTORS')
with open(os.path.join(path, 'test.txt'), 'w', encoding='utf8') as f:
f.write(text)