Извлечение всего текста между двумя HTML-заголовками с помощью lxml - PullRequest
2 голосов
/ 21 апреля 2019

Я пытаюсь разобрать HTML-страницу, используя lxml в Python.

В HTML есть такая структура:

<html>
   <h5>Title</h5>
   <p>Some text <b>with</b> <i>other tags</i>.</p>
   <p>More text.</p>
   <p>More text[2].</p>

   <h5>Title[2]</h5>
   <p>Description.</p>

   <h5>Title[3]</h5>
   <p>Description[1].</p>
   <p>Description[2].</p>

   ***
   and so on...
   ***
</html>

Мне нужно проанализировать этот HTML-код для следующего JSON:

[
   {
      "title": "Title",
      "text": "Some text with other tags.\nMore text.\nMore text[2].",
   },
   {
      "title": "Title[2]",
      "text": "Description.",
   },
   {
      "title": "Title[3]",
      "text": "Description[1].\nDescription[2]",
   }
]

Я могу прочитать все теги h5 с заголовками и записать их в JSON, используя этот код:

array = []
for title in tree.xpath('//h5/text()'):
    data = {
        "title" : title,
        "text" : ""
    }
    array.append(data)

with io.open('data.json', 'w', encoding='utf8') as outfile:
    str_ = json.dumps(array,
                      indent=4, sort_keys=True,
                      separators=(',', ' : '), ensure_ascii=False)
    outfile.write(to_unicode(str_))

Проблема в том, что я не знаю, как прочитать все содержимое этих абзацев между <h5> заголовками и поместить их в text поле JSON.

Ответы [ 2 ]

0 голосов
/ 22 апреля 2019

Есть более простой способ сделать это, просто следите за положением следующего h5 и убедитесь, что вы выбираете p с более низкой позицией:

data = []

for h5 in doc.xpath('//h5'):
  more_h5s = h5.xpath('./following-sibling::h5')
  position = int(more_h5s[0].xpath('count(preceding-sibling::*)')) if len(more_h5s) > 0 else 999
  ps = h5.xpath('./following-sibling::p[position()<' + str(position) + ']')
  data.append({
    "title": h5.text,
    "text": "\n".join(map(lambda p: p.text_content(), ps))
  })

Возможно, будет еще проще просто "следовать" за following-sibling::*, пока он не перестанет быть p

0 голосов
/ 21 апреля 2019

Чтобы получить весь текст «между» двумя элементами, например, между двумя заголовками, нет другого способа, кроме этого:

  • пройти весь tree (мы будем использовать .iterwalk()потому что мы должны различать начало и конец элементов)
  • создать элемент данных для каждого встречаемого заголовка (назовем его current_heading)
  • собрать в список всех отдельныхтекстовые биты любого другого элемента, которые встречаются
  • каждый раз, когда встречается новый заголовок, сохраните собранные данные и начните новый элемент данных

Каждый элемент в элементе ElementTree можетиметь .text и .tail:

<b>This will be the .text</b> and this will be the .tail

Мы должны собрать оба, иначе текст будет отсутствовать в выводе.

Следующее отслеживает, где мы находимся вДерево HTML использует стек, поэтому .head и .tail вложенных элементов собираются в правильном порядке.

collected_text = []
data = []
stack = []
current_heading = {
    'title': '',
    'text': []
}
html_headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']

def normalize(strings):
    return ''.join(strings)

for event, elem in ET.iterwalk(tree, events=('start', 'end')):
    # when an element starts, collect its .text
    if event == 'start':
        stack.append(elem)

        if elem.tag in html_headings:
            # reset any collected text, b/c now we're starting to collect
            # the heading's text. There might be nested elements in it.
            collected_text = []

        if elem.text:
            collected_text.append(elem.text)

    # ...and when it ends, collect its .tail
    elif event == 'end' and elem == stack[-1]:

        # headings mark the border between data items
        if elem.tag in html_headings:
            # normalize text in the previous data item
            current_heading['text'] = normalize(current_heading['text'])

            # start new data item
            current_heading = {
                'title': normalize(collected_text),
                'text': []
            }
            data.append(current_heading)
            # reset any collected text, b/c now we're starting to collect
            # the text after the the heading
            collected_text = []

        if elem.tail:
            collected_text.append(elem.tail)

        current_heading['text'] = collected_text
        stack.pop()

# normalize text in final data item
current_heading['text'] = normalize(current_heading['text'])

Когда я запускаю это для вашего примера HTML, я получаю этот вывод (в формате JSON):

[
    {
        "text" : "\n   Some text with other tags.\n   More text.\n   More text[2].\n\n   ",
        "title" : "Title"
    },
    {
        "text" : "\n   Description.\n\n   ",
        "title" : "Title[2]"
    },
    {
        "text" : "\n   Description[1].\n   Description[2].\n\n   ***\n   and so on...\n   ***\n",
        "title" : "Title[3]"
    }
]

My * 103Функция 1 * очень проста и сохраняет все новые строки и другие пробелы, которые являются частью исходного кода HTML.Напишите более сложную функцию, если вы хотите более хороший результат.

...