Bei Änderungen von docx-Dokumenten verschwinden Fußnoten

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
Möwenspecht
User
Beiträge: 4
Registriert: Montag 10. März 2025, 14:17

Moin moin,
ich bin blutiger Anfänger und hoffe nach dem zweiten Tag lesen, suchen und probieren auf eure Hilfe:
Für ein Projekt muss ich in vielen Word-Dokumenten einzelne Zeichen ersetzen. Die Änderungen funktionieren auch (im Minimalbeispiel nicht vollständig dargestellt), allerdings verhindern die drei Zeilen zu "for para in doc.paragraph:", dass im überarbeiteten Dokument Fußnoten auftauchen. Wenn ich die Zeilen streiche, werden aber die Anführungszeichen (logischerweise) nicht mehr geändert. Mir ist am Ende wichtig, dass die Dokumente nachher noch wie vorher sind, also dass Format, Kursivsetzungen etc. und natürlich auch Fußnoten erhalten bleiben. Nur die Anführungszeichen sollen konsequent - möglichst auch in den Fußnoten - geändert werden. Hat jemand eine Idee, wie ich das umsetzen kann?

Vielen Dank schonmal!
Oh, und tut mir furchtbar leid, wenn ich nicht im richtigen Forum gelandet bin - ich bin neu hier und habe nichts Entsprechendes gefunden.

Code: Alles auswählen

from docx import Document

def ersetze_anfuehrungszeichen(input_datei, output_datei):
    ersetzungen = {
        "„": "»",
        "“": "«"
    }

    doc = Document(input_datei)


    for para in doc.paragraphs:
        for alt, neu in ersetzungen.items():
            para.text = para.text.replace(alt, neu)


    for tabelle in doc.tables:
        for zeile in tabelle.rows:
            for zelle in zeile.cells:
                for alt, neu in ersetzungen.items():
                    zelle.text = zelle.text.replace(alt, neu)

    doc.save(output_datei)


ersetze_anfuehrungszeichen("input.docx", "output.docx")
Benutzeravatar
noisefloor
User
Beiträge: 4175
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

mal geraten: Fussnoten sind ein eigener Typ in Word-Dokumente, die du halt nicht mit kopierst. Die kopierst ja "nur" Paragraphen und Tabellen. Da müsstest du mal in die Struktur von Word-Dokumenten suchen. Bzw. wenn du nach `doc = ...` die Zeile `print(dir(doc))` einfügst bekommst du ausgegeben, welche Attribute das doc-Objekt hat.

Gruß, noisefloor
Sirius3
User
Beiträge: 18253
Registriert: Sonntag 21. Oktober 2012, 17:20

Ein Paragraph besteht aus mehreren Runs,die die Formatierungen und Text enthalten. Wenn Du den Text eines Paragraphen setzt wird dagegen die Formatierungen weggeschmissen. Sowohl an den Inhalt von Dokumenten als auch von Paragraphen oder anderen komplexere n Elementen kommt man über iter_inner_content. Du musst also den Elementbaum bis auf die Run-Blätter nach unten laufen um dann dort den Text zu ersetzen.
Benutzeravatar
snafu
User
Beiträge: 6850
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hier hat jemand eine recht umfangreiche Lösung(?) gepostet:
https://stackoverflow.com/a/67188730

@Möwenspecht: Vielleicht testest du mal, ob das für dich funktioniert.
Möwenspecht
User
Beiträge: 4
Registriert: Montag 10. März 2025, 14:17

Vielen Dank für eure Hinweise!

@ noisefloor: Wenn ich deinen Tipp befolge, bekomme ich einen ganzen Haufen an Attributen, aber leider nichts, was nach Fußnoten klingt.
@Sirius3: Danke. Ich meine, dass ich das schon drin habe und damit tatsächlich die Formatierung nicht verloren geht, aber es leider das Problem nicht löst. Du meist so oder?

Code: Alles auswählen

 # Absätze durchgehen
    for para in doc.paragraphs:
        for run in para.runs:
            run.text = ersetze_text(run.text)

    # Tabellen durchgehen
    for tabelle in doc.tables:
        for zeile in tabelle.rows:
            for zelle in zeile.cells:
                for para in zelle.paragraphs:
                    for run in para.runs:
                        run.text = ersetze_text(run.text)
@ snafu: Das klingt auch spannend und ich probiere das auf jeden Fall aus. Ich befürchte allerdings, dass selbst da nicht die Fußnoten mit eingeschlossen werden (paragraphs, tables, header, footer). Ich habe auch in der Dokumentation einen Hinweis gefunden, dass python-docx einfach nicht mit Fußnoten umgehen kann:
The feature set is still being built out, so you can’t add or change things like headers or footnotes yet, but if the document has them python-docx is polite enough to leave them alone and smart enough to save them without actually understanding what they are.
Für Kopf- und Fußzeilen scheint das schon nachgeholt worden zu sein, für Fußnoten scheinbar noch nicht. Ich habe allerdings den Hinweis auf eine Fork gefunden, die explizit Fußnoten und Kommentare mit einschließt: https://pypi.org/project/bayoo-docx/
Ich werde es damit noch probieren und berichte weiter. :)
Benutzeravatar
snafu
User
Beiträge: 6850
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Die Methodennamen aus meinem Link sprechen ja von Header und Footer. Daher hätte ich jetzt erwartet, dass sie auch dementsprechend funktionieren.
Möwenspecht
User
Beiträge: 4
Registriert: Montag 10. März 2025, 14:17

Ja stimmt. Nun sind Header und Footer Kopf- und Fußzeilen aber explizit nicht die Fußnoten. Mit bayoo-docx lassen sich zwar Fußnoten (und Kommentare) in Dokumenten erstellen, aber nicht in vorhandenen Dokumenten bearbeiten, das war es also auch nicht. Die Lösung ist nun für mich, das Word-Dokument temporär zu entpacken, weil die Fußnoten in einer eigenen xml liegen:

Code: Alles auswählen

import zipfile
import tempfile
import shutil
import os
import re
from lxml import etree


def ersetze_anfuehrungszeichen(text):
    """
    Ersetzt deutsche und englische Anführungszeichen durch Guillemets
    """
    ersetzungen = {
    "„":"»",
    "‚":"›"
    }

    for alt, neu in ersetzungen.items():
        text = text.replace(alt, neu)

    # Schließende Anführungszeichen (nach einem Wort):
    text = re.sub(r'[”“"](\s|\n|$|[.,;:!?])', r'«\1', text)  # ”, “ und " wird zu «
    text = re.sub(r'[’‘](\s|\n|$|[.,;:!?])', r'‹\1', text)  # ’ und ‘ wird zu ‹

    # Öffnende Anführungszeichen (vor einem Wort):
    text = re.sub(r'(^|\s)[”“"]', r'\1»', text)  # ”, “ und " wird zu »
    text = re.sub(r'(^|\s)[’‘]', r'\1›', text)  # ’ und ‘ wird zu ›
    


    return text


def bearbeite_xml(dateipfad):
    """
    Bearbeitet eine XML-Datei, um die gewünschten Ersetzungen vorzunehmen.
    """
    with open(dateipfad, "r", encoding="utf-8") as file:
        tree = etree.parse(file)

    # Suche nach allen Textknoten und ersetze den Inhalt
    for node in tree.findall(".//w:t", namespaces=tree.getroot().nsmap):
        if node.text:
            node.text = ersetze_anfuehrungszeichen(node.text)

    # Speichere die bearbeitete Datei
    with open(dateipfad, "wb") as file:
        tree.write(file, encoding="utf-8", xml_declaration=True)


def bearbeite_docx(input_datei, output_datei):
    """
    Bearbeitet eine DOCX-Datei, indem sie entpackt, bearbeitet und wieder verpackt wird.
    """
    with tempfile.TemporaryDirectory() as tempdir:
        # Entpacke die DOCX-Datei
        with zipfile.ZipFile(input_datei, "r") as zip_ref:
            zip_ref.extractall(tempdir)

        # Bearbeite die XML-Dateien im Dokumenttext und in den Fußnoten
        for dateiname in ["word/document.xml", "word/footnotes.xml"]:
            dateipfad = os.path.join(tempdir, dateiname)
            if os.path.exists(dateipfad):
                bearbeite_xml(dateipfad)

        # Erstelle die bearbeitete DOCX-Datei
        with zipfile.ZipFile(output_datei, "w", zipfile.ZIP_DEFLATED) as zip_ref:
            for root, dirs, files in os.walk(tempdir):
                for file in files:
                    dateipfad = os.path.join(root, file)
                    zip_ref.write(dateipfad, os.path.relpath(dateipfad, tempdir))


bearbeite_docx("input.docx", "output.docx")
Danke nochmal!
Sirius3
User
Beiträge: 18253
Registriert: Sonntag 21. Oktober 2012, 17:20

Das ganze kann man natürlich auch ohne das extra speichern auf Platte machen:

Code: Alles auswählen

import re
import shutil
import zipfile
from lxml import etree

def replace_quotes(text):
    """
    Ersetzt deutsche und englische Anführungszeichen durch Guillemets
    """
    replacements = {
        "„":"»",
        "‚":"›"
    }

    for old, new in replacements.items():
        text = text.replace(old, new)

    # Schließende Anführungszeichen (nach einem Wort):
    text = re.sub(r'[”“"](\s|\n|$|[.,;:!?])', r'«\1', text)  # ”, “ und " wird zu «
    text = re.sub(r'[’‘](\s|\n|$|[.,;:!?])', r'‹\1', text)  # ’ und ‘ wird zu ‹

    # Öffnende Anführungszeichen (vor einem Wort):
    text = re.sub(r'(^|\s)[”“"]', r'\1»', text)  # ”, “ und " wird zu »
    text = re.sub(r'(^|\s)[’‘]', r'\1›', text)  # ’ und ‘ wird zu ›

    return text


def process_xml(input_file, output_file):
    tree = etree.parse(file)

    # Suche nach allen Textknoten und ersetze den Inhalt
    for node in tree.findall(".//w:t", namespaces=tree.getroot().nsmap):
        if node.text:
            node.text = replace_quotes(node.text)

    tree.write(output_file, encoding="utf-8", xml_declaration=True)


def process_single_file(filename, input_file, output_file):
    if filename in ["word/document.xml", "word/footnotes.xml"]:
        process_xml(input_file, output_file)
    else:
        shutil.copy_obj(input_file, output_file)

def process_docx(input_filename, output_filename):
    """
    Bearbeitet eine DOCX-Datei, indem sie entpackt, bearbeitet und wieder verpackt wird.
    """
    with zipfile.ZipFile(input_filename, mode="r") as input_doc:
        with zipfile.ZipFile(output_filename, mode="w") as output_doc:
            for filename in input_doc.namelist():
                with input_doc.open(filename, mode="r") as input_file:
                    with output_doc.open(filename, mode="w") as output_file:
                        process_single_file(filename, input_file, output_file)


if __name__ == "__main__":
    process_docx("input.docx", "output.docx")
Möwenspecht
User
Beiträge: 4
Registriert: Montag 10. März 2025, 14:17

Das ist natürlich eleganter - vielen Dank!
Antworten