Seite 1 von 1

Probleme mit der Ausrichtung von Zellen mit Python docx

Verfasst: Freitag 18. März 2022, 22:17
von schweito
Hallo Python-Forum,

ich würde gerne aus einem Word-Dokument Daten extrahieren und diese Daten in einer Tabelle in einer bestimmten Anordnung ausgeben. Die Extraktion funktioniert auch ohne Probleme (mit Hilfe regulärer Ausdrücke) - mein Problem ist das spezifische Layout, dass meine Tabelle haben sollte. Ich werde es gleich noch mit Bildern und Code verdeutlichen, aber die grundlegende Idee ist die Tabelle wie folgt aufzubauen:

Tabellenkopf soll die Bezeichnungen :

Arbeitspakete , Bezeichnung und Jahr 1 - Jahr 3 beinhalten.

Die Spalten der Zeilen darunter sollten da nahtlos an die Ränder der Überschriften angrenzen mit der Besonderheit, dass die Jahre jeweils in vier Quartale pro Jahr unterteilt werden sollten.

Meine Idee war, zwei Tabellen zu benutzen, eine für die Überschriften und eine für die Daten. Leider ist das Alignment nicht korrekt und mir ist aktuell nicht klar wie ich das beheben kann.

Anbei der Code dann noch zwei Bilder - der aktuelle Zustand (Bild1) und wie es aussehen sollte (Bild2).

Code: Alles auswählen


import docx, docx2txt, re,os,time
from docx.shared import Inches

  
wp_ident =[]
wp_desc =[]

ident=''
desc=''


doc = docx.Document() 
doc.add_heading('Balkenplan', 1) 

text = docx2txt.process("C:/Users/korre/Documents/Tabelle.docx")
work_package=re.compile(r"(^[AP]{2}\d+\.+\d*\.*\d*.*)",re.MULTILINE)


for match in work_package.finditer(text):
     t2 = time.time()
     work_package = match.groups()
     line = str(work_package)
     ident = line.split(" ",1)[-1]
     desc = line.split(" ",1)[0]
     wp_ident.append(ident)
     wp_desc.append(desc)
     
    
table = doc.add_table(rows=1,cols=5,style='Table Grid')
#table.allow_autofit = True
heading_cells = table.rows[0].cells


heading_cells[0].text = 'Arbeitspakete'
heading_cells[1].text = 'Beschreibung'
heading_cells[2].text = '1.Jahr'
heading_cells[3].text = '2.Jahr'
heading_cells[4].text = '3.Jahr'
  
table2 = doc.add_table(rows=0,cols=14,style='Table Grid')
#table2.allow_autofit = True

print("Processing...\n")
print("[1] Parsing input file and creating table...")
for item,desc in zip(wp_ident,wp_desc):
    t3 = time.time()  
   
    row = table2.add_row().cells  
    row[1].text=str(item)
    row[0].text=str(desc)
    
print("[2] Adjusting cell widths...\n")   

t4 = time.time()
print("Summary:")            
print("[1] Parsing and creating rows (" + str(len(wp_desc)) + ")" + " elapsed time: {:.2f}s)".format(t3-t2))
print("[2] Adjusting Geometry and Spacing: " + "elapsed time: {:.2f}s)".format(t4-t3))
print("total: {:.2f}s)".format(t4-t2))      
doc.save('C:/Users/korre/Documents/test.docx') 
os.system('start test.docx')

Bild1: Aktuelles Aussehen (Alignment passt nicht / 4 Cells pro Jahr auch nicht):


https://ibb.co/WBrBHnf



Bild2: Zieldesign:


https://ibb.co/c8HckQd

Re: Probleme mit der Ausrichtung von Zellen mit Python docx

Verfasst: Samstag 19. März 2022, 12:04
von __blackjack__
@schweito: Eingerückt wird per Konvention vier Leerzeichen pro Ebene.

Üblich ist eine `import`-Anweisung pro Modul.

`Inches` wird importiert, aber nirgends verwendet.

Namen sollten keine kryptischen Abkürzungen enthalten, oder gar nur aus solchen bestehen. Wenn man `itendifier` meint, sollte man nicht nur `ident` schreiben und wenn bei `description` nicht nur `desc`. Warum haben die Listen einen Präfix der eine übliche Abkürzung für WordPress ist? Steht das für `work_package`? So etwas sollte man nicht raten müssen.

Nummerieren von Namen ist ebenfalls keine gute Idee. Da will man sich entweder sinnvollere Namen überlegen, oder gar keine einzelnen Werte sondern eine Datenstruktur verwenden. Oft eine Liste.

Warum fängt die Nummerierung überhaupt bei 2 an? Der Punkt im Programmablauf an dem `t2` in jedem Schleifendurchlauf bestimmt wird, ist auch unsinnig, denn es wird ja nur der allerletzte Wert verwendet. Das kann man dann auch *einmal* *nach* der Schleife ermitteln. Das gleiche gilt für `t3`.

`time.time()` ist nur bedingt geeignet für Zeitmessungen. `time.monotonic()` oder `time.perf_counter()` sind dafür geeignet/vorgesehen.

Namen sollten erst definiert werden wenn sie verwendet werden, und nicht am Anfang mal ”auf Vorrat”. Also `wp_ident` und `wp_desc` erst vor der Schleife in der die Daten in diese Listen eingetragen werden. Die Werte von `ident` und `desc` die am Anfang definiert werden, werden nirgends verwendet. Warum werden die also überhaupt definiert an der Stelle?

Das Zieldokument wird erstellt, bevor die Daten aus der Quelle extrahiert wurden. Das würde ich auch nicht machen. Es ist übersichtlicher die Arbeitsschritte tatsächlich nacheinander zu machen und nicht zu mischen. Dann liesse sich das auch einfacher in einzelne Funktionen aufteilen, wenn der Code zu umwangreich wird, oder man einzelne Schritte separat testbar machen möchte.

Die beiden Listen enthalten offensichtlich Daten, die elementweise zusammengehören. Die sollten also gar nicht erst in zwei verschiedenen Listen landen, sondern in *einer*. Zum Beispiel eine Liste mit Tupeln, die jeweils die beiden zusammengehörenden Informationen enthalten.

`work_package` ist kein guter Name für einen regulären Ausdruck, denn der repräsentiert ja nicht das Arbeitspaket selbst. *Dafür* wird der Name im *gleichen* Namensraum, dann ja auch noch mal verwendet. Namen sollten nicht im gleichen Kontext für verschiedene Dinge verwendet werden, das ist verwirrend.

`match.groups()` liefert ein Tupel welches im Programm in eine Zeichenkettendarstellung umgewandelt wird. Das ist falsch. Man operiert nicht mit Zeichenkettenoperationen auf der Darstellung eines Tupels herum. Warum wird dort nicht einfach das eine Element aus dem Tupel heraus geholt? Oder besser: warum ist der reguläre Ausdruck nicht gleich mit zwei Gruppen formuliert, so dass man das Ergebnis nicht an einem Leerzeichen trennen muss, um an die beiden Teilinformationen zu kommen?

Das ist zudem ineffizient geschrieben, die exakt gleiche `split()`-Operation zweimal zu machen, um einmal an den Teil vor dem Leerzeichen und dann an den Teil nach dem Leerzeichen zu kommen. Üblicherweise würde man hier auch `partition()` statt `split()` verwenden.

Der reguläre Ausdruck ist ein bisschen merkwürdig. Dir ist klar, dass der am Anfang nicht nur "AP" erlaubt, sondern auch "AA", "PP", und "PA"? Ist das echt so gewollt, oder soll da nur "AP" für "Arbeitspaket" erkannt werden‽

Dann sind zwischen den Zifferngruppen auch mehrere aufeinanderfolgende Punkte erlaubt. Also "AA1..." oder "PP2..3...." würden auch erkannt. Es ist auch ein bisschen ”unsymmetrisch”/inkonsistent, dass "AP1." und "AP2.3." erkannt werden, also erlaubt sind, "AP1" ohne abschliessenden Punkt nicht, "AP2.3" ohne abschliessenden Punkt dann aber wieder doch.

Beim erstellen der zweiten Tabelle sind `item` und `desc` bereits Zeichenketten. Es macht keinen Sinn da noch einmal `str()` mit aufzurufen.

Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.

`os.system()` sollte man nicht mehr verwenden. Die Dokumentation dort verweist auf das `subprocess`-Modul.

Es werden zum Speichern und zum starten der Textverarbeitung zwei unterschiedliche Dateinamen verwendet. Also einmal mit und einmal ohne den Pfad. Das kann funktionieren, muss es aber nicht.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import re
import subprocess
import time

import docx
import docx2txt


def main():
    text = docx2txt.process("C:/Users/korre/Documents/Tabelle.docx")

    work_packages = [
        match.groups()
        for match in re.finditer(
            r"^(AP\d+(?:\.\d+)*) (.*)", text, re.MULTILINE
        )
    ]

    parse_end_timestamp = time.perf_counter()

    document = docx.Document()
    document.add_heading("Balkenplan", 1)

    for cell, heading in zip(
        document.add_table(rows=1, cols=5, style="Table Grid").rows[0].cells,
        ["Arbeitspakete", "Beschreibung", "1. Jahr", "2. Jahr", "3. Jahr"],
    ):
        cell.text = heading

    print("Processing...\n")
    print("[1] Parsing input file and creating table...")
    table = document.add_table(rows=0, cols=14, style="Table Grid")
    for work_package in work_packages:
        for cell, content in zip(table.add_row().cells, work_package):
            cell.text = content

    rows_created_timestamp = time.perf_counter()

    print("[2] Adjusting cell widths...\n")
    completed_timestamp = time.perf_counter()

    print("Summary:")
    print(
        f"[1] Parsing and creating rows ({len(work_packages)})"
        f" elapsed time: {rows_created_timestamp -  parse_end_timestamp:.2f}s"
    )
    print(
        f"[2] Adjusting Geometry and Spacing:"
        f" elapsed time: {completed_timestamp - rows_created_timestamp:.2f}s)"
    )
    print(f"total: {completed_timestamp - parse_end_timestamp:.2f}s)")

    filename = "C:/Users/korre/Documents/test.docx"
    document.save(filename)
    subprocess.run(["start", filename], check=True)


if __name__ == "__main__":
    main()
Zum Tabellenproblem: Da würde man *eine* Tabelle für erstellen, mit 14 Spalten und bei den letzten drei Überschriften jeweils vier Zellen zusammenfassen. Also so wie man das manuell auch machen würde.

Re: Probleme mit der Ausrichtung von Zellen mit Python docx

Verfasst: Samstag 19. März 2022, 14:02
von schweito
Hi ,

vielen Dank für die schnelle und ausführliche Antwort. Ich werde eine Weile brauchen um einiges zu verwirklichen. Die teilweise seltsame Nummerierung kommt zustande,
da ich viele Sache ausprobiert habe - auch aus Beispielen - dann wieder Sachen geändert habe und da nicht konsequent war in der Umbennenung. Ich danke schonmal herzlich für die vielen Informationen, brauche erst mal eine Weile um die alle umzusetzen.

Ich werde versuchen, Dein Beispiel nachzuvollziehen und dann ggfs. nochmal was dazuschreiben. Das mit den Regeln für die Imports wusste ich nicht, das werde ich mir merken. Ich hatte die Inches eingebunden, da ich einen gewisse Zeit versucht habe die Spalten über diese Funktionen in der Größe anzupassen, was aber nicht gelang.

Herzlichen Dank für die ausführliche Antwort, jetzt obliegt es mir das ganze umzusetzen!

Viele Grüße und ein schönes Wochenende an alle,
Timo