eldamar-tokenizer / src /scrape_locations.py
erenyanic's picture
Add eldamar-tokenizer project
c60ec61 verified
Raw
History Blame Contribute Delete
4.02 kB
"""Fetch Middle-earth place names from Tolkien Gateway.
The site blocks plain page fetches, but it runs MediaWiki, whose API happily
serves the raw wikitext of the A-Z locations index when we send a polite
User-Agent. We ask for one page -- ``Index:Locations`` -- and pull the display
text of every ``*[[link]]`` bullet out of it.
Each index bullet looks like one of:
*[[Abyss]]
*[[Tarlang's Neck|Achad Tarlang]] (Tarlang's Neck)
*[[Ailin (lake in Valinor)]]
*[[Pass of Aglon|Aglon]], Pass of
We keep the link's *display* text (the part after ``|`` when present) plus any
parenthetical that immediately follows the link, and drop trailing prose such as
", Pass of". No normalising happens here -- that is clean_data.py's job -- so
this script simply writes the raw extracted strings, one per line.
Run: python src/scrape_locations.py
Out: data/locations.txt
"""
from __future__ import annotations
import json
import re
import sys
import urllib.parse
import urllib.request
from pathlib import Path
# Repo root is the parent of this src/ directory; data/ lives beside it.
ROOT = Path(__file__).resolve().parents[1]
DATA_DIR = ROOT / "data"
OUTPUT = DATA_DIR / "locations.txt"
API_URL = "https://tolkiengateway.net/w/api.php"
PAGE = "Index:Locations"
# A descriptive User-Agent is required; without one the server returns 403.
USER_AGENT = "eldamar-tokenizer/0.1 (Middle-earth name dataset; educational use)"
# A wikilink: [[target]] or [[target|display]]. We want the display text.
LINK_RE = re.compile(r"\[\[([^\]]+)\]\]")
# A parenthetical sitting right after the closing ]] , e.g. "]] (Mount Doom)".
TRAILING_PAREN_RE = re.compile(r"\]\]\s*\(([^)]+)\)")
def fetch_wikitext() -> str:
"""Return the raw wikitext of the locations index page via the MediaWiki API."""
params = urllib.parse.urlencode(
{
"action": "parse",
"page": PAGE,
"prop": "wikitext",
"format": "json",
"formatversion": "2",
}
)
request = urllib.request.Request(
f"{API_URL}?{params}", headers={"User-Agent": USER_AGENT}
)
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.load(response)
return payload["parse"]["wikitext"]
def extract_location_strings(wikitext: str) -> list[str]:
"""Pull one raw location string from every bullet-list line in the wikitext."""
results: list[str] = []
for raw_line in wikitext.splitlines():
line = raw_line.strip()
# Only the bullet lines (``*[[...]]``) name a location; skip headers,
# table markup (|, !, {|) and blank lines.
if not line.startswith("*"):
continue
link_match = LINK_RE.search(line)
if not link_match:
continue
# Prefer the display half of [[target|display]]; fall back to the target.
link_body = link_match.group(1)
name = link_body.split("|")[-1].strip()
if not name:
continue
# Keep an alternative name given in a trailing parenthetical, so the
# cleaner can later mine it (e.g. "Amon Amarth (Mount Doom)").
paren_match = TRAILING_PAREN_RE.search(line)
if paren_match:
name = f"{name} ({paren_match.group(1).strip()})"
results.append(name)
return results
def main() -> None:
print(f"Fetching '{PAGE}' from Tolkien Gateway ...")
try:
wikitext = fetch_wikitext()
except Exception as error: # noqa: BLE001 -- surface any network/API failure plainly
sys.exit(f"Failed to fetch the locations index: {error}")
names = extract_location_strings(wikitext)
if not names:
sys.exit("No location names were extracted -- the page format may have changed.")
DATA_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text("\n".join(names) + "\n", encoding="utf-8")
print(f"Wrote {len(names)} raw location strings to {OUTPUT.relative_to(ROOT)}")
if __name__ == "__main__":
main()