Korpusverarbeitung – Annotation mit spaCy#
Übersicht#
Im Folgenden wird exemplarisch ein Text (txt-Datei) mit der Bibliothek spaCy annotiert. Dafür werden folgendene Schritte durchgeführt:
Einlesen des Texts
Worthäufigkeiten ohne echte Tokenisierung
Aufteilen des Texts in Wörter auf Grundlage von Leerzeichen
Abfrage von Häufigkeiten
Annotation mit spaCy
Laden des Sprachmodells
Analysekomponenten auswählen
Text annotieren
Worthäufigkeiten anzeigen
Annotation speichern
Prozess für das gesamte Korpus ausführen
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Voraussetzungen zur Ausführung des Jupyter Notebooks
- Installieren der Bibliotheken
- 2. Laden der Daten (z.B. über den Command `wget` (s.u.))
- 3. Pfad zu den Daten setzen
Show code cell content
# 🚀 Install libraries
! pip install tqdm pandas numpy spacy bokeh
# 🚀 Load german language model for annotation
! python -m spacy download de_core_news_sm
Show code cell content
# load libraries
import json
import typing
import requests
from pathlib import Path
from time import time
from collections import OrderedDict, Counter
from tqdm import tqdm
import pandas as pd
import numpy as np
import spacy
from bokeh.io import output_notebook, show
from bokeh.layouts import column
from bokeh.models import CustomJS, TextInput, Div
1. Einlesen des Texts#
Um eine Datei mit Python bearbeiten zu können, muss die Datei zuerst ausgewählt werden, d.h der Pfad zur Datei wird gesetzt und dann eingelesen werden.
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Zuerst wird der Ordner angelegt, in dem die Textdateien gespeichert werden. Der Einfachheit halber wird die gleich Datenablagestruktur wie in dem GitHub Repository, in dem die Daten gespeichert sind, vorausgesetzt. Der Text wird aus GitHub heruntergeladen und in dem Ordner ../data/txt/ abgespeichert. Der Pfad kann in der Variable text_path angepasst werden. Die einzulesenden Daten müssen die Endung `.txt` haben.Show code cell content
# 🚀 Create data directory path
corpus_dir = Path("../data/txt")
if not corpus_dir.exists():
corpus_dir.mkdir()
Show code cell content
# 🚀 Load the txt file from GitHub
! wget https://raw.githubusercontent.com/dh-network/quadriga/refs/heads/main/data/txt/SNP2719372X-19181015-0-0-0-0.txt
# move the file to the data directory
! mv SNP2719372X-19181015-0-0-0-0.txt ../data/txt
# set the path to file to be processed
text_path = Path("../data/txt/SNP2719372X-19181015-0-0-0-0.txt")
# read text and print some parts of the text
if text_path.is_file():
text = text_path.read_text()
print(f"Textauszug:\n {text[10280:10400]}")
else:
print("The file path does not exist. Set the variable text_path to an existing path.")
Textauszug:
irgendeine ins einzelne gehende Erklärung über die Lage abgeben werde. Der nächste Schritt für den Präsidenten Wilson wi
2. Worthäufigkeiten ohne echte Tokenisierung#
2.1 Text in Wörter aufteilen#
Der einfachste Weg einen Text automatisch in Wörter aufzuteilen, ist anzunehmen, das Wörter durch Leerzeichen getrennt sind.
# split the text into words by space
words = text.split()
Prüfen: Wie sieht die Wortliste aus?
# print the 100th up the 120th words
words[100:120]
['respektooller',
'Entfernung,',
'Die',
'beschränkten',
'sich',
'in',
'der',
'Hauptsache',
'de',
'Ortschaften',
'im',
'deutschen',
'Hinter-',
'<->',
'mit',
'Bombengeschwadern',
'anzugreisen.',
'der',
'Zwischenzeit',
'wurde']
Wie viele Wörter gibt es insgesamt?
# print the length of the word list
len(words)
11612
Wie zu sehen ist, hat diese Art der “falschen” Tokenisierung den Nachteil, dass Satzzeichen nicht von Wörtern abgetrennt werden.
Die Wortanzahl ist dementsprechend auch nicht akkurat.
2.2 Anzeigen von Worthäufigkeiten#
Auf Grundlage dieser Wortliste kann trotzdem schon eine erste basale Häufigkeitenabfrage erfolgen. Dafür werden die Wörter zuerst gezählt.
# Count the words with Counter and save the result to a variable
word_frequencies = Counter(words)
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Um die Häufigkeit nur mit Python abzufragen, kann folgende Zeile ausgeführt werden:Show code cell content
# 🚀 get the number of the word "Grippe" in the word frequencies
word_frequencies["Grippe"]
Dann kann die Häufigkeit abgefragt werden:
Show code cell source
# Ensure Bokeh output is displayed in the notebook
output_notebook()
# Convert the dictionary to a JSON string to be passed to javascript
word_freq_json = json.dumps(word_frequencies)
# Create the text input widget
text_input = TextInput(value='', title="Geben Sie ein Wort ein:")
# Create a Div to display the frequency
frequency_display = Div(text="Häufigkeit: ")
# JavaScript callback to update the frequency display
# Only needed for graphical interface
callback = CustomJS(args=dict(frequency_display=frequency_display, text_input=text_input), code=f"""
var word = text_input.value.trim();
console.log('Input value:', word);
// Parse the word frequency dictionary from Python
var word_freq = {word_freq_json};
var frequency = word in word_freq ? word_freq[word] : "not found";
frequency_display.text = "Häufigkeit: " + frequency;
""")
text_input.js_on_change('value', callback)
# Layout and display
layout = column(text_input, frequency_display)
show(layout)
3. Annotation mit spaCy#
Um eine präzisere Einteilung in Wörter zu erhalten (Tokenisierung) und um flektierte Wörter aufeinander abbildbar zu machen (Lemmatisierung), wird der Text im folgenden durch die Bibliothek spaCy annotiert. Dafür werden folgende Schritte ausgeführt:
Das sprachspezifische Modell wird geladen. Wir arbeiten mit dem weniger akkuraten aber schnellsten spaCy Modell
de_core_news_sm
.Für eine erhöhte Annotationsgeschwindigkeit werden nur bestimmte Analysekomponenten geladen. Dies ist vor allem für größere Textmengen sinnvoll.
Der Text wird annotiert und die Token sowie die dazugehörigen Lemmata werden extrahiert.
3.1 Sprachmodell laden#
Das sprachspezifische Modell wird geladen. Es handelt sich dabei um das am wenigsten akkurate aber schnellste Modell.
nlp = spacy.load('de_core_news_sm')
3.2 Analysekomponenten auswählen#
Es werden einige Analysekomponent wie z. B. das Aufteilen des Texts in Sätze (sentencizer) oder die Named Entity Recognition (ner) ausgeschlossen, da diese für die Tokenisierung und die Lemmatisierung nicht benötigt werden. Der Auschluss der Komponentnen erhöht die Annotationsgeschwindikgeit.
disable_components = ['ner', 'morphologizer', 'attribute_ruler', 'sentencizer']
3.3 Annotieren der Texte: Token, Lemma#
Der ausgewählte Text wird mit spaCy annotiert und die Token sowie die dazugehörigen Lemmata werden extrahiert und in einer Tabelle gespeichert. Das Tabellenformat wurde gewählt, da sich darin gut relationale Daten speichern lassen.
# get the current time to display how long the annotation took
current = time()
# annotate with spacy
doc = nlp(text)
# extract tokens and lemmata, save them to a dictionary
text_annotated = {}
text_annotated['Token'] = [tok.text for tok in doc]
text_annotated['Lemma'] = [tok.lemma_ for tok in doc]
# convert the dictionary to a dataframe
text_annotated_df = pd.DataFrame(text_annotated)
# calculate how long the annotation and extraction took and print result
took = time() - current
print(f"Die Annotation hat {round(took, 2)} Sekunden gedauert.")
Die Annotation hat 1.4 Sekunden gedauert.
Auszug aus der Tabelle, in der der annotierte Text gespeichert ist:
# print first five lines of the annotation
text_annotated_df.head()
Token | Lemma | |
---|---|---|
0 | BERLINER | BERLINER |
1 | ||
2 | R | R |
3 | M | M |
4 | AHIRE | AHIRE |
3.4 Worthäufigkeit mit echter Tokenization#
Durch die Tokenisierung wurden z. B. Satzzeichen von Wörtern abgetrennt. An der Textlänge lässt sich dies schon erkennen.
# get the lemmata
text_tokenized = text_annotated_df.Lemma
# print the length
len(text_tokenized)
15062
Auf Grundlage des tokenisierten und lemmatisierten Texts, kann die Häufigkeitenabfrage erneut augeführt werden. Da durch die Lemmatisierung flektierte Wortformen auf die Grundformen zurückgeführt wurden, erwarten wir, dass die Häufigkeit einer Wortgrundform im Gegensatz zur vorherigen Abfrage erhöht ist.
# Count the words with Counter and save the result to a variable
token_frequencies = Counter(text_tokenized)
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Um die Häufigkeit nur mit Python abzufragen, kann folgende Zeile ausgeführt werden:Show code cell content
# 🚀 get the number of the word "Grippe" in the word frequencies
token_frequencies["Grippe"]
Show code cell source
# Ensure Bokeh output is displayed in the notebook
output_notebook()
# Convert the dictionary to a JSON string
tok_freq_json = json.dumps(token_frequencies)
# Create the text input widget
token_input = TextInput(value='', title="Geben Sie ein Wort ein:")
# Create a Div to display the frequency
token_frequency_display = Div(text="Häufigkeit: ")
# JavaScript callback to update the frequency display
# Only needed for graphical interface
tok_callback = CustomJS(args=dict(frequency_display=token_frequency_display, text_input=token_input), code=f"""
var tok = text_input.value.trim();
console.log('Input value:', tok);
// Parse the word frequency dictionary from Python
var word_freq = {tok_freq_json};
var frequency = tok in word_freq ? word_freq[tok] : "0";
frequency_display.text = "Häufigkeit: " + frequency;
""")
token_input.js_on_change('value', tok_callback)
# Layout and display
layout = column(token_input, token_frequency_display)
show(layout)
4. Annotation speichern#
Um den annotierten Text zu speichern, wird zuerst der Dateiname festgelegt. Dafür wird die Dateiendung ersetzt von .txt
zu .csv
.
CSV (comma-separated value) ist das Standardformat um tabellarische Daten im Klartext zu speichern.
Informationen zum Ausführen des Notebooks
Der Pfad zum Schreiben der Ergebnisse wird hier auf den selben Ordner gesetzt, in dem das Notebook liegt. So wird nicht von einer bestimmten Ordner-Struktur ausgegangen, wie in der Code-Zeile danach. Dort wird davon ausgeganen, dass auf der selben Höhe des Ordners, in dem das Notebook liegt, ein Ordner `data` existiert, in dem ein Ordner `csv` vorhanden ist. In dem Ordner `csv` wird die Annotation gespeichert. ⚠️ Die nächste Zeile, in der der Pfad noch einmal gesetzt wird, muss übersprungen werden.# set output path to current directory
output_path = Path.cwd() / text_path.with_suffix(".csv").name
# set output path, change file extension
output_path = Path(r"../data/csv") / text_path.with_suffix(".csv").name
Der Text wird dann unter dem festgelegten Dateinamen gespeichert.
# save the annotation as csv
text_annotated_df.to_csv(output_path, index=False)
5. Prozess für das gesamte Korpus ausführen#
def stream_texts_from_directory(corpus_filepaths: list[Path]) -> typing.Generator[str, None, None]:
"""A generator that yields texts from files in the file list one by one."""
for filepath in corpus_filepaths:
yield filepath.read_text()
def process_corpus(corpus_dir: Path, output_dir: Path) -> None:
"""
Reads files from corpus_dir, annotates the files with spacy and writes the result
to the output_dir
:param Path corpus_dir: The directory in which the txt files are saved
:param Path corpus_dir: The directory in which the annotations are written to as csv
"""
corpus_filepaths = [f for f in corpus_dir.iterdir() if f.is_file() and f.suffix == ".txt"]
start = time()
for filepath, doc in zip(corpus_filepaths, nlp.pipe(stream_texts_from_directory(corpus_filepaths))):
print(filepath)
# Save the token and lemma information to a dictionary
text_annotated = {}
text_annotated['Token'] = [tok.text for tok in doc]
text_annotated['Lemma'] = [tok.lemma_ for tok in doc]
output_path = output_dir / filepath.with_suffix(".csv").name
text_annotated.to_csv(output_path, index=False)
end = time.time()
print(f"""Processed {len(corpus_filepaths)} texts with spacy.
Took {round((end - start) / 60, 4)} minutes in total.""")
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Im folgenden werden alle Textdateien im Korpus heruntergeladen und gespeichert. Dafür sind folgende Schritte nötig:- Es wird eine List erstellt, die die URLs zu den einzelnen Textdateien beinhaltet.
- Die Liste wird als txt-Datei gespeichert.
- Alle Dateien aus der Liste werden heruntergeladen und in dem Ordner ../data/txt gespeichert.
Show code cell content
# 🚀 Create download list
github_api_txt_dir_path = "https://api.github.com/repos/dh-network/quadriga/contents/data/txt"
txt_dir_info = requests.get(github_api_txt_dir_path).json()
url_list = [entry["download_url"] for entry in txt_dir_info]
# 🚀 Write download list to the
url_list_path = Path("github_txt_file_urls.txt")
with url_list_path.open('w') as output_txt:
output_txt.write("\n".join(url_list))
Show code cell content
# ⚠️ Only execute, if you haven't downloaded the files yet!
# 🚀 Download all txt files – this step will take a while
! wget -i github_txt_file_urls.txt -P ../data/txt
Show code cell content
# 🚀 Create output folder
output_dir = Path(r"../data/csv")
if not output_dir.exists():
output_dir.mkdir()
# Set path to corpus and output dir
corpus_dir = Path(r"../data/txt/")
output_dir = Path(r"../data/csv")
# Read, annotate, write
process_corpus(corpus_dir, output_dir)