Einträge analoger Findbüchern automatisiert in Datenbanken übernehmen - Reguläre Ausdrücke

Fischernetz Photo by Reuben Hustler on Unsplash.

Im FDMLab haben wir einige analoge Findbücher digitalisiert und die Einträge automatisiert in unsere Datenbanksysteme übernommen. Dieser Blogbeitrag konzentriert sich auf die Extraktion der Informationen mit regulären Ausdrücken.

Dabei gehen wir recht technisch vor und zeigen Codebeispiele in Python und mit spaCy 3.0.

Bei spaCy handelt es sich um ein Python basiertes Framework für Natural Language Processing (NLP). Wir konzentrieren uns hier auf einen kleinen Teil des Frameworks, nämlich das regelbasierte Matching.

Inhaltsverzeichnis

Folgende imports sind notwendig um die Code-Beispiele auszuführen.

import re

from spacy import displacy
from spacy.lang.de import German
from spacy.matcher import Matcher
from spacy.tokens import Span
Wir versuchen die (Code-)Beispiele so knapp und verständlich wie möglich zu halten. Daher fehlen einige Details (Fehlerbehandlung, Spezialfälle, …).

Einfache reguläre Ausdrücke

Bei unseren Projekten zur Findbuchextraktion haben wir häufig die Situation, dass wir die Inhalte eines Textblocks auf mehrere Felder in der Zieldatenbank mappen.

Hier haben wir ein markiertes Beispiel für den Beginn eines Findbucheintrages.

42 NR HSTA L13 42 SIG ( A9594 ASIG ) 1798 DATUM

Hier sind die Findbuchnummer (NR), die Bestellsignatur (SIG), die Altsignatur (ASIG), die Vorsignatur (VSIG) und ein zugehöriges Datum (DATUM) der Archivalie in einer Zeile und sollen als einzelne Datenfelder extrahiert werden.

Mit einem regulären Ausdruck und so genannten “named capturing groups” lassen sich die benötigten Datenschnippsel (Entities) extrahieren und direkt für die Datenbank weiterverarbeiten.

Code-Beispiel:

text1 = "42 HSTA L13 42 (A9594) 1798"
pattern = "^(?P<NR>\d+)\s+(?P<SIG>[^\(]+)\s+\((?P<ASIG>.+)\)\s+(?P<DATUM>\d+)$"
re.match(pattern, text1).groupdict()

Ausgabe:

{'NR': '42', 'SIG': 'HSTA L13 42', 'ASIG': 'A9594', 'DATUM': '1798'}

Aber: zumindest in den von uns bearbeiteten Findbüchern blieb das Schema nie so konsistent, als dass die Daten mit nur einem verständlichen regulären Ausdruck extrahiert werden konnten. Hier eine weitere Zeile aus einem Findbucheintrag aus dem gleichen Band.

113a NR HSTA L13 50-55c SIG (Vorsignatur: GLAK B33 F 7 13b VSIG , T1234c ASIG ) ca. 18. Jhd. DATUM

Hier funktioniert der oben gezeigte reguläre Ausdruck nicht mehr.

Code-Beispiel:

text2 = "113a HSTA L13 50-55c (Vorsignatur: GLAK B33 F 7 13b, T1234c) ca. 18. Jhd."
re.match(pattern, text2) is None

Ausgabe:

True

Der Einsatz von einzelnen regulären Ausdrücken führte bei uns zu unübersichtlichen und nicht zu pflegenden Konstrukten. Daher sind wir dazu übergegangen, die unterschiedlichen Bestandteile einzeln und mit mehreren regulären Ausdrücken zu erfassen.

Mehrere reguläre Ausdrücke

Anstatt alle Bestandteile in einem regulären Ausdruck zu erfassen, kann man einen Text auch mit mehreren getrennten regulären Ausdrücken bearbeiten.

Code-Beispiel:

asig_regex = r"[A-Z]\d+[a-z]?"
entity_definitions = {
    "NR": [r"^\d+[a-z]?"],
    "SIG": [r"HSTA.+(?=\()"],
    "ASIG": [fr"(?<=\(){asig_regex}(?=\))", fr"(?<=, ){asig_regex}(?=\))"],
    "VSIG": [r"(?<=Vorsignatur: ).+(?=,)"],
    "DATUM": [r"(?<=\) ).+$"]
}

for text in [text1, text2]:
    found = {}
    for key, regex_list in entity_definitions.items():
        for regex in regex_list:
            match = re.search(regex, text)
            if match:
                found[key] = match.group()
    print(found)

Ausgabe:

{'NR': '42', 'SIG': 'HSTA L13 42 ', 'ASIG': 'A9594', 'DATUM': '1798'}
{'NR': '113a', 'SIG': 'HSTA L13 50-55c ', 'ASIG': 'T1234c', 'VSIG': 'GLAK B33 F 7 13b', 'DATUM': 'ca. 18. Jhd.'}

Diese Methode ist aufwendiger in der Implementierung, jedoch deutlich flexibler als die Verwendung eines einzelnen regulären Ausdrucks.

Aber auch hier zeigen sich schnell mehrere Probleme:

  1. Keine Warnung/Fehler, wenn sich reguläre Ausdrücke überlappen.
  2. Keine automatische Anpassung, wenn es mehrere Ergebnisse für ein Entity gibt.
  3. Keine Markierung von schon ausgewählten/extrahierten Informationen.

Dies lässt sich nachrüsten, sorgt jedoch für weitere Komplexität bei der Umsetzung. Eine Lösung hierfür bieten Frameworks zur Extraktion von Informationen aus Text, wobei wir spaCy für diesen Zweck einsetzen.

Regelbasiertes Matching mit spaCy

Das NLP Framework spaCy hat ein eigenes System für das regelbasierte Matching.

Daher hier ein Beispiel mit einer spaCy Pipeline.

Code-Beispiel:

nlp = German()

ruler = nlp.add_pipe("entity_ruler")
patterns = [
    {"label": "NR", "id": "nr", "pattern": [{"IS_SENT_START": True, "IS_DIGIT": True}]},
    {"label": "NR", "id": "nr", "pattern": [
        {"IS_SENT_START": True, "TEXT": {"REGEX": r"^\d+[a-z]?$"}}]},
    {"label": "SIG", "id": "sig", "pattern": [
        {"TEXT": "HSTA"}, {"SHAPE": "Xdd"}, {"TEXT": {"REGEX": r"^\d+[a-z]?$"}},
        {"TEXT": "-", "OP": "?"}, {"TEXT": {"REGEX": r"^\d+[a-z]?$"}, "OP": "?"}]},
    {"label": "ASIG", "id": "asig", "pattern": [{"SHAPE": "Xdddd"}]},
    {"label": "ASIG", "id": "asig", "pattern": [{"SHAPE": "Xddddx"}]},
    {"label": "DATUM", "id": "date", "pattern": [{"SHAPE": "dddd"}]},
    {"label": "DATUM", "id": "date", "pattern": [
        {"TEXT": "ca."}, {"SHAPE": "dd."}, {"TEXT": "Jhd."}]},
]
ruler.add_patterns(patterns)

doc = nlp(text1)
displacy.render(doc, style="ent")
42 NR HSTA L13 42 SIG ( A9594 ASIG ) 1798 DATUM
doc2 = nlp(text2)
displacy.render(doc2, style="ent")
113a NR HSTA L13 50-55c SIG (Vorsignatur: GLAK B33 F 7 13b, T1234c ASIG ) ca. 18. Jhd. DATUM

Bei diesem Code-Beispiel sehen wir einige Vorteile bei der Verwendung von spaCy. Wir können die mitgelieferten Visualisierungstools verwenden, mehrere Regeln für ein Entity hinterlegen und auf weniger komplexe Regeln zurückgreifen (Shape-Pattern, IS_PUNCT, …).

Wir können uns auch direkt die übersprungenen Teile ausgeben lassen (oder sie markieren).

Code-Beispiel:

print([token for token in doc2 if not (token.ent_type_ or token.is_punct)])

Ausgabe:

[Vorsignatur, GLAK, B33, F, 7, 13b]

Hier zeigen sich auch gleich mehrere Probleme bei der Verwendung von regelbasiertem Matching mit spaCy:

  1. Jedes Muster passt immer nur auf einen Token.
  2. Ein “lookahead” oder “lookbehind” wie bei regulären Ausdrücken ist (noch) nicht möglich. Es gibt also keine Entsprechung für (?<=Vorsignatur: )[^,]+(?=,).

Um trotzdem nur die Vorsignatur zu extrahieren, ohne das Prefix “Vorsignatur: " mitzuspeichern, gibt es mehrere Lösungen:

  1. Nachbearbeitung der Entities, also das Prefix in einem separaten Durchlauf löschen.
  2. Die komplexen Muster in einem separaten Durchlauf verarbeiten (nächster Abschnitt).
  3. Reguläre Ausdrücke (Gesamttext) mit spaCy kombinieren (übernächster Abschnitt).

Erweitertes regelbasiertes Matching mit spaCy

Für das folgende Beispiel wollen wir ein Entity basierend auf “Umgebungsinformationen” bestimmen. Nehmen wir zum Beispiel an, dass es ziemlich komplex ist die Signatur zu bestimmen, wir aber wissen, dass die Signatur immer nach der Findbuch-Nr zu finden ist.

Also würden wir gerne ein Muster der Form (?<=NR).+(?=\()definieren. Dafür verarbeiten wir den ersten Beispielsatz zweimal mit spaCy.

Zuerst erstellen wir wieder eine Pipeline, die uns die “einfachen” Entities markiert.

Code-Beispiel:

nlp = German()

ruler = nlp.add_pipe("entity_ruler")

patterns = [{"label": "NR", "pattern": [{"IS_SENT_START": True, "IS_DIGIT": True}]}]
ruler.add_patterns(patterns)

doc = nlp(text1)
displacy.render(doc, style="ent")
42 NR HSTA L13 42 (A9594) 1798

Anschließend verarbeiten wir den mit Markierungen versehenen Beispielsatz mit einem Matcher und unserem komplexen Muster.

Code-Beispiel:

matcher = Matcher(nlp.vocab)
signature_pattern = [
    {"ENT_TYPE": "NR"},
    {"TEXT": {"NOT_IN": ["("]}, "OP": "+"},
    {"TEXT": "("}
]
matcher.add("Signature", [signature_pattern])

matches = matcher(doc)
for _, start, end in matches:
    entity = Span(doc, start + 1, end - 1, label="SIG")
    doc.ents += (entity,)

displacy.render(doc, style="ent")
42 NR HSTA L13 42 SIG (A9594) 1798

Hier erstellen wir das Entity selbst und fügen es den von spaCy verwalteten Informationen hinzu. Der gefundene Treffer beinhaltet das Entity NR und die öffnende Klammer (, daher schneiden wir noch vorne und hinten jeweils einen Token ab.

Im ersten Durchlauf für die Nr hat die Pipeline Komponente diese Aufgaben übernommen.

Hinweis: leider beinhalten die Treffer keinen Hinweis auf die Regel, die den Treffer erzeugt haben. Hat man viele Regeln, die mit unterschiedlichen Labels oder Beschnitt versehen werden sollen, empfehlen sich entweder der on_match Callback für die Regeln, oder eine Custom Component für die Pipeline.

Kombiniere reguläre Ausdrücke mit spaCy

Die Ansätze mit regulären Ausdrücken auf dem Gesamttext und dem regelbasierten Matching mit spaCy lassen sich auch kombinieren. Zuerst lassen wir das Dokument von spaCy analysieren. Anschließend wenden wir die regulären Ausdrücke auf den Gesamttext an und ermitteln die Position der Treffer. Mit dieser Information erzeugen wir dann Entities mit spaCy.

Code-Beispiel:

nlp = German()

ruler = nlp.add_pipe("entity_ruler")
patterns = [
    {"label": "NR", "id": "nr", "pattern": [{"IS_SENT_START": True, "IS_DIGIT": True}]}
]
ruler.add_patterns(patterns)

doc = nlp(text1)
displacy.render(doc, style="ent")
42 NR HSTA L13 42 (A9594) 1798
entity_definitions = {"DATUM": r"(?<=\) ).+$"}

for key, regex in entity_definitions.items():
    match = re.search(regex, doc.text)
    if match:
        start, end = match.span()
        entity = doc.char_span(start, end, label=key)
        doc.ents += (entity,)

displacy.render(doc, style="ent")
42 NR HSTA L13 42 (A9594) 1798 DATUM

Ein ähnliches Beispiel dazu findet sich auch in der spaCy Dokumentation.

Hier sind noch die folgenden Dinge zu beachten:

  1. Es werden keine Entities überschrieben.
  2. Es können keine sich überschneidenden Entities angelegt werden.
  3. Manchmal “rutschen” Trennzeichen (z.B. Leerzeichen) mit in den Beginn oder das Ende der Treffer. Diese führen beim Setzen einer Entity in spaCy zu einem Fehler.

Die entsprechenden Fehlerbehandlungen haben wir im Code-Beispiel ausgelassen.

Fazit

Bei der Aufbereitung von Findbüchern für die Datenbank sind wir sowohl mit regulären Ausdrücken, als auch mit dem Regelsystem von spaCy an die jeweiligen Grenzen der Technologie gestoßen. Dabei haben wir die Toolunterstützung von spaCy zu schätzen gelernt, die uns einiges an Entwicklungsaufwand abnimmt und bei der Visualisierung der Ergebnisse unterstützt.

Mit einer Kombination aus regulären Ausdrücken und Regeln in spaCy, die auf schon identifizierten Entities basieren, konnten wir mit einer übersichtlichen Codebasis selbst komplexe Datenaufbereitungen durchführen.

Benjamin Rosemann
Benjamin Rosemann
Data Scientist

Ich evaluiere KI- und Software-Lösungen und integriere sie in den Archivalltag.

Ähnliches