Daten automatisch Gruppieren, in dicts und lists

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
ruiin
User
Beiträge: 20
Registriert: Mittwoch 11. April 2012, 16:31
Wohnort: Essen

Hi,

ich schreibe mir grad ein Progrämmchen das mir eine conf-Datei auslesen soll. Diese Datei ist in Abschnitte untergliedert (ähnlich wie in HTML head und body) deren Information ich später seperat ansteuern will.

Code: Alles auswählen

#conf
head
version=1
programm=bla.py
/head
body
name=irgendwas
infoA=a
infoB=b
/body
Ich lese dazu erst einmal die Datei zeilenweise ein und versuche nun die Daten neu zu struckturieren. So soll die head-Sektion im Dict mit gleichem Bezeichner landen, also den Bezeichner für die head-Sektion kann ich hardcoden. Doch will ich bei der body-Sektion, das dieses Dict während der Laufzeit einen Bezeichner erhält. Es kann in der Conf-Datei vorkommen das es einfach viele body-Sektionen gibt, und jede davon soll ihr eigenes dict bekommen.

bis jetzt würde ich das so machen:

Code: Alles auswählen

for i in lines:
	if i.startswith("#"):
		pass
	elif i.startswith("head"):
		headb = True
	elif i.startswith("version") and headb:
		head = { "version" : i.split("=", 1)[1] }
	elif i.startswith("programm" and headb:
		head = { "programm" : i.split("=", 1)[1] }
	#...
	elif i.startswith("body"):
		#... 
			
Doch ich hab keine Ahnung wie ich einen Bezeichnernamen generieren kann. Oder Soll ich eine Liste erstellen und mit einem counter die Indizes immer weiter hochzählen?

Währ toll wenn jemand da was besseres weis. :?
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

ruiin hat geschrieben:Doch ich hab keine Ahnung wie ich einen Bezeichnernamen generieren kann. Oder Soll ich eine Liste erstellen und mit einem counter die Indizes immer weiter hochzählen?
NImm auf jeden Fall ne Liste. Durchnummerieren von Variablennamen ist eine ganz schlechte Idee.

Was meinst du mit dem Counter? Die Nummer eines Elements ergibt sich doch aus seiner Position in der Liste.
ruiin
User
Beiträge: 20
Registriert: Mittwoch 11. April 2012, 16:31
Wohnort: Essen

Mit counter meine ich die Position in der Liste

Code: Alles auswählen

counter = 0
liste = []
if True:
liste[counter] = X
counter = counter + 1
welche automatisch hochgezählt wird.
BlackJack

@ruiin: Das wird nicht funktionieren denn bei ``a_list[index] = something`` muss es ein Element an `index` schon geben. Du willst einfach an die Liste *anhängen*. Da brauchst Du keinen Zähler/Index für.
ruiin
User
Beiträge: 20
Registriert: Mittwoch 11. April 2012, 16:31
Wohnort: Essen

ich will die datei aber zeile für zeile abarbeiten. Also muss ich das dict unter list[index] mehrmals erreichen können in einem ablauf. Wenn ich die Information nur anhänge, muss ich ja für jede Information einen Platz in der Liste haben, dabei will ich doch unter jeden Platz ein dict mit den Infos.
BlackJack

@ruiin: Dazu brauchst Du nur einen fixen Index, nämlich den des letzten Elements in der Liste. Das wäre ``a_list[-1]``. Andererseits würde ich wohl eher erst alle notwendigen Informationen zusammen zu suchen und wenn der Abschnit zuende ist, erst der Liste hinzu zu fügen. Dann könnte man das Auslesen der Informationen aus einem Body-Abschnitt zum Beispiel in eine Funktion auslagern die nicht wissen muss was *danach* mit dem erstellten Wörterbuch geschieht.

Ich würde das wahrscheinlich sowieso etwas modularer gestalten als eine grosse Schleife über alle Zeilen mit vielen ``if``/``elif``-Zweigen und einem impliziten Zustandsautomaten.

Mal ein „Top-down”-Entwurf: `parse_config(lines)` in der Kommentarzeilen getrennt von der restlichen Verarbeitung ausgefiltert werden. Stichpunkte `itertools.ifilter()` oder einen Generatorausdruck.

Dann eine `parse_section()`-Funktion die alles zwischen ``section_name`` und ``/section_name`` parst und aus dem iterator konsumiert und den ``section_name`` und das Wörterbuch mit dem Inhalt liefert. Die kann man in der `parse_config()` dann aufrufen bis alles verarbeitet ist.
ruiin
User
Beiträge: 20
Registriert: Mittwoch 11. April 2012, 16:31
Wohnort: Essen

ich hab das jetzt so verstanden:

Code: Alles auswählen

def parse(list):
	dict = {}
	if x in list:
		dict = { item : x }
	#...
	return dict
	
for i in list:
	if "head" in i:
		a.append(parse(list))
aber dafür muss ich die liste mit den daten schon vorher in die abschnitte unterteilen oder?
BlackJack

@ruiin: Die `parse()`-Funktion für einen Abschnitt bekommt einen Iterator über Zeilen der garantiert mit einem Abschnitt beginnt. Dann musst Du die erste Zeile holen, dass ist der Anfangstag, also 'head' oder 'body' in Deinem Fall und dann solange Zeilen verarbeiten bis der Anfangstag mit einem '/' davor kommt. Dann kann man die Daten für den Abschnitt zurück geben.

Das ``if x in list:`` in Deiner Funktion verstehe ich nicht. Vor allem konsumiert das ja den kompletten Iterator, dann kann man danach nicht beim nächsten Abschnitt weiter machen wenn es sich bei `list` nicht um eine Sequenz handelt, sondern zum Beispiel um ein Dateiobjekt.

Ich meinte so etwas hier:

Code: Alles auswählen

from functools import partial
from itertools import takewhile
from pprint import pprint

TEST_DATA = """\
#conf
head
version=1
programm=bla.py
/head
body
name=irgendwas
infoA=a
infoB=b
/body

#nach dem kommentar noch ein body:
body
name=noch irgendwas
infoA=a2
infoB=b2
/body
""".splitlines(True)


def parse_section(lines):
    name = next(lines)
    end_name = '/' + name
    section_lines = takewhile(lambda s: s != end_name, lines)
    return name, dict(s.split('=', 1) for s in section_lines)


def parse_config(lines):
    lines = (s.strip() for s in lines if s.strip() and not s.startswith('#'))
    
    name, head_content = parse_section(lines)
    if name != 'head':
        raise ValueError('expected head section, got %r instead' % name)
    
    bodies = list()
    for name, body_content in iter(partial(parse_section, lines), object()):
        if name != 'body':
            raise ValueError('expected body section, got %r instead' % name)
        bodies.append(body_content)
    
    return {'head': head_content, 'bodies': bodies}


def main():
    pprint(parse_config(TEST_DATA))


if __name__ == '__main__':
    main()
Ausgabe:

Code: Alles auswählen

{'bodies': [{'infoA': 'a', 'infoB': 'b', 'name': 'irgendwas'},
            {'infoA': 'a2', 'infoB': 'b2', 'name': 'noch irgendwas'}],
 'head': {'programm': 'bla.py', 'version': '1'}}
Edit: Die beiden Parse-Funktionen noch einmal refaktorisiert:

Code: Alles auswählen

def parse_section(lines, expected_name):
    name = next(lines)
    if name != expected_name:
        raise ValueError(
            'expected %r section, got %r instead' % (expected_name, name)
        )
    end_name = '/' + name
    section_lines = takewhile(lambda s: s != end_name, lines)
    return dict(s.split('=', 1) for s in section_lines)


def parse_config(lines):
    lines = (s.strip() for s in lines if s.strip() and not s.startswith('#'))
    return {
        'head': parse_section(lines, 'head'),
        'bodies': list(iter(partial(parse_section, lines, 'body'), object())),
    }
ruiin
User
Beiträge: 20
Registriert: Mittwoch 11. April 2012, 16:31
Wohnort: Essen

Ich versuch das mal nachzu vollziehen:
die funktion parse_section() übernimmt die gesammte liste aus zeilen, welche von Kommentaren schon bereinigt sind. Nimmt die liste[0] mit der next() funktion. Generiert daraus den namen und den Schluss. Die takewhile() Funktion nimmt jede zeile die darauf folgt, solange die bedingung wahr ist. Die Funktion gibt den namen und das dict getrennt zurück.

Der name wird geprüft. Dann eine Liste für bodies erstellt.

Den nächsten Teil fände ich etwas umständlich. Ich kann ihn auch nicht nachvollziehen (was die iter(partial()) funktionsverkettung angeht). Doch währe es nicht einfacher die Ausgabe so zu gestalten das man jeden body als eigenständigen datensatz auf der ebene des head ansetzt, und bei der anwendung nur den head-Datensatz ausnimmt?
BlackJack

@ruiin: Die `parse_section()` bekommt keine Liste von Zeilen sondern einen Iterator über Zeilen und „verbraucht” die verarbeiteten Zeilen. Bei einem Iterator kommt man mit `next()` an das jeweils nächste Element. Da gibt es keinen wahlfreien Zugriff über Indizes. Und ein Element, dass man mit `next()` geholt hat, ist aus dem Iterator raus, das kann man man nicht noch einmal vom Iterator abfragen. Iteratoren sind Datenströme; Folgen von Elementen die man der Reihe nach verarbeiten kann.

Ab dem letzten Satz im ersten Absatz scheinst Du Dich auf die noch nicht refaktorisierten Parse-Funktionen zu beziehen. Und ich denke der letzte Absatz geht von der falschen Annahme aus, dass dort Listen verarbeitet werden. Im zweiten Teil ist der Head-Abschnitt schon verarbeitet, die entsprechenden Zeile sind dem Iterator schon entnommen, man kann und braucht ihn deshalb nicht mehr beachten.
BlackJack

Noch eine Variation, die vielleicht etwas einfacher ist als die mit dem `iter()`-Aufruf mit zwei Argumenten:

Code: Alles auswählen

from functools import partial
from itertools import chain, imap, repeat, takewhile
from pprint import pprint

TEST_DATA = """\
#conf
head
version=1
programm=bla.py
/head
body
name=irgendwas
infoA=a
infoB=b
/body

#nach dem kommentar noch ein body:
body
name=noch irgendwas
infoA=a2
infoB=b2
/body
""".splitlines(True)


def parse_section(lines, expected_name):
    lines = iter(lines)
    name = next(lines)
    if name != expected_name:
        raise ValueError(
            'expected %r section, got %r instead' % (expected_name, name)
        )
    end_name = '/' + name
    section_lines = takewhile(lambda s: s != end_name, lines)
    return dict(s.split('=', 1) for s in section_lines)


def parse_config(lines):
    lines = (s.strip() for s in lines if s.strip() and not s.startswith('#'))
    expected_section_names = chain(['head'], repeat('body'))
    sections = imap(partial(parse_section, lines), expected_section_names)
    return {'head': next(sections), 'bodies': list(sections)}


def main():
    pprint(parse_config(TEST_DATA))


if __name__ == '__main__':
    main()
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

basierend auf der Struktur der Conf-Daten kannst du (man) doch auch eine INI-Datei nehmen. Ähnliche Struktur, fertiges Python-Modul, was ootb alles kann...

Gruß, noisefloor
lunar

Oder JSON, dann gibt es sogar die Typkonvertierungen frei Haus.
ruiin
User
Beiträge: 20
Registriert: Mittwoch 11. April 2012, 16:31
Wohnort: Essen

Mit JSON hab ich mich noch nicht beschäftigt, aber nach dem ich das mal gegooglet hab weis ich jetzt warum die Transmission-conf so aussieht. Werd das mal ausprobieren.
Antworten