iroh / ingestion /parser.py
DaudiAI's picture
Iroh v1 - Kenyan legal research tool
876270a
Raw
History Blame Contribute Delete
4.44 kB
"""
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()