Legora / db /parsers /consitution /part_parser.py
sai-Rohan's picture
increased complexity of constitution parser
d7ae59e
Raw
History Blame Contribute Delete
6.55 kB
from __future__ import annotations
import re
from typing import List, Dict
class PartParser:
"""
Constitution Part Parser
Handles:
PART I
PART IXA
PART IXB
PART XIVA
Ignores:
Part A States
Part B States
Schedule references
Appendix references
"""
VALID_PARTS = {
"I", "II", "III", "IV", "IVA",
"V", "VI", "VII", "VIII",
"IX", "IXA", "IXB",
"X", "XI", "XII", "XIII",
"XIV", "XIVA",
"XV", "XVI", "XVII",
"XVIII", "XIX", "XX",
"XXI", "XXII"
}
PART_RE = re.compile(
r"(?im)^\s*PART\s+([IVXLCDM]+(?:[A-Z])?)\s*$"
)
# =====================================================
# CLEAN
# =====================================================
@staticmethod
def clean_text(text: str) -> str:
# footnotes
text = re.sub(
r"(?im)^\s*\d+\.\s+(Subs\.|Ins\.|Omitted|Added|Inserted).*?$",
"",
text
)
text = re.sub(
r"(?im)^\s*\d+\.\s+Vide.*?$",
"",
text
)
text = re.sub(
r"(?im)^\s*\d+\.\s+The words.*?$",
"",
text
)
# page separators
text = re.sub(
r"_{5,}",
"",
text
)
# page numbers
text = re.sub(
r"(?m)^\s*\d+\s*$",
"",
text
)
text = re.sub(
r"\n{3,}",
"\n\n",
text
)
return text
# =====================================================
# CUT AFTER MAIN CONSTITUTION
# =====================================================
@staticmethod
def cut_before_schedules(
text: str
) -> str:
match = re.search(
r"(?im)^FIRST\s+SCHEDULE\s*$",
text
)
appendix_match = re.search(
r"(?im)^\s*APPENDIX\s+I\b",
text
)
if appendix_match:
cutoff = appendix_match.start()
elif match:
cutoff = match.start()
else:
cutoff = None
if cutoff:
text = text[:cutoff]
if match:
return text[:match.start()]
return text
# =====================================================
# TITLE EXTRACTION
# =====================================================
def extract_part_title(
self,
text: str,
start: int
) -> str:
chunk = text[
start:start + 250
]
lines = [
line.strip()
for line in chunk.split("\n")
if line.strip()
]
if len(lines) < 2:
return ""
for line in lines[1:]:
if re.match(
r"^\d+[A-Z]{0,3}\.",
line
):
break
if re.match(
r"^PART\s+",
line,
re.I
):
break
if re.match(
r"^CHAPTER\s+",
line,
re.I
):
break
if (
"CONSTITUTION OF INDIA"
in line.upper()
):
continue
if len(line.split()) > 20:
continue
if len(line) > 200:
continue
return line
return ""
# =====================================================
# EXTRACT
# =====================================================
def extract_parts(
self,
text: str
) -> List[Dict]:
text = self.clean_text(text)
text = self.cut_before_schedules(
text
)
matches = list(
self.PART_RE.finditer(text)
)
parts = []
for i, match in enumerate(
matches
):
part_no = (
match.group(1)
.replace("-", "")
.replace(" ", "")
.upper()
)
if (
part_no
not in self.VALID_PARTS
):
continue
start = match.start()
end = (
matches[i + 1].start()
if i + 1 < len(matches)
else len(text)
)
part_title = (
self.extract_part_title(
text,
start
)
)
part_text = (
text[start:end]
.strip()
)
parts.append(
{
"part_no":
part_no,
"part_title":
part_title,
"text":
part_text,
"start":
start,
"end":
end
}
)
return parts
# =====================================================
# VALIDATION
# =====================================================
def validate_parts(
self,
parts: List[Dict]
) -> List[str]:
errors = []
seen = set()
for part in parts:
part_no = (
part["part_no"]
)
if part_no in seen:
errors.append(
f"Duplicate Part "
f"{part_no}"
)
seen.add(part_no)
if not (
part["part_title"]
):
errors.append(
f"Missing title "
f"for Part "
f"{part_no}"
)
return errors
# =========================================================
# TEST
# =========================================================
if __name__ == "__main__":
with open(
"../../pdfs/constitution.txt",
"r",
encoding="utf8"
) as f:
text = f.read()
parser = PartParser()
parts = parser.extract_parts(
text
)
print(
f"Parts Found: "
f"{len(parts)}"
)
print()
for part in parts:
print(
f"{part['part_no']} "
f"-> "
f"{part['part_title']}"
)