← back to Paul Conrad Archive
src/conrad/crawlers/newspaper_probes.py
89 lines
"""Probes of free digitized-newspaper / museum archives that could show Conrad cartoons as printed (TK-12199 image
priority). Each is tested through the project's polite client (robots.txt per path, descriptive UA) and registered
with an honest status. Nothing is bypassed: a robots 'Disallow: /' or a 403 to our UA is recorded as blocked.
Sources that actually yield records live in their own crawlers (ia_fulltext, discovery/DPLA, latimes)."""
from __future__ import annotations
import json
from .. import db
from .base import Blocked, Crawler, Transient
# (source_id, name, probe url, params or None, how)
PROBES = [
("cdnc", "California Digital Newspaper Collection (cdnc.ucr.edu, Veridian)", "https://cdnc.ucr.edu/?a=q&txq=%22Paul+Conrad%22",
None, "plain"),
("colorado_newspapers", "Colorado Historic Newspapers (Veridian)",
"https://www.coloradohistoricnewspapers.org/?a=q&txq=%22Paul+Conrad%22", None, "plain"),
("chronicling_america", "Chronicling America (loc.gov, digitized newspapers to 1963)",
"https://www.loc.gov/collections/chronicling-america/",
{"q": '"paul conrad"', "fo": "json", "c": 100, "dates": "1946/1963"}, "loc_json"),
("nara_artifacts", "JFK Library eMuseum (jfk.artifacts.archives.gov) — 2 Denver Post originals 1961-63",
"https://jfk.artifacts.archives.gov/people/6554/paul-conrad-denver-post/objects", None, "plain"),
("lbj_artifacts", "LBJ Library eMuseum (lbj.artifacts.archives.gov) — ~11 Conrad originals 1964-68",
"https://lbj.artifacts.archives.gov/people/14908/paul-conrad/objects", None, "plain"),
("billy_ireland_dc", "Ohio State Billy Ireland — Digital Collections catalog JSON (library.osu.edu/dc)",
"https://library.osu.edu/dc/catalog.json", {"q": '"paul conrad"', "per_page": 100}, "plain"),
("stanford_daily", "Stanford Daily archive search API (ran Conrad from 1964-09-28 / 1976-11-15)",
"https://ehabp6fuc5.execute-api.us-east-1.amazonaws.com/prod", {"q": '"Paul Conrad"', "size": 50}, "plain"),
("truthdig_api", "Truthdig WordPress REST API (cartoons search)", "https://www.truthdig.com/wp-json/wp/v2/cartoons",
{"search": "conrad", "per_page": 100}, "plain"),
("google_news_archive", "Google News Archive (news.google.com/newspapers)",
"https://news.google.com/newspapers?nid=conrad", None, "plain"),
]
NOTES = {
"cdnc": "robots.txt 'User-agent: * Disallow: /' (only archive.org_bot/bingbot allowed, and even they may not query)",
"colorado_newspapers": "robots.txt 'User-agent: * Disallow: /' (Veridian default); Denver Post 1950-64 not reachable",
"nara_artifacts": "robots.txt allows /people/ and /objects/ (Crawl-delay 30, Disallow /assets/) but the site answers "
"HTTP 403 to this crawler's descriptive UA; a browser UA is NOT substituted (that would be a bypass)",
"lbj_artifacts": "robots.txt allows /people/ and /objects/ (Crawl-delay 30, Disallow /assets/) but the site answers "
"HTTP 403 to this crawler's descriptive UA; a browser UA is NOT substituted (that would be a bypass)",
"google_news_archive": "robots.txt disallows /newspapers for generic agents",
"stanford_daily": "the API host's robots.txt answers 403 -> treated as disallowed (conservative rule)",
"truthdig_api": "robots.txt disallows /wp-json/ (the author page /author/paul_conrad/ is used instead, see truthdig)",
}
class NewspaperProbes(Crawler):
source_id = "newspaper_probes"
name = "Digitized-newspaper / museum archive probes"
repository = None
url = "https://www.loc.gov/collections/chronicling-america/"
classification = "PUBLIC_HTML"
access_notes = "Reachability probes only; each probed archive is registered as its own source."
def crawl(self) -> None:
summary = []
for sid, name, url, params, how in PROBES:
status, cls, note = "worked", "PUBLIC_HTML", ""
try:
st, body = self.http.get(url, params=params)
self.stats["pages"] += 1
if st != 200:
status, note = "failed", f"HTTP {st}"
elif how == "loc_json":
d = json.loads(body)
n = (d.get("pagination") or {}).get("of") or 0
cls = "PUBLIC_API"
note = (f"q=\"paul conrad\" 1946-1963: {n} page hit(s); Conrad drew for the Denver Post (not in "
f"Chronicling America) and was syndicated only from 1964 -> 0 records")
if n:
note += "; hits: " + "; ".join(f"{r.get('date')} {str(r.get('title'))[:50]}"
for r in (d.get("results") or [])[:10])
else:
note = "reachable (HTTP 200); no parser for item-level results"
except Blocked as e:
status, cls = "blocked", "REQUIRES_PERMISSION"
note = f"{NOTES.get(sid, '')} ({e})".strip()
except Transient as e: # timeouts / 5xx after retries: unmeasured, NOT blocked
status, cls = "failed", ("PUBLIC_API" if how == "loc_json" else "PUBLIC_HTML")
note = f"not measured — endpoint timed out / errored after retries ({e})"
db.upsert_source(self.conn, sid, name, url=url, classification=cls, status=status, notes=note[:1500])
summary.append(f"{sid}={status}")
self.conn.commit()
self.notes.append("probes: " + ", ".join(summary))
CRAWLER = NewspaperProbes