Source:scans/baldessari-sprengel.pdf Purpose: Extract structured artwork and artist data from a scanned exhibition catalogue into a CSV ready for Wikibase import.
1 Workflow
Stage
Script
Description
1
scripts/ocr_catalogue.py
Render PDF pages → run Tesseract OCR → save page texts
2
scripts/extract_artworks.py
Parse OCR text → extract artwork rows → save CSV
3
scripts/validate_csv.py
Validate CSV against data models → quality report
4
(future)
Generate DTD + XML for Wikibase import
2 Data Models
The output CSV columns align with three Linked Open Exhibition data models: - data-models/item-in-exhibition.csv – artwork fields - data-models/artists-data-model.csv – artist fields - data-models/exhibition.csv – exhibition-level metadata
Note: After Stage 3, review the CSV in a spreadsheet before proceeding to XML generation.
3 0. Setup
Install dependencies and configure paths. Run this cell first.
In [4]:
Show code
import subprocess, sys# Install OCR and image-processing packages if not already presentpackages = ["pdf2image", "pytesseract", "Pillow", "pymupdf"]for pkg in packages:try:__import__(pkg.replace("-", "_").split("[")[0])exceptImportError:print(f"Installing {pkg} ...") subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", pkg])print("All dependencies available.")
Installing Pillow ...
All dependencies available.
In [5]:
Show code
from pathlib import Pathdef _find_repo_root() -> Path:"""Walk up from CWD until a directory containing _quarto.yml is found.""" cwd = Path(".").resolve()for candidate in [cwd] +list(cwd.parents):if (candidate /"_quarto.yml").exists():return candidate# Fallback: assume notebook lives one directory below the repo rootreturn cwd.parentREPO_ROOT = _find_repo_root()PDF_PATH = REPO_ROOT /"scans"/"baldessari-sprengel.pdf"OUTPUT_DIR = REPO_ROOT /"output"/"ocr"CSV_OUTPUT = REPO_ROOT /"output"/"baldessari-sprengel-artworks.csv"SCRIPTS_DIR = REPO_ROOT /"scripts"# ---- Exhibition metadata ----EXHIBITION_TITLE ="Baldessari. Sprengel Museum Hannover"EXHIBITION_LOCATION ="Sprengel Museum Hannover"EXHIBITION_START =""# fill in if known, e.g. "2000-09-17"EXHIBITION_END =""# fill in if knownEXHIBITION_ORGANIZER ="Sprengel Museum Hannover"# ---- OCR settings ----OCR_LANG ="deu+eng"# Tesseract language(s)OCR_DPI =300# higher DPI = better quality but slowerPAGE_RANGE =None# None = all pages; or e.g. "1-30"assert PDF_PATH.exists(), f"PDF not found: {PDF_PATH}"print(f"PDF : {PDF_PATH}")print(f"Output dir : {OUTPUT_DIR}")print(f"CSV output : {CSV_OUTPUT}")
PDF : C:\git\linked-open-exhibition\scans\baldessari-sprengel.pdf
Output dir : C:\git\linked-open-exhibition\output\ocr
CSV output : C:\git\linked-open-exhibition\output\baldessari-sprengel-artworks.csv
4 1. OCR – Convert PDF Pages to Text
Each page is rendered at OCR_DPI, converted to greyscale, then fed to Tesseract. Already-processed pages are skipped (cached in output/ocr/pages/).
Tesseract must be installed separately on your system.
Windows installer: https://github.com/UB-Mannheim/tesseract/wiki
Make sure the language packs deu and eng are included.
This step can take several minutes for a long catalogue.
In [2]:
Show code
# Check Tesseract installation before running OCRimport shutil, subprocess, systesseract_bin = shutil.which("tesseract")if tesseract_bin: result = subprocess.run(["tesseract", "--version"], capture_output=True, text=True)print(f"Tesseract found: {tesseract_bin}")print(result.stdout.splitlines()[0] if result.stdout else result.stderr.splitlines()[0])else:print("="*60)print("TESSERACT NOT FOUND ON PATH")print("="*60)print()print("This PDF is a pure image scan and requires Tesseract OCR.")print()print("Install steps (Windows):")print(" 1. Download installer from:")print(" https://github.com/UB-Mannheim/tesseract/wiki")print(" 2. During install, tick 'German' and 'English' language packs.")print(" 3. Add the install folder to your PATH, e.g.:")print(" C:\\Program Files\\Tesseract-OCR")print(" 4. Restart this notebook kernel (Kernel > Restart).")print()print("Alternatively, set the path explicitly in the next cell:")print(" import pytesseract")print(" pytesseract.pytesseract.tesseract_cmd = r'C:\\Program Files\\Tesseract-OCR\\tesseract.exe'")
============================================================
TESSERACT NOT FOUND ON PATH
============================================================
This PDF is a pure image scan and requires Tesseract OCR.
Install steps (Windows):
1. Download installer from:
https://github.com/UB-Mannheim/tesseract/wiki
2. During install, tick 'German' and 'English' language packs.
3. Add the install folder to your PATH, e.g.:
C:\Program Files\Tesseract-OCR
4. Restart this notebook kernel (Kernel > Restart).
Alternatively, set the path explicitly in the next cell:
import pytesseract
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
In [9]:
Show code
import os# Full path to tesseract.exe - adjust if installed elsewhereTESSERACT_CMD = os.path.join( os.environ.get("LOCALAPPDATA", r"C:\Users\worthingtons\AppData\Local"),r"Programs\Tesseract-OCR\tesseract.exe")if os.path.exists(TESSERACT_CMD):print(f"Tesseract found: {TESSERACT_CMD}")else:print(f"WARNING: tesseract.exe not found at {TESSERACT_CMD}") TESSERACT_CMD =None
import sysimport importlibsys.path.insert(0, str(SCRIPTS_DIR))# Force reload so edits to the script file are picked up without restarting the kernelimport ocr_catalogueimportlib.reload(ocr_catalogue)from ocr_catalogue import ocr_pdffull_txt_path = ocr_pdf( pdf_path=PDF_PATH, output_dir=OUTPUT_DIR, lang=OCR_LANG, dpi=OCR_DPI, page_range=PAGE_RANGE, tesseract_cmd=TESSERACT_CMD,)print(f"\nFull OCR text: {full_txt_path}")
Review a sample page to assess OCR quality before proceeding.
In [4]:
Show code
# Show the OCR text for a specific page (change page number as needed)SAMPLE_PAGE =5sample_file = OUTPUT_DIR /"pages"/f"page_{SAMPLE_PAGE:04d}.txt"if sample_file.exists():print(f"--- Page {SAMPLE_PAGE} ---")print(sample_file.read_text(encoding="utf-8"))else:print(f"Page {SAMPLE_PAGE} not found. Check that OCR ran successfully.")
Page 5 not found. Check that OCR ran successfully.
In [5]:
Show code
# Show total pages processedpages_dir = OUTPUT_DIR /"pages"page_files =sorted(pages_dir.glob("page_*.txt"))print(f"Pages processed: {len(page_files)}")# Character-level OCR statisticstotal_chars =sum(len(p.read_text(encoding='utf-8')) for p in page_files)avg_chars = total_chars //len(page_files) if page_files else0print(f"Total characters extracted: {total_chars:,}")print(f"Average characters per page: {avg_chars:,}")# Flag suspiciously short pages (may be blank or image-only)short_pages = [p.stem for p in page_files iflen(p.read_text(encoding='utf-8')) <100]if short_pages:print(f"\nShort pages (possibly blank/image-only): {short_pages}")
5 2. Extract Artwork Records
Parse the OCR text into structured rows using the Linked Open Exhibition data models.
# Preview as a DataFrameimport pandas as pdfrom dataclasses import asdictdf = pd.DataFrame([asdict(r) for r in records])# Show key columns only for readabilitypreview_cols = ["catalogue_no", "artwork_title", "artist_full_name","artwork_inception", "medium_material","height_cm", "width_cm", "collection", "source_page"]df[preview_cols].head(20)
In [9]:
Show code
# Save the CSVwrite_csv(records, CSV_OUTPUT)print(f"CSV written: {CSV_OUTPUT}")
6 3. Validate the CSV
Check completeness and flag uncertain values before manual review.
In [11]:
Show code
from validate_csv import validatevalidate(CSV_OUTPUT)
6.1 3a. Field Completeness Chart
In [13]:
Show code
import pandas as pddf = pd.read_csv(CSV_OUTPUT, encoding="utf-8-sig")# Completeness: fraction of non-empty values per columncompleteness = (df.replace("", pd.NA).notna().mean() *100).sort_values(ascending=False)try:import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) completeness.plot(kind="barh", ax=ax, color="steelblue") ax.set_xlabel("% rows with value") ax.set_title("Field Completeness") ax.axvline(80, color="orange", linestyle="--", label="80% threshold") ax.legend() plt.tight_layout() plt.show()exceptImportError:print(completeness.to_string())
6.2 3b. Rows Needing Review
Rows that still carry [?] markers or have empty required fields.
Open output/baldessari-sprengel-artworks.csv in a spreadsheet and:
Correct OCR artefacts in artwork_title, medium_material, collection etc.
Remove [?] suffixes from corrected values.
Fill in artwork_inception, height_cm, width_cm where missing.
Add artist_wikidata_qid and artist_gnd_id for known artists.
Verify catalogue_no matches the printed catalogue numbering.
Once corrected, re-run Stage 3 (validate) until the quality score is satisfactory.
8 5. Next Steps: DTD and XML for Wikibase
When the CSV quality is high enough, the following will be produced:
DTD (output/artwork-catalogue.dtd) – document type definition for the XML structure
XML (output/baldessari-sprengel-import.xml) – one <item> per artwork, ready for Wikibase
These will be created in a future notebook (Stage 4).
In [17]:
Show code
# Quick summary of current stateimport pandas as pddf = pd.read_csv(CSV_OUTPUT, encoding="utf-8-sig")print("=== Current CSV Summary ===")print(f"Total artwork records : {len(df)}")print(f"Unique artists : {df['artist_full_name'].nunique()}")print(f"Rows with dimensions : {(df['height_cm'].fillna('') !='').sum()}")print(f"Rows with medium : {(df['medium_material'].fillna('') !='').sum()}")print(f"Rows with cat. no. : {(df['catalogue_no'].fillna('') !='').sum()}")print(f"Rows with collection : {(df['collection'].fillna('') !='').sum()}")