Search, chat and research over parliamentary speeches and documents
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

"""
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")