""" Parser: Converts raw HTML → clean text + structured metadata Output: JSON files in /data/parsed/ """ import json import re from pathlib import Path from bs4 import BeautifulSoup RAW_DIR = Path("data/raw") PARSED_DIR = Path("data/parsed") PARSED_DIR.mkdir(parents=True, exist_ok=True) def clean_text(text: str) -> str: text = re.sub(r'\s+', ' ', text) text = re.sub(r'\n{3,}', '\n\n', text) return text.strip() def parse_legislation(html: str, meta: dict) -> dict: soup = BeautifulSoup(html, "lxml") for tag in soup(["nav", "footer", "script", "style", "header"]): tag.decompose() content_div = ( soup.select_one(".akn-act") or soup.select_one(".document-content") or soup.select_one("main") or soup.body ) raw_text = content_div.get_text(separator="\n") if content_div else "" cleaned = clean_text(raw_text) sections = extract_sections(cleaned) return { "title": meta.get("title", ""), "url": meta.get("url", ""), "type": "legislation", "full_text": cleaned, "sections": sections, "char_count": len(cleaned) } def parse_case_law(html: str, meta: dict) -> dict: soup = BeautifulSoup(html, "lxml") for tag in soup(["nav", "footer", "script", "style", "header"]): tag.decompose() content_div = ( soup.select_one(".akn-judgment") or soup.select_one(".judgment-body") or soup.select_one(".akn-act") or soup.select_one("main") or soup.body ) raw_text = content_div.get_text(separator="\n") if content_div else "" cleaned = clean_text(raw_text) citation = extract_citation(soup) court = extract_court(soup) date = extract_date(soup) return { "title": meta.get("title", ""), "url": meta.get("url", ""), "type": "case_law", "citation": citation, "court": court, "date": date, "full_text": cleaned, "char_count": len(cleaned) } def extract_sections(text: str) -> list[dict]: sections = [] pattern = re.compile( r'(?:^|\n)((?:Section\s+)?\d+[A-Z]?\.)\s+(.+?)(?=\n(?:(?:Section\s+)?\d+[A-Z]?\.|$))', re.DOTALL | re.MULTILINE ) for match in pattern.finditer(text): section_num = match.group(1).strip() section_text = match.group(2).strip() if len(section_text) > 10: sections.append({ "section": section_num, "text": section_text[:2000] }) return sections def extract_citation(soup: BeautifulSoup) -> str: for selector in [".citation", ".case-citation", "h2", "h3"]: el = soup.select_one(selector) if el: text = el.get_text(strip=True) if re.search(r'\[\d{4}\]', text): return text return "" def extract_court(soup: BeautifulSoup) -> str: for selector in [".court-name", ".court", "[class*='court']"]: el = soup.select_one(selector) if el: return el.get_text(strip=True) return "" def extract_date(soup: BeautifulSoup) -> str: for selector in [".date", ".judgment-date", "[class*='date']"]: el = soup.select_one(selector) if el: return el.get_text(strip=True) return "" def parse_all(): html_files = list(RAW_DIR.glob("*.html")) print(f"Parsing {len(html_files)} files...") for html_path in html_files: meta_path = html_path.with_suffix(".json") if not meta_path.exists(): print(f" No metadata for {html_path.name}, skipping.") continue meta = json.loads(meta_path.read_text()) html = html_path.read_text(encoding="utf-8") try: if meta["type"] == "legislation": parsed = parse_legislation(html, meta) elif meta["type"] == "case_law": parsed = parse_case_law(html, meta) else: continue out_path = PARSED_DIR / html_path.with_suffix(".json").name out_path.write_text(json.dumps(parsed, indent=2, ensure_ascii=False)) print(f" Parsed: {parsed['title'][:50]} ({parsed['char_count']} chars)") except Exception as e: print(f" ERROR parsing {html_path.name}: {e}") print(f"\nParsed files saved to {PARSED_DIR}/") if __name__ == "__main__": parse_all()