2
votes

Extraire tout le texte entre deux en-têtes HTML avec lxml

J'essaye d'analyser la page HTML en utilisant lxml en Python.

En HTML, j'ai cette structure:

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_))

J'ai besoin d'analyser ce HTML pour suivre le 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]",
   }
]

Je peux lire toutes les balises h5 avec des titres et les écrire en JSON en utilisant ce code:

<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>

Le problème est , Je ne sais pas comment lire tout le contenu de ces paragraphes entre les titres

et les mettre dans le champ JSON text .


1 commentaires

La seule chose à laquelle je peux penser est d'analyser tout tag par tag et d'en construire JSON ...


3 Réponses :


0
votes

Pour obtenir tout le texte "entre" deux éléments, par exemple entre deux en-têtes, il n'y a pas d'autre moyen que celui-ci:

  • parcourez toute l ' arborescence (nous utiliserons .iterwalk () car nous devons faire une distinction entre le début et la fin des éléments)
  • créer un élément de données pour chaque en-tête rencontré (appelons-le le current_heading )
  • rassembler dans une liste tous les bits de texte individuels de tout autre élément fourni
  • chaque fois qu'un nouvel en-tête est rencontré, stocker les données collectées jusqu'à présent et commencer un nouvel élément de données

Chaque élément de l'élément ElementTree peut avoir un .text et un .tail:

[
    {
        "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]"
    }
]

Nous devons collecter les deux, sinon le texte sera absent de la sortie.

Ce qui suit garde une trace de l'endroit où nous en sommes dans l'arborescence HTML à l'aide d'une pile, donc .head et .tail des éléments imbriqués sont collectés dans le bon ordre.

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'])

Lorsque j'exécute ceci sur votre exemple de HTML, j'obtiens cette sortie (au format JSON) :

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

Ma fonction normalize () est très simple et conserve toutes les nouvelles lignes et autres espaces blancs qui font partie du code source HTML. Écrivez une fonction plus sophistiquée si vous voulez un meilleur résultat.


0 commentaires

0
votes

Il existe un moyen plus simple de le faire, il suffit de garder une trace de la position de la prochaine h5 et de vous assurer de sélectionner les p avec une position inférieure:

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))
  })

Il pourrait même être encore plus simple de "suivez" simplement le suivant-frère :: * jusqu'à ce qu'il ne soit plus un p


4 commentaires

Cela ne fonctionne que pour les

, et seulement si tout le texte est vraiment à l'intérieur d'un

. Les documents du monde réel ne seront probablement pas structurés de manière optimale.


Vous pouvez changer ce p en ce que vous voulez. Habituellement, je vais juste suivre le "next ()" dans une boucle while mais ceux-ci sont maladroits en python


Ouais, mais cela ne couvrira toujours pas le texte en dehors de

(ou autre). De plus, précédent-frère ne s'arrête pas au

précédent, donc au dernier
, le compte ( previous-sibling :: *) sera tous les éléments avant ce
, donc je ne suis même pas sûr de savoir comment toute cette position < / code> chose est censée fonctionner? (Autre que cela: L'objet 'AttributeError: lxml.etree._Element' n'a pas d'attribut 'text_content' )


le texte en dehors de p ou quoi que ce soit peut être sélectionné avec text (). count (precedent-sibling :: *) donnera la position de l'élément que vous pouvez utiliser dans votre expression xpath position (). J'utilise html de lxml.



0
votes

Commencez par diviser les enfants de l'élément en sections individuelles en fonction de la balise transmise.

data = []
for section in split(html, "h5"):
    if section and section[0].tag == "h5":
        data.append(
            {
                "title": section[0].text_content(),
                "text": "\n".join(q.text_content() for q in section[1:]),
            }
        )

De là, il peut être remodelé en dictionnaire. Quelque chose comme ce qui suit devrait faire:

def split(element, tag):
    sections = [[]]
    for element in element:
        if element.tag == tag:
            sections.append([])
        sections[-1].append(element)
    return sections


0 commentaires