Textblöcke extrahieren?

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
xoxox
User
Beiträge: 12
Registriert: Dienstag 11. September 2007, 13:51

Hallo,

ich versuche Folgendes zu machen:

Gegeben sei eine eingelesene Text-Datei, welche komplett in einer Liste vorliegt. In diesem Text gibt es ein oder mehrere Textblöcke, die ich extrahieren möchte.

Jeder Textblock beginnt mit einen start_string und endet mit end_string.
Jeder start_string ist eindeutig dem gesuchten Textblock zugeordnet.
Der end_string beginnt immer mit demselben Text, endet aber unterschiedlich.
Der end_string kann auch an anderer Stelle im Text vorkommen.

Das Problem mit meinem Code ist das die letzte Zeile mit dem end_string nicht ausgegeben wird. Mir ist klar warum, aber ich sehe aufgrund der Text-Struktur keine andere Möglichkeit den end_string als Abbruchbedingung zu benutzen.

Habt ihr eine Idee, wie ich die letzte Zeile des Textblocks mit extrahieren kann, ohne die Abbruchbedingung von takewhile zu verändern?

Code: Alles auswählen

def extract_text(lines):
    def check_line(line):
        return not line.startswith(end_string)

    line_nums = (line_num for line_num,line in enumerate(lines) if start_string in line)

    for line_num in line_nums:
        for line in takewhile(check_line, iter(lines[line_num:])):
            yield line
BlackJack

Kannst Du ein Beispiel für die Eingabedaten geben, mit problematischen Fällen und wie die Ausgabe aussehen soll. So ganz klar ist mir das nämlich noch nicht.

Da Du mehrfach über Kopien von Teilen von `lines` iterierst, machen die itertools hier übrigens nicht so viel Sinn; da wäre eine Schleife und ein Flag, ob man sich gerade in einem Textblock befindet oder nicht, effizienter.
xoxox
User
Beiträge: 12
Registriert: Dienstag 11. September 2007, 13:51

Den Originaltext möchte ich nicht posten (sind Logs von diversen Steuergeräten, die nicht für die Öffentlichkeit bestimmt sind).

Aber prinzipiell sehen die Zeilen so aus:

...
Zeile mit irgendeinemText
Zeile mit irgendeinem Text
start_string
Zeile mit wichtigen Text
Zeile mit wichtigen Text
Zeile mit wichtigen Text
Zeile mit wichtigen Text
end_string
Zeile mit irgendeinem Text
Zeile mit irgendeinem Text
...

Extrahieren will ich:
start_string
Zeile mit wichtigen Text
Zeile mit wichtigen Text
Zeile mit wichtigen Text
Zeile mit wichtigen Text
end_string

Mit meinem Code komm halt raus:
start_string
Zeile mit wichtigen Text
Zeile mit wichtigen Text
Zeile mit wichtigen Text
Zeile mit wichtigen Text

Die Textblöcke die zu extrahieren sind, sind in der Regel so 20-25 Zeilen lang.
Da Du mehrfach über Kopien von Teilen von `lines` iterierst, machen die itertools hier übrigens nicht so viel Sinn;
Mmh, ich dachte ich iteriere komplett nur wo ich die Zeilennummern von start_string ermittle.
Die "takewhile-Schleife" bricht doch recht früh ab, sobald der end_string gefunden wurde?

Letzlich hatte ich auch mal ne klassische Variante mit ner While-Schleife.
Aber erstens sah der Code grausig aus und zweitens versuche ich gerade die itertools etwas näher kennen zu lernen.

Edit:
Hab noch etwas mit der While-Schleife gespielt und folgender Code sollte es eigentlich tun.
Der Vorteil der While-Schleife ist, dass man nach dem Abbruch im else-Teil noch weitermachen kann. Gibt es etwas Vergleichbares bei den itertools?

Code: Alles auswählen

def extract_text(lines):
    line_nums = (line_num for line_num,line in enumerate(lines) if start_string in line)

    for line_num in line_nums:
        while not lines[line_num].startswith(end_string):
            yield lines[line_num]
            line_num += 1
        else:
            yield lines[line_num]
Zuletzt geändert von xoxox am Montag 19. November 2007, 13:51, insgesamt 1-mal geändert.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Das ist jetzt nur mal kurz zusammengeschrieben und sicher nicht sehr stabil, aber vielleicht ein Ansatz:

Code: Alles auswählen

>>> text = ["bla", "blub", "blubber", "start", "eins", "zwei", "ende", "brumm", "start", "hallo", "ende", "spam"]
>>> def at_lines(text, sub):
	for index, elem in enumerate(text):
		if elem.startswith(sub):
			yield index

			
>>> y = zip(at_lines(text, "start"), at_lines(text, "ende"))
>>> y
[(3, 6), (8, 10)]
>>> for (start, stop) in y:
	print text[start:stop+1]

	
['start', 'eins', 'zwei', 'ende']
['start', 'hallo', 'ende']
>>> 
BlackJack

In den Beispieldaten sehe ich jetzt aber kein Problem mit der Endkennzeichnung!? Oder habe ich Dein erstes Posting falsch verstanden?

Du brichst mit dem `takewhile()` zwar ab bevor das Ende von den Listen erreicht ist, aber die Kopien werden vorher ja trotzdem bis zu Ende erstellt.

Die klassische Variante ist nicht mir einer ``while``-Schleife, sondern eine ``for``-Schleife, die *einmal* über die Eingabe geht und ein Flag benutzt um sich zu merken ob die aktuelle Zeile in einem Textblock ist, oder nicht:

Code: Alles auswählen

def extract_text(lines, start_string, end_string):
    in_block = False
    for line in lines:
        if start_string in line:
            in_block = True
        if in_block:
            yield line
        if line.startswith(end_string):
            in_block = False
Einfacher dürfte das mit `itertools` auch nicht werden. Eher komplexer.

Und wenn Du Dich mit Generatoren und `itertools` beschäftigen willst, solltest Du Code auch versuchen so zu schreiben, das man nicht die gesamte Eingabe auf einen Schlag benötigt, sondern immer nur den Teil den man wirklich gerade braucht. Bei Deinen Funktionen muss `lines` komplett in den Speicher gelesen werden. Bei der in diesem Beitrag wird immer nur eine Zeile verarbeitet. Wieviel davon im Speicher gehalten wird, entscheidet der Code, der diese Funktion benutzt.
xoxox
User
Beiträge: 12
Registriert: Dienstag 11. September 2007, 13:51

@EyDu
Du vergisst, dass der end_string auch unabhängig von start_string auftreten kann. Also als wenn in deinem Beispiel noch irgendwie ein zusätzliches "ende" wäre.

@Blackjack
Gefällt mir deine Lösung.
Hab mich wohl etwas in die itertools versteift, aber es ist nicht leicht als Anfänger den gerade passigen Lösungsansatz zu wählen.
Du brichst mit dem `takewhile()` zwar ab bevor das Ende von den Listen erreicht ist, aber die Kopien werden vorher ja trotzdem bis zu Ende erstellt.
Das verstehe ich jetzt nicht.
Die Doku sagt zu takewhile: Make an iterator that returns elements from the iterable as long as the predicate is true.

Sobald die Zeile mit end_string bei mir erreicht ist, wird die Bedingung false und die vorherigen Elemente werden ausgegeben. Aber eben nicht die Zeile in der die Abbruchbedingung steht (end_string). Was meinst du mit "die Kopien werden vorher ja trotzdem bis zu Ende erstellt."=

Den Text hab ich komplett eingelesen, da ich noch ein paar andere Sachen extrahieren möchte. Ich müsste ja sonst für jeden Parser einen neuen File-Iterator machen, und es würde jedesmal auf die Festplatte zugegriffen?
Da die Dateien teilweise auf Netzlaufwerken liegen, halte ich es für sinnvoller einmalig den Text in den lokalen Ram einzulesen (Text-Größe << Ram)
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

xoxox hat geschrieben:Du vergisst, dass der end_string auch unabhängig von start_string auftreten kann. Also als wenn in deinem Beispiel noch irgendwie ein zusätzliches "ende" wäre.
Das war mit der noch nicht ganz stabilen Lösung gemeint.
BlackJack

@xoxox: Du hast da ``takewhile(check_line, iter(lines[line_num:]))`` stehen und was als allererstes passiert ist, dass ``lines[line_num:]`` eine (flache) Kopie von der Zeilennummer bis zum Ende erstellt. Und das für jeden Block. Die Listen werden zwar immer kleiner, aber wie gesagt benutzt man die `itertools` für gewöhnlich um solche Kopien bzw. komplette Listen zu vermeiden.

Das `iter()` ist übrigens überflüssig weil Listen selbst schon "iterable" sind.

Und auch wenn man alles komplett in den Speicher lädt, können Iteratoren Speicher sparen.
Antworten