311 lines
10 KiB
Python
311 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Fully automated NFR dictionary generator for all years.
|
|
|
|
This script uses web search and scraping to find announcement pages
|
|
and extract film data for each NFR year from 1989-2023.
|
|
|
|
Usage:
|
|
python3 scripts/generate_all_nfr_years.py
|
|
python3 scripts/generate_all_nfr_years.py --start 2020 --end 2023
|
|
python3 scripts/generate_all_nfr_years.py --year 2015
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
from urllib.parse import quote_plus
|
|
|
|
import requests
|
|
|
|
# Configuration
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
NFR_DATA_DIR = SCRIPT_DIR / "nfr_data"
|
|
FIRST_NFR_YEAR = 1989
|
|
|
|
# Known URLs to speed things up
|
|
KNOWN_ANNOUNCEMENT_URLS = {
|
|
2024: "https://newsroom.loc.gov/news/25-films-named-to-national-film-registry-for-preservation/s/55d5285d-916f-4105-b7d4-7fc3ba8664e3",
|
|
2023: "https://newsroom.loc.gov/news/25-films-selected-for-preservation-in-national-film-registry/s/aa4bef48-95f6-486f-882d-110613633b1e",
|
|
2022: "https://newsroom.loc.gov/news/25-eclectic-films-chosen-for-national-film-registry/s/8c41f7a1-b9d9-4f9e-b252-4795b73a4aaf",
|
|
2021: "https://blogs.loc.gov/now-see-hear/2021/12/librarian-of-congress-adds-25-films-to-the-national-film-registry/",
|
|
2020: "https://blogs.loc.gov/now-see-hear/2020/12/librarian-of-congress-adds-25-films-to-the-national-film-registry/",
|
|
2019: "https://blogs.loc.gov/now-see-hear/2019/12/librarian-of-congress-announces-national-film-registry-selections-for-2019/",
|
|
2018: "https://blogs.loc.gov/now-see-hear/2018/12/librarian-of-congress-announces-national-film-registry-selections-for-2018/",
|
|
2017: "https://blogs.loc.gov/now-see-hear/2017/12/librarian-of-congress-announces-national-film-registry-selections-for-2017/",
|
|
2016: "https://blogs.loc.gov/now-see-hear/2016/12/librarian-of-congress-announces-2016-national-film-registry-selections/",
|
|
2015: "https://blogs.loc.gov/now-see-hear/2015/12/announcing-the-2015-national-film-registry-selections/",
|
|
2014: "https://blogs.loc.gov/now-see-hear/2014/12/announcing-the-2014-national-film-registry-selections/",
|
|
2013: "https://blogs.loc.gov/now-see-hear/2013/12/announcing-the-2013-national-film-registry-selections/",
|
|
2012: "https://blogs.loc.gov/now-see-hear/2012/12/announcing-the-2012-national-film-registry-selections/",
|
|
2011: "https://blogs.loc.gov/now-see-hear/2011/12/announcing-the-2011-national-film-registry-selections/",
|
|
2010: "https://blogs.loc.gov/now-see-hear/2010/12/announcing-the-2010-national-film-registry-selections/",
|
|
2009: "https://blogs.loc.gov/now-see-hear/2009/12/announcing-the-2009-national-film-registry-selections/",
|
|
2008: "https://blogs.loc.gov/now-see-hear/2008/12/announcing-the-2008-national-film-registry-selections/",
|
|
2007: "https://blogs.loc.gov/now-see-hear/2007/12/announcing-the-2007-national-film-registry-selections/",
|
|
}
|
|
|
|
|
|
def search_for_announcement_url(year):
|
|
"""
|
|
Try to find the announcement URL for a given year using various strategies.
|
|
"""
|
|
print(f" Searching for {year} announcement URL...")
|
|
|
|
if year in KNOWN_ANNOUNCEMENT_URLS:
|
|
print(f" Using known URL for {year}")
|
|
return KNOWN_ANNOUNCEMENT_URLS[year]
|
|
|
|
# Try common URL patterns
|
|
# Pattern 1: blogs.loc.gov (most common for older years)
|
|
year_patterns = [
|
|
f"https://blogs.loc.gov/now-see-hear/{year}/12/announcing-the-{year}-national-film-registry-selections/",
|
|
f"https://blogs.loc.gov/now-see-hear/{year}/12/librarian-of-congress-announces-{year}-national-film-registry-selections/",
|
|
f"https://blogs.loc.gov/now-see-hear/{year}/12/announcing-the-{year}-national-film-registry/",
|
|
]
|
|
|
|
for url in year_patterns:
|
|
try:
|
|
resp = requests.head(url, timeout=10, allow_redirects=True)
|
|
if resp.status_code == 200:
|
|
print(f" Found URL: {url}")
|
|
return url
|
|
except Exception:
|
|
pass
|
|
|
|
# If we can't find it automatically, return None
|
|
print(f" Could not find announcement URL for {year} - will need manual search")
|
|
return None
|
|
|
|
|
|
def fetch_page_content(url):
|
|
"""Fetch HTML content from a URL."""
|
|
try:
|
|
resp = requests.get(url, timeout=30)
|
|
resp.raise_for_status()
|
|
return resp.text
|
|
except Exception as e:
|
|
print(f" Error fetching {url}: {e}")
|
|
return None
|
|
|
|
|
|
def extract_films_basic(html, year):
|
|
"""
|
|
Basic extraction of films from HTML content.
|
|
This is a simple heuristic-based approach.
|
|
"""
|
|
films = {}
|
|
|
|
# Look for film title patterns: "Title" (Year) or Title (Year)
|
|
# This is very basic and may need refinement
|
|
patterns = [
|
|
r'"([^"]+)"\s*\((\d{4})\)', # "Title" (Year)
|
|
r'<strong>([^<]+)</strong>\s*\((\d{4})\)', # <strong>Title</strong> (Year)
|
|
r'<b>([^<]+)</b>\s*\((\d{4})\)', # <b>Title</b> (Year)
|
|
]
|
|
|
|
found_titles = set()
|
|
|
|
for pattern in patterns:
|
|
matches = re.findall(pattern, html)
|
|
for title, film_year in matches:
|
|
title = title.strip()
|
|
film_year = int(film_year)
|
|
|
|
# Sanity checks
|
|
if (len(title) > 3 and
|
|
1890 <= film_year <= year and
|
|
title not in found_titles):
|
|
|
|
films[title] = {
|
|
"year": film_year,
|
|
"description": f"[Description needs to be added for this {year} NFR inductee]"
|
|
}
|
|
found_titles.add(title)
|
|
|
|
return films
|
|
|
|
|
|
def generate_nfr_dict_file(year, films, source_url=""):
|
|
"""Generate a Python file containing the NFR dictionary for a year."""
|
|
if not films:
|
|
print(f" No films to generate for {year}")
|
|
return None
|
|
|
|
output_path = NFR_DATA_DIR / f"nfr_{year}.py"
|
|
|
|
# Build the Python code
|
|
code = f'''# {year} National Film Registry inductees
|
|
# Source: {source_url if source_url else "[Add source URL]"}
|
|
# Generated automatically - descriptions may need review/enhancement
|
|
|
|
NFR_{year} = {{
|
|
'''
|
|
|
|
for title, data in films.items():
|
|
# Escape single quotes in strings
|
|
title_escaped = title.replace("'", "\\'").replace('"', '\\"')
|
|
desc_escaped = data["description"].replace("'", "\\'").replace('"', '\\"')
|
|
|
|
code += f''' "{title_escaped}": {{
|
|
"year": {data["year"]},
|
|
"description": "{desc_escaped}"
|
|
}},
|
|
'''
|
|
|
|
code += "}\n"
|
|
|
|
# Save the file
|
|
output_path.write_text(code)
|
|
print(f" ✓ Saved {len(films)} films to {output_path.relative_to(SCRIPT_DIR)}")
|
|
|
|
return output_path
|
|
|
|
|
|
def process_year(year, force=False):
|
|
"""Process a single year: find URL, fetch content, extract films, generate file."""
|
|
print(f"\n{'='*60}")
|
|
print(f"Processing NFR {year}")
|
|
print(f"{'='*60}")
|
|
|
|
# Check if already exists
|
|
output_file = NFR_DATA_DIR / f"nfr_{year}.py"
|
|
if output_file.exists() and not force:
|
|
print(f" ⚠️ {output_file.name} already exists (use --force to overwrite)")
|
|
return False
|
|
|
|
# Find announcement URL
|
|
url = search_for_announcement_url(year)
|
|
if not url:
|
|
print(f" ✗ Skipping {year} - no URL found")
|
|
print(f" You can manually process this year with:")
|
|
print(f" python3 scripts/setup_nfr.py {year}")
|
|
return False
|
|
|
|
# Fetch content
|
|
html = fetch_page_content(url)
|
|
if not html:
|
|
print(f" ✗ Could not fetch content for {year}")
|
|
return False
|
|
|
|
# Extract films
|
|
print(f" Extracting films...")
|
|
films = extract_films_basic(html, year)
|
|
|
|
if not films:
|
|
print(f" ✗ Could not extract films from {year}")
|
|
print(f" The page format may require manual processing:")
|
|
print(f" python3 scripts/setup_nfr.py {year} --url \"{url}\"")
|
|
return False
|
|
|
|
print(f" Found {len(films)} films")
|
|
|
|
# Generate file
|
|
output_file = generate_nfr_dict_file(year, films, url)
|
|
|
|
if output_file:
|
|
print(f" ✓ Successfully processed {year}")
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate NFR dictionaries for all years"
|
|
)
|
|
parser.add_argument(
|
|
"--start",
|
|
type=int,
|
|
default=FIRST_NFR_YEAR,
|
|
help=f"Start year (default: {FIRST_NFR_YEAR})"
|
|
)
|
|
parser.add_argument(
|
|
"--end",
|
|
type=int,
|
|
default=2023,
|
|
help="End year (default: 2023)"
|
|
)
|
|
parser.add_argument(
|
|
"--year",
|
|
type=int,
|
|
help="Process a single year"
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Overwrite existing files"
|
|
)
|
|
parser.add_argument(
|
|
"--delay",
|
|
type=float,
|
|
default=1.0,
|
|
help="Delay between requests in seconds (default: 1.0)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Create output directory
|
|
NFR_DATA_DIR.mkdir(exist_ok=True)
|
|
|
|
# Determine years to process
|
|
if args.year:
|
|
years = [args.year]
|
|
else:
|
|
years = list(range(args.start, args.end + 1))
|
|
|
|
print(f"Will process {len(years)} years: {min(years)}-{max(years)}")
|
|
print(f"Output directory: {NFR_DATA_DIR}")
|
|
print(f"Force overwrite: {args.force}")
|
|
|
|
# Process years
|
|
successful = []
|
|
failed = []
|
|
skipped = []
|
|
|
|
for i, year in enumerate(years):
|
|
# Add delay between requests to be polite
|
|
if i > 0:
|
|
time.sleep(args.delay)
|
|
|
|
result = process_year(year, force=args.force)
|
|
|
|
if result is True:
|
|
successful.append(year)
|
|
elif result is False:
|
|
# Check if it was skipped vs failed
|
|
output_file = NFR_DATA_DIR / f"nfr_{year}.py"
|
|
if output_file.exists():
|
|
skipped.append(year)
|
|
else:
|
|
failed.append(year)
|
|
|
|
# Summary
|
|
print(f"\n{'='*60}")
|
|
print("SUMMARY")
|
|
print(f"{'='*60}")
|
|
print(f"\n✓ Successfully generated: {len(successful)} files")
|
|
if successful:
|
|
print(f" Years: {successful}")
|
|
|
|
if skipped:
|
|
print(f"\n⊘ Skipped (already exist): {len(skipped)}")
|
|
print(f" Years: {skipped}")
|
|
|
|
if failed:
|
|
print(f"\n✗ Failed/Need manual processing: {len(failed)}")
|
|
print(f" Years: {failed}")
|
|
print(f"\n Process these manually with:")
|
|
for year in failed:
|
|
print(f" python3 scripts/setup_nfr.py {year}")
|
|
|
|
print(f"\n📁 Generated files: {NFR_DATA_DIR}/")
|
|
print(f"\nNote: Generated files use basic extraction and may need review.")
|
|
print(f"For better results with descriptions, use setup_nfr.py with ollama.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|