| """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 |
|
|
| |
| 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" |
| |
| USER_AGENT = "eldamar-tokenizer/0.1 (Middle-earth name dataset; educational use)" |
|
|
| |
| LINK_RE = re.compile(r"\[\[([^\]]+)\]\]") |
| |
| 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() |
| |
| |
| if not line.startswith("*"): |
| continue |
|
|
| link_match = LINK_RE.search(line) |
| if not link_match: |
| continue |
|
|
| |
| link_body = link_match.group(1) |
| name = link_body.split("|")[-1].strip() |
| if not name: |
| continue |
|
|
| |
| |
| 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: |
| 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() |
|
|