XML-Parsing von Text-Dateien und Output in CSV-Datei erfassen

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
Jan93
User
Beiträge: 4
Registriert: Dienstag 16. Juli 2019, 12:06

Hallo zusammen,

ich bin gerade dabei Text-Dateien mit XML-Struktur zu parsen. Folgender Link zeigt euch zur Veranschaulichung eine Beispiel-Datei:
https://www.sec.gov/Archives/edgar/data ... 003763.txt

Die Dateien habe ich bereits heruntergelanden und aufgrund von Performance-Problemen um die ersten Zeilen gekürzt (mit einem anderen Code), sodass die untersuchten Dateien bei "<?xml version="1.0"?>" beginnen.

Zu meinem Code:
Die einzelnen Textdokumente werden nach bestimmten "Tags" durchsucht. Anschließend wird in einer CSV-Datei pro Zeile der Output von einem Dokument wiedergegeben und die herausgelesenen Informationen nacheinander in Spalten erfasst. Allerdings sind in manchen Text-Dokumenten einige Informationen mehrfach vorhanden, da z.b. mehrere Personen (in BspDatei ReportingOwner) in einer Datei aufgeführt werden. Dies führt dazu, dass mein bisheriger Code die Informationen in den vorgesehenen Zellen überschreibt und nicht automatisch in die nächste freie Zelle springt, sodass am Ende nur eine Person in meiner CSV-Datei erfasst wurde.
Hier mein Code:

Code: Alles auswählen

import csv
import glob
import re
import string
import time
import bs4 as bs


# User defined directory for files to be parsed
TARGET_FILES = r'D:\Files\**'

# User defined output file
OUTPUT_FILE =  r'D:\Output\outputfile.csv'
# Setup output
OUTPUT_FIELDS = [r'Datei',r'IssuerCIK', r'issuerName', r'ReOwnerCIK', r'ReOwnerCIK',   r'transactionDate', r'transactionsCode', r'Director', r'Officer', r'Titel', r'10-% Eigner', r'sonstiges', r'SignatureDate']


def main():

    f_out = open(OUTPUT_FILE, 'w')
    wr = csv.writer(f_out, lineterminator='\n', delimiter=';')
    wr.writerow(OUTPUT_FIELDS)

    file_list = glob.glob(TARGET_FILES)
    for file in file_list:
        print(file)
        with open(file, 'r', encoding='UTF-8', errors='ignore') as f_in:
            soup = bs.BeautifulSoup(f_in, 'xml')

        output_data = get_data(soup)
        output_data[0] = file                       # the number determine the column for the output
        wr.writerow(output_data)


def get_data(soup):

    odata = [0] *200


    Iscik = soup.find_all('issuerCik')
    Iname = soup.find_all('issuerName')
    Owncik = soup.find_all('rptOwnerCik')
    name = soup.find_all('rptOwnerName')
    direct = soup.find_all('isDirector')
    off = soup.find_all('isOfficer')
    ten = soup.find_all('isTenPercentOwner')
    other = soup.find_all('isOther')
    titel = soup.find_all('officerTitle')


    for x in range(0, len(Iscik)):
        try:
            odata[1]= (Iscik[x].get_text())
        except IndexError:
            odata[1]= ('ka')
        try:
            odata[2]= (Iname[x].get_text())
        except IndexError:
            odata[2]= ('ka')


    for ii in range(0, len(Owncik)):
        try:
            for i in range(3, 33):
                odata[i] = (Owncik[ii].get_text())
        except IndexError:
            for i in range( 3, 33):
                odata[i] =('ka')
        try:
            for i in range(33, 63):
                odata[i] =(name[ii].get_text())
        except IndexError:
            for i in range(33, 63):
                odata[i] =('ka')
        try:
            for i in range(63, 93):
                odata[i] =(direct[ii].get_text())
        except IndexError:
            for i in range(63, 93):
                odata[i] =('ka')
     
         return odata


if __name__ == '__main__':
    print('\n' + time.strftime('%c') + '\nGeneric_Parser.py\n')
    main()
    print('\n' + time.strftime('%c') + '\nNormal termination.')
Mein For-Loop bei "for i in range(3, 33) odata = (Owncik[ii].get_text())" führt leider nur dazu, dass er in den angegeben 30 Zellen pro Zelle den loop immer von vorne beginnt und nicht, wie gewünscht pro Ergebnis eine Zeile weiter springt, sodass ich in den 30 Zeilen immer den gleichen Namen/bzw. Nummer drin stehen habe.

Wie kann ich meinen Code so anpassen, dass die Info automatisch in die nächste Zelle geschrieben wird?

Ich hoffe jmd hat hier eine Idee und kann mir evtl weiterhelfen. Jeder Tipp ist willkommen! Vielen Dank!
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jan93: Du machst da echt komische Sachen. `odata` wird mit 0en initialisiert die dann durch Texte ersetzt werden? Warum 0en? Wenn überhaupt würde man da `None` als Platzhalter nehmen, aber selbst das ist extrem unpythonisch. Man baut Listen durch Anhängen der Werte auf die man in der Liste haben möchte, nicht in dem man erste eine Liste mit Dummy-Werten erzeugt, und die dann per Indexzugriff durch die tatsächlichen Werte ersetzt.

Die 300 passt auch irgendwie nicht zur Kopfzeile in `OUTPUT_FIELDS`. Mir scheint eine CSV-Datei ist nicht wirklich geeignet wenn Du Felder hast die mehr als einen Wert pro Datensatz haben. Oder Du musst die da irgendwie geeignet verschachteln. Beispielsweise als JSON kodiert. Schön ist das aber nicht und die Frage ist, ob das überhaupt sinnvoll ist für die Weiterverarbeitung der Daten.

Was soll das `o` eigentlich bedeuten? Gewöhn Dir irgendwelche komischen Abkürzungen gar nicht erst an. `direct` bedeutet etwas anderes als `is_director` und ein Leser sollte nicht raten müssen dass das eine die Abkürzung des anderen ist. Wo wir bei der Schreibweise in Python wären: alles klein_mit_unterstrichen, ausgenommen Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

`re` und `string` werden importiert, aber nicht verwendet. Und warum `bs4` als `bs` importiert wird, ist mir auch nicht klar.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

Ich verstehe nicht, was Du versuchst zu erreichen. Kannst Du ein kurzes Beispiel machen, mit XML-Input und gewünschtem Output, vielleicht wird dann klarer, was Du willst. Denn der Code macht so überhaupt keinen Sinn und es ist schwierig daraus zu raten, wie es richtig gehen sollte.

Statt alle Tags irgendwo zu suchen, solltest Du die Struktur der XML-Daten parsen.
Du hast eine Liste mit reportingOwner aus der Du bestimmte Daten herausholen willst, dann tue auch genau das. Suche auf Ebene von <ownershipDocument> alle <reportingOwner> und darin dann die Daten die Du willst. Erzeuge daraus eine Liste von z.B. Wörterbüchern mit diesen Daten. Überlege Dir dann in einem weiteren Schritt, wie Du diese Wörterbücher dann in die CSV-Datei schreiben kannst.
Jan93
User
Beiträge: 4
Registriert: Dienstag 16. Juli 2019, 12:06

@__blackjack_ Danke für dein Feedback und die Hinweise. Werde ich beachten (y) Bin noch neu beim Pythonprogrammieren, deswegen sind die Bezeichnungen etwas untypisch.
Das Grundgerüst des Codes stammt von einem andern Code und einige Bezeichnungen (so auch _odata)/Importe habe ich übernommen. Ich schätze mal das 'o' steht für 'Output'.

@ Sirius3, klar kann ich versuchen. Sorry, dass es etwas kompliziert ausgedrückt ist.

XML-Input:

<ownershipDocument>
<reportingOwner>
<reportingOwnerId>
<rptOwnerCik>0001706059</rptOwnerCik>
<rptOwnerName>Hicks George G</rptOwnerName>
</reportingOwnerId>
</reportingOwner>
<reportingOwner>
<reportingOwnerId>
<rptOwnerCik>0001704962</rptOwnerCik>
<rptOwnerName>Varde Fund XI (Master), L.P.</rptOwnerName>
</reportingOwnerId>
</reportingOwner>


gewünschter CSV-Datei Output:

Datei; Reporting Owner CIK; Reporting Owner CIK; Reporting Owner Name; Reporting Owner Name;
Dateiname; 0001706059; 0001704962; Hicks George G; Varde Fund XI (Master), L.P.;


leider kommt mit meinem jetzigen Code dass raus:
Datei; Reporting Owner CIK; Reporting Owner CIK; Reporting Owner Name; Reporting Owner Name; (der Header der CSV-File bleibt ja immer gleich)
Dateiname; 0001704962; 0001704962; Varde Fund XI (Master), L.P; Varde Fund XI (Master), L.P.;

Ich hoffe dass hilft dir.

Prinzipiell parst mein Code die Dateien schon richtig, allerdings klappt die Speicherung im CSV-File nicht.
Würde ich alle Ergebnisse mit der Print-Funktion ausgeben lassen, listet er mir die Ergebnisse schön nacheinander auf.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OUTPUT_FIELDS: Da passt `OUTPUT_FIELDS` als erstes mal nicht zu, und Du müsstest halt vom Ergebnis der Abfrage aus dem XML-Dokument eben die ersten beiden Treffer nehmen. Wenn es mehr gibt, dann entsprechend abschneiden, oder was immer Du in dem Fall machen willst. Das Beispieldokument welches Du verlinkt hast, hat da soweit ich das sehe 10 Einträge. Und falls es weniger sind, dann mit irgend etwas auffüllen. Eine recht allgemein nutzbare Funktion dafür wäre `more_itertools.padded()` und zum ”abschneiden” von zu viel Einträgen `itertools.islice()` oder normales „slicing“.

Und wie gesagt nicht eine Liste mit Dummywerten erstellen. Du fängst mit einer Liste mit dem Dateinamen an, und erweiterst die dann mit `append()` und/oder `extend()` entsprechend.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Jan93
User
Beiträge: 4
Registriert: Dienstag 16. Juli 2019, 12:06

Danke Blackjack für den Befehl append(), genau dass habe ich gesucht. :)
Auch der 'more_itertools.padded()'-Befehl ist ein super Hinweis gewesen. Ich muss allerdings noch herausfinden, wie ich diesen genau verwende.
Abschneiden tu ich nix, da ich möglichst alle Informationen aus den Meldungen herauslesen will. :lol:

Vielen Dank für deinen Tipps und Hilfe!
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jan93: Aber dann hast Du am Ende doch CSV-Dateien mit einer Variablen Anzahl von Spalten. Dazu müsstest Du dann auch die Kopfzeile entsprechend generieren, und die Dateien untereinander haben dann unterschiedliche Strukturen. Das klingt alles wenig sinnvoll. Entweder ist CSV die gänzlich falsche Art die Daten zu speichern, oder Du müsstest noch einmal dringend über die Anordnung nachdenken. Sinnvoll sind CSV-Dateien mit einer festen Anzahl von Spalten, wo auch jede Spaltenüberschrift nur einmal vorkommt und mit mehreren Datensätzen pro Datei. Also die <ReportingOwner> könnte man beispielsweise in CSV mit einem ”Owner” pro Datensatz speichern. In der Spalte ”Datei” steht dann immer der Name der Datei aus der der jeweilige ”Owner” kommt oder man lässt das Weg und vergibt einen CSV-Dateinamen so, dass man das der ursprünglichen SGML-Datei zuordnen kann.

Problematisch wird dann der ”issuer” und die Transaktionsdaten, weil die ja für alle ”Owner”-Datensätze gleich sind. Das passt halt nicht in so ein zweidimensionales Tabellenschema. Weshalb ich die Vermutung habe, dass CSV das falsche Dateiformat ist. Das wird schon einen Grund haben warum die SGML/XML gewählt haben. JSON oder „JSON Lines” würde sich vielleicht als Format anbieten.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Jan93
User
Beiträge: 4
Registriert: Dienstag 16. Juli 2019, 12:06

Ja das mit den unterschiedlichen Spalten ist durchaus ein Problem, dachte aber dass kann man mit dem padded() Befehl beheben?

Grundsätzlich gehts mir gar nicht daraum, sämtliche Transaktionen korrekt den Leuten zuzuordnen. Dementsprechend wäre es ok, wenn die Transaktionen nacheinander dargestellt werden. Mehrere CSV-Dateien wäre in der Tat eine einfach Lösung, allerdings möchte bzw. muss ich die gesamten Daten am Ende noch auswerten, z.b. wie viel wurde in diesem Jahr verkauft/gekauft, wer hat die Transaktionen gemeldet (dabei geht es eher um Director, Officer, 10 % Eigner, sonstige) usw.. Deswegen muss ich die Informationen auch noch so aufbereiten, dass ich diese dann mit einem Statistik-Programm (Stata, R usw.) auswerten kann.
Würde das mit JSON Datei gehen? Dh. kann Stata/R JSON Dateien einlesen?
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jan93: Das Problem ist nicht das man das nicht in diese komische Spaltenform bekommt, das geht sicher irgendwie, aber was will man dann damit anfangen? Das ist einfach ein unsinniges Format das bei der Erstellung schon Aufwand bedeutet, und bei der Weiterverarbeitung dann auch wieder. CSV-Dateien haben in der Regel das Format einer 2D-Tabelle mit einer Spalte pro Werttyp. Das lässt sich leicht verarbeiten und viele Werkzeuge erwarten genau so ein Format. Auch Stata/R erwarten ziemlich sicher so ein Format um da sinnvoll mit Arbeiten zu können. Es reicht also nicht zu wissen das man CSV-Dateien in Stata/R einlesen kann, sondern die müssen auch in einer Form gespeichert sein, in der die Software damit etwas anfangen kann.

Der Fehler im Vorgehen hier ist IMHO das Du die Daten in ein Format umwandelst ohne Dir vorher angeschaut zu haben in welchem Format Du sie am Ende für die Weiterverarbeitung benötigst. CSV alleine ist nicht die Antwort auf diese Frage sondern da gehört auch dazu wie die Daten innerhalb der CSV-Datei angeordnet sind. Und die Antwort auf *diese* Frage hat auch damit zu tun in welcher Weise auf welche der Daten zugegriffen werden soll.

Und auch bei JSON ist nicht alleine die Frage ob das von Stata/R eingelesen werden kann, die Frage kann man sehr wahrscheinlich mit Ja beantworten, sondern die Daten innerhalb der Dateien organisiert sein müssen, damit man die gewünschten Abfragen leicht realisieren kann.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten