Textdatei einlesen und in dict ablegen

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
tvtue
User
Beiträge: 4
Registriert: Dienstag 29. Mai 2012, 07:53

Hallo zusammen,

was wäre der eleganteste Weg eine Textdatei in ein dict einzulesen, wobei der Inhalt der Textdatei einem bestimmten Muster. Die Textdatei ist die Ausgabe von linklint bzw ein File der Ausgabe von linklint und sieht so aus:

Code: Alles auswählen

/path/to/bla/bla1
     used in 3 files:
     /path/to/other/file1
     /path/to/other/file2
     /path/to/other/file3

/path/to/bla/bla2
     used in 2 files:
     /path/to/other/file4
     /path/to/other/file5
Eine for Schleife über alle Zeilen mit einer regex die auf die entsprechenden Zeilenanfänge matched habe ich schon implementiert. Das scheint auch zu funktionieren, ich frage mich nur, ob es nicht einen eleganteren Weg gibt.

Gruß
tvtue
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Es wäre hilfreich, wenn du noch erwähnen würdest, was in dem dict die Schlüssel und was die Werte sein sollen.

Unter der Annahme, dass du zu jedem Schlüssel eine Liste von Werten ablegen willst, ist http://docs.python.org/library/collecti ... efaultdict eine praktische Variante von dict, wo man sagen definieren kann, dass jeder neue Eintrag automatisch einen Standardwert hat, also zum Beispiel eine leere Liste. Möglicherweise brauchst du das aber nicht, denn wenn die eingerückten Pfade die Werte sein sollen, kannst du diese ja immer selbst zu Listen zusammenfassen.

Vielleicht so:

Code: Alles auswählen

import re

links, key, paths = {}, None, None

for line in lines:
	if re.match(r"^\s*$"):
		continue
	if re.match(r"^\S"):
		if key:
			links[key] = paths
		key = line
		paths = []
		continue
	if re.match(r"^\s+used in.*:$"):
		continue
	paths.append(line)

if key:
	links[key] = paths
Stefan
BlackJack

@tvtue: Was testet denn der reguläre Ausdruck was man nicht auch ohne ihn testen könnte? Ich sehe drei Zeilentypen: Leerzeilen, Zeilen die mit „whitespace” anfangen, und den Rest. Das kann man auch ohne `re` auseinander halten.

Ich würde mit Generatoren und Iteratoren arbeiten. Im ersten Schritt die Leerzeilen ausfiltern. Dann in einem Generator die Abschnitte markieren und mit `itertools.groupby()` die gruppieren. Und die kann man dann verarbeiten.
tvtue
User
Beiträge: 4
Registriert: Dienstag 29. Mai 2012, 07:53

Hey, danke für eure schnellen Antworten!

In der Tat hätte ich noch mehr Infos dazu geben können, sorry!
Trotzdem habt ihr es kapiert. :)
Ja, die Zeile ohne Whitespace, die mit /path anfängt, soll der Key sein.
Und ja, die Zeilen, die nach der "used in" Zeile kommen, sollen als Array bei dem Key im dict stehen.

Mein Code dafür lautet so:

Code: Alles auswählen

#!/usr/bin/env python
# *-* coding: UTF-8 *-*
"""
~/tmp/linklint/linkdoc/errorAX_tvtue.txt

format:

/wiki/official/advancedtopics-adaptative#id2656141
    used in 1 file:
    /wiki/official/part-advancedtopics

/wiki/official/advancedtopics-adaptative#id2656160
    used in 1 file:
    /wiki/official/part-advancedtopics

- im dict soll der link der key sein und die vorkommnisse ein array
- andersrum wäre es ja besser, dann bräuchte ich jede url nur einmal zu editieren
"""

import os, sys, re

textfile = os.path.expanduser('~/tmp/linklint/linkdoc/errorAX_tvtue.txt')

file = open(textfile,'r')

dict = {}

for line in file:
   if re.match('^\/', line):
      key = line.rstrip()
   elif re.match('^    used in', line):
      pass
   elif re.match('^    /wiki/', line):
      val = line.strip()
      if dict.has_key(key):
         dict[key].append(val)
      else:
         dict[key] = [ val ]

# kontrollausgabe zu testzwecken
for key,val in dict.iteritems():
   print key, " -> ", val
   print
Wie gesagt, ich vielleicht gibts was eleganteres als diese Lösung. Wobei Deine @sma Regex gefallen mir besser.

In meinem Kommentar im Code hab ich noch angedeutet, daß ich das ganze noch andersrum besser finde. Der Sinn des ganzen ist es später ein Wiki automatisch zu editieren. Daher wäre es besser als Keys die Vorkommnisse (also die Zeilen nach "used in") als Key zu verwenden und als Value den ersten Pfad ohne die Leerzeichen. Dazu hab ich mir aber noch kein Gedanken gemacht. Als Anfänger wollte erst mal überhaupt einen Weg finden. :) Entweder muß ich dann komplett anders einlesen oder das dict umsortieren bzw die Werte anderes in zweites dict reinschreiben.

@BlackJack: Generatoren und Iteratoren sind mir nicht bekannt. Da muß ich mich erst mal ein wenig einlesen.
Zuletzt geändert von Anonymous am Dienstag 29. Mai 2012, 19:19, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

So, ich habe das mal in Richtung von dem, was BlackJack angeregt hatte, versucht umzusetzen: Link

Ich vermute das geht alles noch kompakter :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@tvtue: Du magst reguläre Ausdrücke wohl. ;-) Ein '^' ist bei `match()` nicht nötig, denn das ist implizit. Und alle drei Fälle wären auch ohne `re` gegangen, mit der `startswith()`-Methode auf Zeichenketten.

`dict.has_key()` ist „deprecated”, dafür benutzt man den ``in``-Operator. Mit einem `collections.defaultdict()` könnte man sich das ``if``/``else`` an der Stelle komplett sparen.

Mein Ansatz hätte so ausgesehen:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from itertools import groupby, imap
from operator import itemgetter
from pprint import pprint


LINES = '''\
/path/to/bla/bla1
     used in 3 files:
     /path/to/other/file1
     /path/to/other/file2
     /path/to/other/file3
    
/path/to/bla/bla2
     used in 2 files:
     /path/to/other/file4
     /path/to/other/file5
'''.splitlines(True)


def iter_groups(lines):
    def stamp():
        i = 0
        for line in lines:
            if not line[0].isspace():
                i += 1
            yield (i, line)
    
    for i, group in groupby(stamp(), itemgetter(0)):
        result = imap(itemgetter(1), group)
        if i == 0:
            raise ValueError(
                'junk before first group detected:\n' + '\n'.join(result)
            )
        yield result


def _extract_file_count(line):
    try:
        used, in_, i, files = line.split()
        if (used, in_, files) != ('used', 'in', 'files:'):
            raise ValueError
        return int(i)
    except ValueError:
        raise ValueError(
            'expected "used in ... files:" line, got %r instead' % line
        )


def main():
    lines = (line.rstrip() for line in LINES if line.strip())
    
    result = dict()
    for line_group in iter_groups(lines):
        key = next(line_group)
        value_count = _extract_file_count(next(line_group))
        values = [s.strip() for s in line_group]
        if value_count != len(values):
            raise ValueError(
                'expected %d filenames for %r, got %d instead' % (
                    value_count, key, len(values)
                )
            )
        result[key] = values
    
    pprint(result)


if __name__ == '__main__':
    main()
tvtue
User
Beiträge: 4
Registriert: Dienstag 29. Mai 2012, 07:53

Wow, cool!

Danke euch allen, das muß ich erst mal schlucken und verdauen. :)

@BlackJack: ja, ich bin Regex Fan :) , aber ich lasse mich auch gerne auf Neues ein.
Gut zu wissen, daß mit der has_key() Funktion.

So, nun muß ich noch ein bisschen weiter verdauen ;)

Danke !
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

@BlackJack: Den Trick mit dem Index-Counter zum Markieren der Gruppen muss ich mir dringend merken :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
tvtue
User
Beiträge: 4
Registriert: Dienstag 29. Mai 2012, 07:53

@BlackJack bin immer noch am verdauen ;)

Es hapert ja schon an der ersten Zeile in main(). Würdest Du die bitte mal näher erläutern?

Code: Alles auswählen

lines = (line.rstrip() for line in LINES if line.strip())
Mit den runden Klammern ganz außen wird das Generator Objekt erzeugt. Das in den Klammern würde ich eher für Perl halten. *duck* Von dort kenne ich sowas wie print $var if $bedingung; Aber um sowas handelt es sich wohl nicht, oder? Der for Schleife fehlt ein Doppelpunkt. Wieso darf man den weglassen und zwischen dem rstrip() und dem for sucht mein Hirn irgendwie einen Trenner, Semikolon oder sowas. ;) In welcher Reihenfolge werden die drei Ausdrücke innerhalb der Klammern denn nun ausgeführt? Lauter Fragezeichen.
Benutzeravatar
/me
User
Beiträge: 3561
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

tvtue hat geschrieben:

Code: Alles auswählen

lines = (line.rstrip() for line in LINES if line.strip())
In welcher Reihenfolge werden die drei Ausdrücke innerhalb der Klammern denn nun ausgeführt? Lauter Fragezeichen.
Man kann es eigentlich direkt lesen:
Liefere den Wert line.rstrip() für jedes Element line aus dem Iterable LINES wenn die Zeile überhaupt Inhalt hat.

Um das "überhaupt Inhalt" zu erläutern: line.strip() liefert einen leeren String wenn nur Whitespaces in line enthalten sind.

Schau dir einfach mal List Comprehensions im Tutorial an. Im Prinzip ist die Syntax die gleiche wie bei einem Generator.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

tvtue hat geschrieben:

Code: Alles auswählen

lines = (line.rstrip() for line in LINES if line.strip())
Das ist eine Kurzform für

Code: Alles auswählen

def einzeiler():
    for line in LINES:
        if line.strip():
            yield line.rstrip()
lines = einzeiler()
Stünden da eckige statt runder Klammern, wäre es vielleicht einfacher zu verstehen:

Code: Alles auswählen

lines = []
for line in LINES:
    if line.strip():
        lines.append(line.rstrip())
Stefan
Antworten