spaCy: Veranstaltungs-ID erkennen#

Ein Web-CMS eines Kulturzentrums stellt für seine Veranstaltungen permanente IDs zur Verfügung, z.B.

Die Veranstaltung mit der Veranstaltungs-ID 
127166_2022-06-12T12:00 
ist für alle Interessierten offen.

Aufgabe: Extrahieren Sie den Permalink; mögliche Herangehensweisen:

Vor- und Nachteile der jeweiligen Methode?

import spacy
from spacy.matcher import Matcher
text_de = """Die Veranstaltung mit der Veranstaltungs-ID 
127166_2022-06-12T12:00
ist für alle Interessierten offen."""

Ausgangspunkt#

https://spacy.io/usage/rule-based-matching#example2

nlp = spacy.load("en_core_web_sm")
matcher = Matcher(nlp.vocab)
pattern = [{"ORTH": "("}, {"SHAPE": "ddd"}, {"ORTH": ")"}, {"SHAPE": "ddd"},
           {"ORTH": "-", "OP": "?"}, {"SHAPE": "ddd"}]
matcher.add("PHONE_NUMBER", [pattern])

doc = nlp("Call me at (123) 456 789 or (123) 456 789!")
print([t.text for t in doc])
['Call', 'me', 'at', '(', '123', ')', '456', '789', 'or', '(', '123', ')', '456', '789', '!']
matches = matcher(doc)
for match_id, start, end in matches:
    span = doc[start:end]
    print(span.text)
(123) 456 789
(123) 456 789

Matching Rules#

# nlp = spacy.load("en_core_web_sm")
nlp_de = spacy.load("de_core_news_sm")
matcher_de = Matcher(nlp_de.vocab)
doc_de = nlp_de(text_de)

Challenge: wir müssen die Strultur der VID, die wir in der regex_vid herausgefunden haben, in ein pattern übersetzen.

Herausfordernde Randbedingungen:

  • Die regex in spaCy arbeiten nur auf einzelenen Tokens, token-übergreifende regex sind nicht möglich.

  • Die Tokenization in spaCy ist “intelligent”, also heuristisch und nicht immer klar vorherzusehen.

Wie sieht die Tokenisation zu unserer VID aus?

[t.text for t in doc_de]
['Die',
 'Veranstaltung',
 'mit',
 'der',
 'Veranstaltungs-ID',
 '\n',
 '127166_2022',
 '-',
 '06',
 '-',
 '12T12:00',
 '\n',
 'ist',
 'für',
 'alle',
 'Interessierten',
 'offen',
 '.']

Wie wir sehen, ist die Tokenization nicht ganz intuitiv. Unsere matching rules müssen sich aber an diesen Tokens orientieren, z.B. so:

pattern_de_VID = [  
    {"TEXT": {"REGEX": "\d+_\d\d\d\d"}},    # 127166_2022
    {"ORTH": "-"},                          # -
    {"SHAPE": "dd"},                        # 06
    {"ORTH": "-"},                          # -
    {"TEXT": {"REGEX": "^\d\dT\d\d:\d\d$"}} # 12T12:00
]
matcher_de.add("tagUhr", [pattern_de_VID])
matches_de = matcher_de(doc_de)
for match_id, start, end in matches_de:
    span = doc_de[start:end]
    print(span.text)
127166_2022-06-12T12:00

Bewertung: Unser pattern_de_VID ist unintuitiv und fehleranfällig. Um es zu bauen, müssen wir uns auf die Ergebnisse des Tokenizers als heuristische Black-Box-Vorstufe verlassen.

Wer das nicht will, kann versuchen, die VID direkt per regex auf dem Eingabestring zu entdecken.

regex über dem Eingabestring#

Wir benötigen eine regex für die VID. Die bauen wir interaktiv in https://regex101.com/ zusammen. Ergebnis:

regex_vid_de = r"(\d+)_(\d\d\d\d-\d\d-\d\dT\d\d:\d\d)"

regex101 liefert uns über TOOLS > Code Generator folgenden Code, um unsere Regex zu testen

import re

regex_matches_vid_de = re.finditer(regex_vid_de, text_de, re.MULTILINE)

for matchNum, match in enumerate(regex_matches_vid_de, start=1):
    
    print ("Match {matchNum} was found at {start}-{end}:\n   {match}".format(matchNum = matchNum, start = match.start(), end = match.end(), match = match.group()))
    
    for groupNum in range(0, len(match.groups())):
        groupNum = groupNum + 1
        
        print ("Group {groupNum} found at {start}-{end}:\n   {group}".format(groupNum = groupNum, start = match.start(groupNum), end = match.end(groupNum), group = match.group(groupNum)))
Match 1 was found at 45-68:
   127166_2022-06-12T12:00
Group 1 found at 45-51:
   127166
Group 2 found at 52-68:
   2022-06-12T12:00