Zeilen in einer Datei löschen über List-Comprehension

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.
sfx2k
User
Beiträge: 54
Registriert: Dienstag 2. September 2014, 13:29

Hallo,

meine Ziel ist es, aus einer Datei alle Zeilen zu löschen, die mindestens ein Wort aus einer festgelegten Liste enthalten.
Ich weiß nicht, ob das der korrekte Weg für mein Vorhaben ist, aber ich lese die Datei mit readlines() in eine Liste ein, und erzeuge mittels List-Comprehension eine neue Liste der Zeilen, die die Begriffe nicht enthalten.

Das funktioniert auch halbwegs

Code: Alles auswählen

to_delete = ['ich', 'du', 'er', 'sie', 'es']
fo = open('C:\\Download\\testdatei.txt', 'r')
lines = fo.readlines()
fo.close()
for s in to_delete:
    lines = [x for x in lines if x.lower().find(s) == -1]
print(lines)
fo = open('C:\\Download\\testdatei.txt', 'w')
fo.writelines(lines)
fo.close()
Halbwegs deshalb, weil auch der Begriff 'nicht' gefunden wird, wenn ich nach 'ich' suche - ich möchte aber nur nach ganzen Wörtern suchen.
Wäre hier die Suche über Reguläre Ausdrücke sinnvoller, als in x oder x.find() ?

Oder ist der Weg über die Listen vielleicht gar nicht der Optimale?
BlackJack

@sfx2k: Wenn Du nur ganze Worte testen möchtest, dann musst Du die jeweilige Zeile in Worte zerlegen und dann prüfen. Oder mit regulären Ausdrücken arbeiten die sicherstellen das nur ganze Worte verglichen werden.

Die Reihenfolge der Verarbeitung ist ungünstig. Du hast eine kurze Liste mit Worten die eine feste Länge hat, und eine sehr wahrscheinlich längere Liste mit Zeilen die Du a) komplett in den Speicher liest, b) für jedes Wort erneut durchgehst und einiges mit der Zeile anstellst um zu testen ob das Wort enthalten ist. Anders herum wäre es sicher effizienter: Nur einmal über die Zeilen in der Datei iterieren und für jede Zeile testen ob eines der Worte enthalten ist. So könnte man sich am Ende mit Hilfe einer temporären Datei sogar das komplette einlesen aller Zeilen auf einen Schlag in den Arbeitsspeicher ersparen.
sfx2k
User
Beiträge: 54
Registriert: Dienstag 2. September 2014, 13:29

@Blackjack:
Danke für Deine Antwort. Ich werde hinsichtlich der Suche dann mal optimieren.
Ist es denn zielführend, das so zu machen, also über eine Liste, oder gibt es bessere Wege?

Und was meinst Du mit
BlackJack hat geschrieben:So könnte man sich am Ende mit Hilfe einer temporären Datei sogar das komplette einlesen aller Zeilen auf einen Schlag in den Arbeitsspeicher ersparen.
Sirius3
User
Beiträge: 17746
Registriert: Sonntag 21. Oktober 2012, 17:20

@sfx2k: Probleme, wie filtere alle Zeilen einer Datei nach einem bestimmten Kriterium gibt es ja häufiger:

Code: Alles auswählen

INPUT_FILE = r'C:\Download\testdatei.txt'
OUTPUT_FILE = r'C:\Download\testdatei.txt'

def some_filter(line):
    return len(line)%2 == 0

def main():
    with open(INPUT_FILE, 'r') as input_lines:
        with open(OUTPUT_FILE, 'w') as output_file:
            for line in input_lines:
                if some_filter(line):
                    ouput_file.write(line)

if __name__ == '__main__':
    main()
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Zielfuehrend ist es natuerlich: Du hast am Ende alle Zeilen die erhalten bleiben sollen.
Wenn du aber eigentlich nur eine neue Datei in derselben Reihenfolge schreiben willst und gar nicht alle neuen Zeilen im Speicher brauchst, ist es Overkill.
Da kommt der "bessere" Weg ins Spiel, den du nicht verstanden hast ;)

Code: Alles auswählen

with open('pruned', 'w') as pruned:
    with open('origin') as origin:
      for line in origin:
          parts = line.split()
          if not any(w in parts for w in to_delete):
              pruned.write(line)
Oder eben mit einer temporaeren Datei (siehe `tempfile` Modul), wenn du zwar keine neue Datei brauchst, aber auch nur einen sequenziellen bzw zeilenweisen Zugriff brauchst.
nezzcarth
User
Beiträge: 1634
Registriert: Samstag 16. April 2011, 12:47

Den Ausgangsansatz könnte man vielleicht so ähnlich schreiben wie dies:

(python3)

Code: Alles auswählen

def main():
    to_delete = {'ich', 'du', 'er', 'sie', 'es'}
    with open('test_f.txt') as f:
        result = [line.strip() for line in f.readlines() if not any(map(lambda x: x in to_delete, line.split())) ]
    print(result)

if __name__ == '__main__':
    main()
Du hattest ja von Listcomprehensions gesprochen, daher auf diese Weise; zwei For-Schleifen wären vielleicht einfacher zu überblicken. In jedem Fall würde man statt der Liste eine Menge nehmen, und schauen, dass man das Einlesen in einem with unterbringt.
BlackJack

@nezzcarth: Da würde ich statt des `map()` + ``lambda`` einen Generatorausdruck schreiben. Das ist nur ein Zeichen mehr zu tippen wenn man bei einem einbuchstabigen Namen für das Wort bleibt, und IMHO einfacher zu lesen.

Und das ``f.read().split('\n')`` ist quatsch. Warum nicht einfach nur ``f`` an der Stelle? Wegen der Zeilenenden?
nezzcarth
User
Beiträge: 1634
Registriert: Samstag 16. April 2011, 12:47

@BlackJack:
Ja, du hast recht, danke für den Hinweis. Mit deinen Vorschlägen sähe es dann vielleicht so aus:

Code: Alles auswählen

def main():
    to_delete = {'ich', 'du', 'er', 'sie', 'es'}
    with open('test_f.txt') as f:
        result = [line.strip() for line in f if not any(item in to_delete for item in line.split())]
    print(result)

if __name__ == '__main__':
    main()
sfx2k
User
Beiträge: 54
Registriert: Dienstag 2. September 2014, 13:29

@ BlackJack, Sirius3, cofi und nezzcarth:
Besten Dank für Eure Hinweise.

Der genannte 'bessere' Weg scheint mir dann doch am sinnvollsten für mein Vorhaben :)
sfx2k
User
Beiträge: 54
Registriert: Dienstag 2. September 2014, 13:29

Nur zur Info; so sieht es jetzt aus:

Code: Alles auswählen

# ******************************************************************************

INPUT_FILE = r'C:\Download\testdatei - Kopie.txt'
OUTPUT_FILE = r'C:\Download\new.txt'

# ******************************************************************************

def filter(line):

    to_delete = ['ich', 'du', 'er', 'sie', 'es']
    parts = line.lower().split()
    return not any(w in parts for w in to_delete)

# ******************************************************************************

def main():
    with open(INPUT_FILE, 'r') as input_lines:
        with open(OUTPUT_FILE, 'w') as output_file:
            for line in input_lines:
                if filter(line):
                    output_file.write(line)

# ******************************************************************************

if __name__ == '__main__':
    main()
nezzcarth
User
Beiträge: 1634
Registriert: Samstag 16. April 2011, 12:47

Meiner Ansicht nach wäre der passende Datentyp für 'to_delete' ein Set, keine Liste, weil du keine Sortierung oder doppelte Einträge brauchst. Weiterhin würde ich diese 'Blacklist' nicht in die Funktion aufnehmen, sondern als Parameter in die Signatur schreiben; die spezifische Ausgestaltung kannst du ja als Konstante auf Modulebene setzen und übergeben, das wäre denke ich sauberer. Filter ist auch kein guter Name, weil es schon ein gleichnamiges Builtin gibt.
mcdwerner
User
Beiträge: 113
Registriert: Donnerstag 7. Juli 2011, 14:27

@sfx2k:
bin neugierig 8)
funktioniert dein filter() mit der Zeile
"mich freut es, dass wir hier sind"
so wie von Dir gewünscht?
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Man kann sich auch eine Einrückungsebene noch sparen:

Code: Alles auswählen

In [13]: cat foo
Hallo
Welt!
In [14]: with open("foo") as infile, open("bar", "w") as outfile:
   ....:     outfile.write(infile.read())
   ....:     

In [15]: cat bar
Hallo
Welt!
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
sfx2k
User
Beiträge: 54
Registriert: Dienstag 2. September 2014, 13:29

Hallo Ihr drei,

vielen Dank für Eure Antworten.
nezzcarth hat geschrieben: Meiner Ansicht nach wäre der passende Datentyp für 'to_delete' ein Set, keine Liste, weil du keine Sortierung oder doppelte Einträge brauchst. Weiterhin würde ich diese 'Blacklist' nicht in die Funktion aufnehmen, sondern als Parameter in die Signatur schreiben; die spezifische Ausgestaltung kannst du ja als Konstante auf Modulebene setzen und übergeben, das wäre denke ich sauberer. Filter ist auch kein guter Name, weil es schon ein gleichnamiges Builtin gibt.
Habe Deine Vorschläge so umgesetzt
mcdwerner hat geschrieben: @sfx2k:
bin neugierig 8)
funktioniert dein filter() mit der Zeile
"mich freut es, dass wir hier sind"
so wie von Dir gewünscht?
Hehe, nein - natürlich nicht :)
Ich bin daher mal auf Reguläre Ausdrücke umgestiegen.
Hyperion hat geschrieben:Man kann sich auch eine Einrückungsebene noch sparen:
Ok, gespart :)

Code: Alles auswählen

import re

# ******************************************************************************

INPUT_FILE = r'C:\Download\testdatei - Kopie.txt'
OUTPUT_FILE = r'C:\Download\testdatei - Kopie_neu.txt'
TO_DELETE = set(['ich', 'du', 'er', 'sie', 'es'])

# ******************************************************************************

def some_filter(line, args):

    result = True

    for arg in args:
        if re.search('{1}{0}{1}'.format(arg, r'\b'), line, re.I):
           result = False
           break

    return result

# ******************************************************************************

def main():
    with open(INPUT_FILE, 'r') as input_lines, open(OUTPUT_FILE, 'w') as \
        output_file:
        for line in input_lines:
            if some_filter(line, TO_DELETE):
                output_file.write(line)

# ******************************************************************************

if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 17746
Registriert: Sonntag 21. Oktober 2012, 17:20

@sfx2k: So baut man keine reguläre Ausdrücke zusammen. Wenn Du irgendwann nach Strings suchst, die Sonderzeichen enthalten, dann funktioniert die Filterfunktion nicht mehr wie gewünscht, und der Fehler ist nicht schwer zu finden.

Deshalb:

Code: Alles auswählen

def some_filter(line, args):
    return not re.search(r'\b({})\b'.format('|'.join(map(re.escape, args))), line. re.I)
BlackJack

@sfx2k: Hier macht das `set` nun aber wieder keinen Sinn mehr. Da wäre eine Liste angebrachter weil das eine einfacherere Datenstruktur ist und Du von der Funktionalität vom `set` nichts verwendest. ;-)
BlackJack

Noch eine Variante das Ganze aufzuteilen. Eine Funktion zum Lesen, eine die eine einzelne Zeile prüft, und eine zum Schreiben des Ergebnis, und dann noch eine Hauptfunktion welche die drei Teile kombiniert:

Code: Alles auswählen

#!/usr/bin/env python
import re
from itertools import ifilterfalse


def iter_lines(filename):
    with open(filename, 'r') as lines:
        for line in lines:
            yield line


def create_line_test(words):
    return re.compile(
        r'\b({0})\b'.format('|'.join(re.escape(word) for word in words)),
        re.IGNORECASE
    ).search


def save_lines(filename, lines):
    with open(filename, 'w') as out_file:
        out_file.writelines(lines)


def main():
    to_delete = ['ich', 'du', 'er', 'sie', 'es']
    save_lines(
        'test2.txt',
        ifilterfalse(create_line_test(to_delete), iter_lines('test.txt'))
    )


if __name__ == '__main__':
    main()
sfx2k
User
Beiträge: 54
Registriert: Dienstag 2. September 2014, 13:29

Sirius3 hat geschrieben:

Code: Alles auswählen

def some_filter(line, args):
    return not re.search(r'\b({})\b'.format('|'.join(map(re.escape, args))), line. re.I)
Ha, das gefällt mir ausgezeichnet. Besonders die Möglichkeit, automatisch escapen zu lassen :)
BlackJack hat geschrieben:@sfx2k: Hier macht das `set` nun aber wieder keinen Sinn mehr. Da wäre eine Liste angebrachter weil das eine einfacherere Datenstruktur ist und Du von der Funktionalität vom `set` nichts verwendest. ;-)
Ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhh :wink:
Okay, dann erkläre mir doch bitte, warum drei Posts vorher laut nezzcarth noch ein Set besser geeignet war als eine Liste, da ich deren Funktionalitäten gar nicht benötige, und jetzt genau andersrum? :?

Bezüglich Deines Quelltextes:
In Delphi hätte ich alles einzeln gemacht. Diese vielen verschachtelten Aufrufe - da muss ich erstmal mit klarkommen.
Besonders das ifilterfalse ist mir noch nicht ganz klar. Naja - mal das Doc dazu lesen :)

Sirius3 benutzt zum Zusammensetzen des Regulären Ausdrucks die map() Funktion, Du eine for-Schleife.
Gibt es da für diesen Fall signifikante Unterschiede? Oder ist das einfach nur Geschmackssache?

Und dann noch eine grundlegende Frage, auf die ich aber keine Antwort finden konnte:
Muss ich, wenn ein File in einer with-Anweisung geöffnet wird, dieses nicht auch wieder schließen?
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

sfx2k hat geschrieben: Ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhh :wink:
Okay, dann erkläre mir doch bitte, warum drei Posts vorher laut nezzcarth noch ein Set besser geeignet war als eine Liste, da ich deren Funktionalitäten gar nicht benötige, und jetzt genau andersrum? :?
Weil Du da noch *ohne* reguläre Ausdrücke geprüft hast, ob ein Wort in einer *Menge* vorkommt. Du hattest eine Liste genommen, die beim ``in`` Operator jedes Mal schlimmsten Falls komplett durchlaufen werden muss (das beschreibt man auch mit O(n)). Bei einer Menge kann der ``in`` Operator in *konstanter* Zeit (O(1)) ermitteln, ob ein Element in einer Liste ist.

Das kann man in einer Python-Shell (hier iPython) leicht nachprüfen:

Code: Alles auswählen

In [1]: values = list(range(10000000))

In [3]: some_value = 9999999

In [4]: timeit some_value in values
1 loops, best of 3: 305 ms per loop

In [5]: values = set(range(10000000))

In [6]: timeit some_value in values
10000000 loops, best of 3: 147 ns per loop
Beachte die Dauer - Millisekunden vs Nanosekunden! Meine Testzahl ist natürlich so schlecht wie möglich für die Liste gewählt, da sie dort die *letzte* Position hat. ``in`` muss also *alle* Einträge durchlaufen, bevor er auf die gewünschte Zahl trifft.

Auch eine nicht vorhandene Zahl muss natürlich immer alle Elemente durchlaufen.

Bei Sammlungen, die auf Hashing basieren (Sets, Dictionaries), kann dies *unabhängig* von der Größe der Sammlung immer in konstanter Zeit erreicht werden.

Edit: Noch einmal zur Verdeutlichung eine Reduktion der Anzahl um den Faktor 10 also von 10 Millionen auf eine Million Elemente. Man erkennt schön, wie der Listen basierte Zugriff sich *linear* um den Faktor reduziert (30ms vs 300ms), wohingegen der Mengen basierte Ansatz gleich geblieben ist:

Code: Alles auswählen

In [14]: some_value = 999999

In [15]: values = list(range(1000000))

In [16]: timeit some_value in values
10 loops, best of 3: 30.6 ms per loop

In [17]: values = set(range(1000000))

In [18]: timeit some_value in values
10000000 loops, best of 3: 145 ns per loop
sfx2k hat geschrieben: Muss ich, wenn ein File in einer with-Anweisung geöffnet wird, dieses nicht auch wieder schließen?
Nein, eben nicht! :-) (Das ist ja das tolle; das passiert übrigens auch bei Ausnahmen!)
Zuletzt geändert von Hyperion am Donnerstag 18. September 2014, 09:01, insgesamt 1-mal geändert.
Grund: Vergleichswerte ergänzt
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@sfx2k: Drei Posts vorher hat ein `set` noch etwas gebracht weil der ``in``-Operator bei Listen die Liste linear, Element für Element, vergleicht, und ein `set()` diese Anfrage ”sofort” beantworten kann, ohne sich alle enthaltenen Element einzeln anschauen zu müssen. Wenn man nur über die Elemente itertiert, dann verwendet man diese Eigenschaft von `set` nicht. Dann könnte man nur noch argumentieren, dass man sicherstellen möchte, dass die Testworte alle unterschiedlich sind, was man bei den wenigen Worten noch ganz gut ohne Unterstützung vom Programm hinbekommt.

Man kann es auch unverschachtelt(er) schreiben, wenn man sich Namen für die Zwischenergebnisse ausdenkt.

Code: Alles auswählen

def main():
    to_delete = ['ich', 'du', 'er', 'sie', 'es']
    lines = iter_lines('test.txt')
    filtered_lines = ifilterfalse(create_line_test(to_delete), lines)
    save_lines('test2.txt', filtered_lines)
Da gute Namen finden, in der Regel schwieriger ist als den Code an sich zu schreiben, spare ich mir das gerne. :-)

Bei den regülären Ausdrücken benutze ich einen Generatorausdruck. Da hätte ich in der Tat `map()` oder `itertools.imap()` verwenden können.

Die ``with``-Anweisung sorgt dafür das egal wo und wie der Programmfluss den Block verlässt, die `__exit__()`-Methode auf dem Objekt aufgerufen wird das nach dem ``with``-Schlüsselwort erzeugt wird. Also in diesem Fall die Datei. Und bei Dateien ist eine `__exit__()`-Methode implementiert, welche die Datei schliesst. Das ist aber auch *das* Beispiel für die ``with``-Anweisung. Da sollte man eigentlich etwas zu finden.
Antworten