BeautifulSoup: Große HTML Zweige parsen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
microkernel
User
Beiträge: 271
Registriert: Mittwoch 10. Juni 2009, 17:27
Wohnort: Frankfurt
Kontaktdaten:

Hallo,

da ich mich zur Zeit in Sachen XML-Parsing etwas weiterbilden möchte (ich hab das Thema früher immer gemieden), versuche ich zur Zeit eine HTML-Kopie meiner Facebook-Nachrichten (es ist möglich, eine Kopie deiner Facebook-Daten als Archiv herunterzuladen) zu parsen. Alle Nachrichten, die ich je auf Facebook verschickt habe sind in dieser Datei enthalten - dementsprechend ist die Datei etwa 10MB groß.
Der Aufbau einer Unterhaltung sieht dort in etwa so aus:

Code: Alles auswählen

<div class="thread"><div class="border">
<div class="header">

<span class="profile fn">Chatpartner1</span>, <span class="profile fn">Chatpartner 2</span> <!-- ggf. noch mehr !-->

<abbr class="time published" title="2012-12-15T23:31:17+0000">15. Dezember 2012 um 16:31</abbr> <!-- Datum des letzten Kontakts !-->

</div>

<!-- Selbsterklärend !-->
<div class="message">
<div class="from"><span class="profile fn">Name Absender</span></div>
<abbr class="time published" title="2010-09-18T09:17:53+0000">18. September 2010 um 3:17</abbr>
<div class="msgbody">
NACHRICHT
</div>
</div>

<div class="message">
<div class="from"><span class="profile fn">Name Absender</span></div>
<abbr class="time published" title="2010-09-18T09:17:53+0000">18. September 2010 um 3:17</abbr>
<div class="msgbody">
NACHRICHT
</div>
</div>

<!-- weitere Nachrichten !-->

</div></div>
So wie ich das sehe, müsste sich das doch eigentlich ganz nett parsen lassen, oder? Mein derzeitiger Ansatz sind folgende Funktionen (,welche allerdings erstaunlich langsam sind):

Code: Alles auswählen

def process_threads(html_source):
    data = []
    print "[*] Initializing soup..."
    soup = BeautifulSoup(html_source) # dauert sehr lange!
    print "[*] Intialized soup!" 
    threads = soup.find_all("div", {"class" : "border"})
    print "[*] Processing threads..."
    for thread in threads:
        data.append(process_thread(thread))
    print "[*] Threads created"
    return data

def process_thread(thread):
    data = {"messages" : []}
    # Get partners name
    header = thread.find("div", {"class" : "header"})
    partners = header.find_all("span", {"class" :
                                           "profile fn"})
    data["partners"] = frozenset(map(lambda obj: obj.getText(), partners))
    # Get last contact
    last_contact = header.find("abbr", {"class" :
                                        "time published"})
    data["last contact"] = datetime.datetime.strptime(last_contact.get("title"),
                                             "%Y-%m-%dT%H:%M:%S+0000")
    # Extract messages
    messages = thread.find_all("div", {"class" : "message"})
    for message in messages:
        msg = {}
        msg["from"] = message.find("span", {"class" : "profile fn"}).getText()
        msg["time"] = datetime.datetime.strptime(message.find("abbr").get("title"),
                                        "%Y-%m-%dT%H:%M:%S+0000")
        msg["text"] = message.find("div", {"class" : "msgbody"}).getText()
        data["messages"].append(msg)

    return data
Was mich allerdings an der ganzen Sache stört, ist dass es furchtbar lange dauert die Html-Datei zu parsen. Ich bin jetzt wirklich keiner, der ständig nur am optimieren ist, aber ich denke, dass man das bestimmt schneller parsen kann. Zur Zeit benötigt die Funktion process_threads(htmlsource) bei einer 13MB großen Datei etwa 75 Sekunden. Ich jemand Optimierungsvorschläge? Vielleicht gehe ich beim parsen auch ganz "falsch" vor. Vielleicht sollte ich nicht die ganze zeit .find(...) benutzen.
lunar

@microkernel BeautifulSoup per se ist langsam. Nutze lxml.html.
Benutzeravatar
microkernel
User
Beiträge: 271
Registriert: Mittwoch 10. Juni 2009, 17:27
Wohnort: Frankfurt
Kontaktdaten:

Danke für den Tipp! ;) Könntest du mir ein Hinweis geben, wie man so etwas mit lxml parst? Wie gesagt kenn ich mich damit so gut wie gar nicht aus... Ich verlang jetzt kein fertigen code - nur ein Weg wie ich das machen könnte.
lunar

@microkernel Lies die Dokumentation. Sie enthält eine Referenz, Beispiele und alles, was das Herz so begehrt :)
Benutzeravatar
__blackjack__
User
Beiträge: 14017
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@microkernel: Zum parsen selbst kann auch `BeautifulSoup` die `lxml.html`-Bibliothek verwenden. Einfach entsprechendes zweites Argument beim Erstellen des Objekts angeben.

Das befüllen von Wöerterbüchern ist etwas kompakter und übersichtlicher wenn man nicht erst ein leeres Wörterbuch anlegt und danach die festen Schlüssel mit den Werten zuweist, sondern das gleich in das Wörterbuch schreibt. „List comprehensions“ können da auch hilfreich sein.

Wenn man für `map()` keine Funktion zur Hand hat, sondern die sich ad hoc als ``lambda``-Ausdruck bastelt, ist ein Generatorausdruck oft verständlicher und kürzer.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import datetime
from bs4 import BeautifulSoup


def get_timestamp(element):
    return datetime.datetime.strptime(
        element.get("title"), "%Y-%m-%dT%H:%M:%S+0000"
    )


def process_thread(thread):
    header = thread.find("div", "header")
    return {
        "partners": frozenset(
            span.get_text() for span in header.find_all("span", "profile fn")
        ),
        "last contact": get_timestamp(header.find("abbr", "time published")),
        "messages": [
            {
                "from": message.find("span", "profile fn").get_text(),
                "time": get_timestamp(message.find("abbr")),
                "text": message.find("div", "msgbody").get_text(),
            }
            for message in thread.find_all("div", "message")
        ],
    }


def process_threads(html_source):
    print("[*] Initializing soup...")
    soup = BeautifulSoup(html_source, "lxml.html")
    print("[*] Intialized soup!")
    threads = soup.find_all("div", "border")
    print("[*] Processing threads...")
    data = list(map(process_thread, threads))
    print("[*] Threads created")
    return data
Was man als nächstes prüfen sollte, ist ob es tatsächlich bei jedem `find_all()` wirklich nötig ist, dass da *rekursiv* gesucht wird. Und ob man bei den `find()`-Aufrufen nicht auch hier und dort darauf verzichten kann, und lieber direkt dort hin navigiert.

"partners" und "last contact" sind redundant, weil man diese Informationen auch aus den Nachrichten ermitteln kann. Könnte also eventuell schneller gehen.

Falls die Threads ähnlich gross und/oder zahlreich sind, kann es sich vielleicht auch lohnen `concurrent.futures` zu verwenden um das auf mehrere Prozesse aufzuteilen.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Antworten