Text zu XML mit Python auf Basis des „Bomber’s Baedeker“

von Felix Bach und Cristian Secco

Die Transformation von digitalisierten Druckwerken von einer Bilddatei zur maschinenlesbaren XML-Datei ist für zahlreiche Methoden der Digital Humanities ein wichtiger Schritt in der Datenaufbereitung. In diesem Beitrag präsentieren wir einen Ansatz auf Basis eines Python-Skripts am Beispiel eines Werkes mit einer besonderen Binnenstruktur: Der Bomber’s Baedeker war ein „Reiseführer“, welcher von der Royal Air Force genutzt wurde, um während des 2. Weltkrieges deutsche Industriestandorte anzugreifen.

Die Quelle

„The Bomber’s Baedeker. A Guide to the Economic Importance of German Towns and Cities“ wurde vom britischen Ministry of Economic Warfare erstellt. Das 1943 und 1944 in erweiterter Auflage erschienene Buch besteht aus zwei Bänden und verfügt über Informationen zu geografischer Lage, Einwohnerzahl, Entfernung zu London in Meilen sowie umfassenden Beschreibungen von mehr als 500 Städten in Deutschland – zum Zwecke einer effektiven und effizienten Bombardierung. Eines der seltenen Exemplare, welches im Besitz des Leibniz-Institut für Europäische Geschichte (IEG), wurde im Rahmen eines gemeinsamen Projektes von der Universitätsbibliothek Mainz digitalisiert und zur Nutzung zur Verfügung gestellt: The Bomber’s Baedeker. In einer Machbarkeitsstudie im Wintersemester 2019/20 an der Hochschule Darmstadt entstand das Konzept für den automatisierten Prozess der XML-isierung, welcher im Herbst 2020 im Rahmen einer Zusammenarbeit zwi­schen Bibliothek und DH Lab umgesetzt werden konnte, so dass nun eine weitergehende Bearbeitung und Auszeichnung auf dieser Basis möglich ist.

Bild zu Text: OCR

Das Erzeugen eines maschinenlesbaren Textes aus einem Scan wird durch ein OCR-Programm ermöglicht. Für dieses Projekt haben wir hierfür „ABBYY Finereader“ genutzt. Bevor es allerdings möglich war, eine akkurate Textextraktion zu erzielen, musste das Digitalisat angepasst werden. Im Sinne der optimalen Erkennung der gedruckten Zeichen war es nötig, alle Seiten des Baedekers mit hohem Kontrast darzustellen. Dazu verwendeten wir die in MacOS integrierte Applikation Photos. Mit dieser ist es sehr einfach Bildkorrekturen zu kopieren und per Copy-Paste auf weitere Seiten anzuwenden. Nach der Optimierung aller Seiten und das Einlesen des Textes, konnten wir mit der Umwandlung zu XML starten.

Text zu XML: Python-Skript

Die Struktur des finalen Skripts, kann in drei Ebenen unterteilen werden. Ebene eins stellt das Preprocessing dar. Es werden wiederholt auftretende Fehler in der OCR und der Verfasser verbessert, die im weiteren Verlauf des Skriptes zu Fehlern oder falscher Erfassung der Objekte führen könnten. Zusätzlich werden strukturelle Abweichungen, wie beispielsweise die Benutzung eines Semikolons statt Doppelpunkten an im Baedeker größtenteils benutze Normen angepasst.

# Dictionary für die zu korrigierenden Werte
replacement_values = {"1o": "10", "2o": "20", "3o": "30", "4o": "40", "5o": "50", "6o": "60", "7o": "70", "8o": "80", "9o": "90", "1O": "10", "2O": "20", "3O": "30", "4O": "40", "5O": "50", "6O": "60", "7O": "70", "8O": "80", "9O": "90", "50 ": "50'", "50 '": "50'", ",50": "50", "J55": "355", "JO": "30", "J5": "35", "A8": "48", "’": "'", " °": "°", "U°": "14°", "m i": "mi", "0'm": "0 m", "0'M": "0 m", " '": "'", "0'N": "0 'N", "Nt": "N.", "N,": "N.", "N;": "N.", "Es": "E:", "E;": "E:", "'N": "' N", "»": "'", "y;": "y)", "?": "F", "N.": " N. ", "E:": " E: ", "U°": "14°", "AO'": "40'", "Ne": "N.", "E ..": "E:", "$0": "50", "AD": "40'", "AO": "40'", "B:": "E:", ".'": "'", "B.": "E:", "$4°": "54°", "®": "°", "$00": "600", "^5": "48°", "JJ.": "15'", "4ß°": "48°", "£:": "E:", "N -": "N:", "E.": "E:", "E": "E: ", "miles;": "miles:", "m iles:": "miles:", "miles": " miles", "ndles": "miles", "ndles;": "miles:",
for line in inputText:
    firstCleaning = line.replace("{", "(")

    # Schleife die durch das replacement_values Dictionary läuft und die entsprechenden Werte im Text ersetzt
    for key in replacement_values.keys():
        firstCleaning = firstCleaning.replace(key, replacement_values[key])

    secondCleaning.append(firstCleaning)

if secondCleaning:
    print("first cleaning done!")
    print(len(secondCleaning), "charakter")

with open("out.txt", "w", encoding='utf-8') as file:
    for line in secondCleaning:
        file.write(line)

secondCleaning = []
secondCleaning = open("out.txt", "r", encoding='utf-8')

for line in secondCleaning:
    if re.findall(r'^[A-K][A-ZÄÖÜ_\s-]{3,}', line) \
            and not '(contd.)' in line \
            and not '(oontd.)' in line \
            and not '(continued.)' in line \
            and not '(continued)' in line \
            and not '(co n td .)' in line \
            and not '(conti)' in line \
            and not '(BEWAG)' in line \
            and not '(pontd.)' in line \
            and not '(c o n td .)' in line \
            and not '(oontcL )' in line \
            and not '''(Cont'd .)''' in line \
            and not '(Contd.)' in line:
        line = line.replace(" ", "") \
            .replace("(", " (") \
            .replace("near", " near ") \
            .replace("-", " ") \
            .replace("\n\n", "\n")

    thirdCleaning.append(line)

if thirdCleaning:
    print("\nsecond cleaning done!")
    print(len(thirdCleaning), "charakter")

# creates final output txt
with open("BB1_CLEAN.txt", "w", encoding='utf-8') as file:
    for line in thirdCleaning:
        file.write(line)
print("\nBB1_CLEAN.txt created")
print("preprocessing finished")

run_preprocessing.py [github]

Die zweite Ebene beinhaltet die Objekterkennung- und Erfassung. Durch von uns festgelegte Muster erkennt das Skript, wie der Name einer Stadt im Baedeker dargestellt wird und welche Attribute eine Stadt besitzt. Die geografischen Daten, Einwohner und Entfernung der Stadt zu London werden im Druck immer eine Zeile unter dem Städtenamen dargestellt. Eine Schleife sucht im gesamten Text, nach den von uns definierten Mustern. Ein Beispiel für ein solches Muster ist der unten stehende reguläre Ausdruck. Dieser definiert, dass das aktuell betrachtete Textelement nur aus Großbuchstaben von A-Z (inkl. Umlaute) bestehen darf und mindestens drei Zeichen lang sein muss. Da der Text Zeile für Zeile untersucht wird und die Städtenamen im Originaltext immer in Großbuchstaben dargestellt werden, können so die Städte erkannt werden.

def find_cities_detailed(text, volume):

    # Volume spezifische Informationen, x = Erster Stadteintrag, Regex = Erkennungsmuster für Städte
    if volume == 1:
        x = 67
        regex = r'^[A-K][A-ZÄÖÜ_\s-]{3,}'

    else:
        x = 44
        regex = r'^[L-Z][A-ZÄÖÜ_\s-]{3,}'

    # Liste der Städtenamen
    city_names = []

    # Check Variable für korrekte Ergebnisse
    is_valid = False

    # Aufsplitten des Textes in Zeilen
    text_split = text.splitlines()

    # Hilfsvariablen
    i = 1

    # Die Zeile nach der aktuellen Zeile
    next_line = None

    # Durchlaufen des Textes
    for index, line in enumerate(text_split):

        # Überspringen der Titel- und Innenseite
        if i > x:

            # Zwischenspeichern der nächsten Zeile
            if index < (len(text_split) - 1):
                next_line = text_split[index + 1]

            # Zwischenspeichern des Stadtnamens
            city_name_temp = re.findall(regex, line)

functions.py, L22 [github]

In der dritten und letzten Ebene werden die gefundenen Städte als Objekt in der XML gespeichert und zusätzlich mit den dazugehörigen Attributen versehen. Zudem werden zu jeder Stadt noch die entsprechende Seitenzahl des Drucks und eine URL der UB Mainz vermerkt. Das finale Skript enthält etwa 650 Zeilen Code und kann über einen Funktionsaufruf gestartet werden. Beispiel: Anfügen eines neuen Sub-Elements „city“ mit dem Stadtnamen als Wert des Attributs „name“ in die XML-Struktur:

# Funktion, die die Ergebnisse an eine vorhandene (manuell erstellte) XML-Datei anfügt
def insert_cities_xml(xml_file, data_dic, output_file):
    # Initialisieren des XML-Trees
    tree = ET.parse(xml_file)

    # Zuweisen der Wurzel des XML-Trees
    root = tree.getroot()

    # Städtenamen
    city_names = data_dic['cities']

    # Bundesländer
    state_list = data_dic['states']

    # Koordinaten
    coordinates_list = data_dic['coordinates']

    # Entfernungen zu London
    distances_list = data_dic['distances']

    # Einwohnerzahlen
    population_list = data_dic['population']

    # Beschreibungen
    description_list = data_dic['description']

    # Index
    index_dic = read_csv("extract_city_page_url/city_index_cities.csv")

    # Durchlaufen aller Elemente "book" im XML-Tree
    for book in root.iter('book'):

        # Durchlaufen der Städteliste
        for city in city_names:
            # Anfügen eines neuen Sub-Elements "city" mit dem Stadtnamen als Wert des Attributs "name"
            sub = ET.SubElement(book, 'city')
            sub.set('name', city)

functions.py, L454 [github]

Das vollständige Python-Script ist auf Github verfügbar: https://github.com/ieg-dhr/bombers_baedeker/.

Durch eine zusätzliche, manuelle Korrektur des Rohtextes, konnten alle 544 Städte erfasst werden. Die Struktur der XML sieht wie folgt aus:

<book>
	<city name="MAINZ">
		<state>Hesse</state>
		<coordinates>50.,8.15</coordinates>
		<distance> 380 miles</distance>
		<population> 145.000</population>
		<description> Mainz is on the left bank of the Rhine...</description>
	</city>
</book>

Durch weitere manuelle Korrekturen, wäre es noch möglich weitere Attribute der Städte zu finden. Dies erfordert jedoch hohen Aufwand, da es viele einzelne Fehler gibt und widerspricht damit dem Grundgedanken dieser Arbeit, der automatischen Extraktion.

Nutzungsszenarien 

Die von uns automatisch erstellte XML-Datei kann für Websites genutzt werden und eignet sich ebenfalls für Szenarien mit maschinellem Lernen. Informationen aus der XML-Datei können sehr leicht visualisiert werden: In unserem Beispiel stellten wir die im Baedeker erwähnten Koordinaten mit dem Geo-Browser von DARIAH-DE dar.

Die Visualisierung liefert wichtige Informationen auf zwei Ebenen: Einige Koordinaten sind liegen außerhalb Deutschlands, sie sind vermutlich fehlerhaft oder in in einigen wenigen Fällen fehlerhaft erkannt. Die Punkteverteilung bietet aber auch Ansatzpunkte für inhaltliche Analysen: Es lässt sich das Muster eines Rasters erkennen.

Future Work

Das vorgestellte Skript bietet eine mögliches Vorgehen, um automatisch XML-Dateien aus gedruckten Werken zu erzeugen. Um den Ansatz noch zu erweitern und zu verbessern, könnten beispielsweise Machine-Learning-Verfahren zu Einsatz kommen, um Fehler im Text zu erkennen sowie eine akkurate Mustererkennung zu gewährleisten. Letzteres ist vor allem wichtig, um weitere Werke automatisch umwandeln zu können, deren Muster sich von dem hier untersuchten Text unterscheiden.


Cite this article as: Felix Bach und Cristian Secco, „Text zu XML mit Python auf Basis des „Bomber’s Baedeker“,“ in Digital Humanities Lab, 28/01/2021, https://dhlab.hypotheses.org/?p=1820.

Die Autoren

Felix Bach

  • Hochschule Darmstadt
  • Bachelor of Science in Informationswissenschaft (2017-2020)
  • Praktische Tätigkeit bei der Knowledge Engineering Group der TU Darmstadt  (2020)
  • seit 2020 beim DIPF, Leibniz-Institut für Bildungsforschung und Bildungsinformation
  • seit 2020 im Studiengang Information Science, Master of Science

Cristian Secco

  • Hochschule Darmstadt
  • Bachelor of Science in Informationswissenschaft (2017-2020)
  • Praktische Tätigkeit bei der Fraport AG (2020)
  • seit 2020 im Studiengang Information Science, Master of Science

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.