You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
5.5 KiB
167 lines
5.5 KiB
""" |
|
Läser in anföranden från JSON-filer till PostgreSQL. |
|
|
|
Ersätter scripts/documents_to_arango.py. |
|
|
|
Används av sync_talks.py (update_folder) och kan köras direkt för att |
|
(om)ladda alla mappar i speeches/: |
|
|
|
python scripts/documents_to_postgres.py |
|
""" |
|
from pathlib import Path |
|
|
|
import json |
|
import logging |
|
import os |
|
import re |
|
import sys |
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
|
import bootstrap # noqa: E402,F401 — sets cwd and sys.path to the project root |
|
|
|
from postgres_client import pg |
|
|
|
logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s") |
|
|
|
|
|
def clean_text(text: str) -> str: |
|
if text is None: |
|
return "" |
|
text = text.replace("<br>", "\n").replace("<br/>", "\n").replace("<br />", "\n") |
|
text = text.replace("</p>", "\n") |
|
text = re.sub(r"\n+", "\n", text) |
|
text = text.strip() |
|
text = re.sub(r"<.*?>", "", text) |
|
return text |
|
|
|
|
|
def clean_speaker_name(name: str) -> str: |
|
if name is None: |
|
return "" |
|
name = name.strip() |
|
name = re.sub(r"\s*\(.*?\)\s*$", "", name) |
|
return name.strip() |
|
|
|
|
|
def _parse_date(s: str) -> str | None: |
|
if not s: |
|
return None |
|
return str(s).split(" ")[0][:10] or None |
|
|
|
|
|
def process_folder(folder_path: str, already_processed: set[str] = frozenset()) -> list[dict]: |
|
"""Parse JSON files in folder_path, return list of talk dicts (skipping known IDs).""" |
|
docs = [] |
|
for file in os.listdir(folder_path): |
|
if not file.endswith(".json"): |
|
continue |
|
try: |
|
with open(os.path.join(folder_path, file), "r", encoding="utf-8-sig") as f: |
|
data = json.load(f) |
|
doc = data["anforande"] |
|
source_doc_id = doc.get("dok_id", "") |
|
sequence = doc.get("sequence", "") |
|
speech_id = f"{source_doc_id}-{sequence}" # unique per speech, e.g. "GH09116-16" |
|
if speech_id in already_processed: |
|
continue |
|
doc["year"] = int(doc.get("dok_rm", "0000")[:4]) |
|
doc.pop("dok_rm", None) |
|
doc["text"] = clean_text(doc.get("text", "")) |
|
doc["speaker_name"] = clean_speaker_name(doc.get("speaker_name", "")) |
|
doc["id"] = speech_id |
|
doc["dok_id"] = source_doc_id |
|
doc["source_speech_id"] = doc.get("source_speech_id", "") # UUID, kept for reference |
|
doc["date"] = _parse_date(doc.get("source_datetime", "")) |
|
doc["source_datetime"] = doc.get("source_datetime", "") |
|
doc["title"] = doc.get("dok_titel", "") |
|
doc.pop("dok_titel", None) |
|
doc["sequence"] = int(sequence) if sequence else 0 |
|
doc["source_record_id"] = doc.get("dok_hangar_id", "") |
|
doc.pop("dok_hangar_id", None) |
|
doc["is_reply"] = doc.get("is_reply", "N") == "Y" |
|
doc.pop("source_updated_at", None) |
|
doc.pop("underrubrik", None) |
|
year = doc.get("year") or (int(doc["date"][:4]) if doc.get("date") else None) |
|
doc["year"] = year |
|
docs.append(doc) |
|
except (json.JSONDecodeError, KeyError, ValueError) as e: |
|
logging.warning(f"Skipping {file}: {e}") |
|
return docs |
|
|
|
|
|
_UPSERT_SQL = """ |
|
INSERT INTO speeches ( |
|
id, source_speech_id, source_doc_id, |
|
text, section_title, |
|
sequence, activity_type, |
|
speaker_name, party, person_id, |
|
date, source_datetime, year, year, |
|
related_doc_id, source_doc_number, source_record_id, title, |
|
is_reply |
|
) VALUES %s |
|
ON CONFLICT (id) DO NOTHING |
|
""" |
|
|
|
|
|
def _doc_to_row(doc: dict) -> tuple: |
|
return ( |
|
doc.get("id"), # source_speech_id UUID — primary key |
|
doc.get("source_speech_id"), # same value, kept for reference |
|
doc.get("dok_id"), # debate/protocol document id |
|
doc.get("text"), |
|
doc.get("section_title"), |
|
doc.get("sequence"), |
|
doc.get("activity_type"), |
|
doc.get("speaker_name"), |
|
doc.get("party"), |
|
doc.get("person_id"), |
|
doc.get("date"), |
|
doc.get("source_datetime"), |
|
doc.get("year"), |
|
doc.get("year"), |
|
doc.get("related_doc_id"), |
|
doc.get("source_doc_number"), |
|
doc.get("source_record_id"), |
|
doc.get("title"), |
|
doc.get("is_reply", False), |
|
) |
|
|
|
|
|
def insert_docs(docs: list[dict]) -> None: |
|
if not docs: |
|
return |
|
rows = [_doc_to_row(d) for d in docs if d.get("id")] |
|
if rows: |
|
pg.execute_values(_UPSERT_SQL, rows) |
|
|
|
|
|
def update_folder(path: str, already_processed: set[str] = None) -> int: |
|
""" |
|
Upsert talk documents from JSON files in path into PostgreSQL. |
|
Returns the number of new speeches inserted. |
|
""" |
|
if already_processed is None: |
|
rows = pg.execute("SELECT id FROM speeches") |
|
already_processed = {row["id"] for row in rows} |
|
|
|
docs = process_folder(path, already_processed) |
|
insert_docs(docs) |
|
return len(docs) |
|
|
|
|
|
if __name__ == "__main__": |
|
# Load all folders in speeches/ into PostgreSQL |
|
existing = {row["id"] for row in pg.execute("SELECT id FROM speeches")} |
|
total = 0 |
|
for folder in sorted(os.listdir("speeches")): |
|
path = str(bootstrap.DATA_DIR / 'speeches' / folder) |
|
if not os.path.isdir(path): |
|
continue |
|
print(f"Processing {folder} …", end=" ", flush=True) |
|
docs = process_folder(path, already_processed=existing) |
|
insert_docs(docs) |
|
new_ids = {d["id"] for d in docs if d.get("id")} |
|
existing |= new_ids |
|
total += len(new_ids) |
|
print(f"{len(new_ids)} inserted") |
|
print(f"\nTotal: {total} new speeches inserted")
|
|
|