Spaces:
Sleeping
Sleeping
File size: 6,762 Bytes
d52dd8c f99e19b d52dd8c f99e19b d52dd8c f99e19b d52dd8c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """
extractor.py β Structured entity & key-value extraction from OCR text.
Features:
- Date extraction (multiple formats)
- Monetary amount and currency detection
- Email / phone / URL extraction
- Invoice / receipt field detection
- PII detection & redaction (Aadhaar, PAN, credit cards, SSN)
- Document type classifier
"""
import re
from typing import Dict, List, Any, Optional
# βββββββββββββββββββββββββββββββββββββββββββββ
# Regex Patterns
# βββββββββββββββββββββββββββββββββββββββββββββ
DATE_PATTERNS = [
r"\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b",
r"\b(\d{4}[/-]\d{1,2}[/-]\d{1,2})\b",
r"\b(\d{1,2}\s+(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+\d{4})\b",
r"\b((?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+\d{1,2},?\s+\d{4})\b",
]
MONEY_PATTERNS = [
r"(?:βΉ|Rs\.?|INR|USD|\$|β¬|Β£|EUR|GBP)\s*[\d,]+(?:\.\d{1,2})?",
r"[\d,]+(?:\.\d{1,2})?\s*(?:βΉ|Rs\.?|INR|USD|EUR|GBP)",
]
EMAIL_PATTERN = r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b"
PHONE_PATTERN = r"(?:\+91[\s\-]?)?[6-9]\d{4}[\s\-]?\d{5}|(?:\+?1[\s\-]?)?\(?\d{3}\)?[\s\-]?\d{3}[\s\-]?\d{4}"
URL_PATTERN = r"https?://[^\s<>\"]+|www\.[^\s<>\"]{2,}"
# PII Patterns
AADHAAR_PATTERN = r"\b[2-9]\d{3}\s?\d{4}\s?\d{4}\b"
PAN_PATTERN = r"\b[A-Z]{5}[0-9]{4}[A-Z]\b"
CREDIT_CARD_PATTERN = r"\b(?:\d{4}[\s\-]?){3}\d{4}\b"
SSN_PATTERN = r"\b\d{3}[\-]\d{2}[\-]\d{4}\b"
PASSPORT_PATTERN = r"\b[A-Z][1-9][0-9]{7}\b"
# Invoice / Receipt fields
INVOICE_FIELDS = {
"invoice_number": r"(?i)\b(?:invoice|inv|bill|order)[\s#:\.-]*([A-Z0-9\-_/]{3,30})",
"po_number": r"(?i)\b(?:p\.?o\.?\s*(?:#|num(?:ber)?)?|purchase\s+order(?:\s*#)?)[\s#:\.-]*([A-Z0-9\-_/]{3,30})",
"gstin": r"(?i)\b(?:GSTIN|GST\s*No\.?)[\s:\.]*([0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z])",
"due_date": r"(?i)\b(?:due\s+date|payment\s+due|pay\s+by)[\s:]*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})",
"total": r"(?i)\b(?:grand\s+total|total\s+due|total\s+amount|amount\s+due|net\s+payable|total)[\s:βΉ$]*([0-9,]+(?:\.[0-9]{2})?)",
"subtotal": r"(?i)\b(?:subtotal|sub-total|taxable\s+amount)[\s:βΉ$]*([0-9,]+(?:\.[0-9]{2})?)",
"tax": r"(?i)\b(?:tax(?:\s*\(\d+%\))?|gst|vat|igst|cgst|sgst)[\s:βΉ$%]*([0-9,]+(?:\.[0-9]{2})?)",
"discount": r"(?i)\b(?:discount|special\s+offer)[\s:βΉ$%-]*([0-9,]+(?:\.[0-9]{2})?)",
}
DOCUMENT_TYPE_KEYWORDS = {
"Invoice / Receipt": ["invoice", "receipt", "bill", "amount due", "gst", "total", "subtotal", "payment"],
"Identity Document": ["aadhaar", "pan", "passport", "driving licence", "voter", "date of birth", "dob", "nationality"],
"Medical / Prescription": ["patient", "diagnosis", "prescription", "mg", "tablet", "dosage", "doctor", "hospital", "lab"],
"Legal / Contract": ["whereas", "party", "agreement", "clause", "indemnify", "jurisdiction", "hereby", "execute"],
"Academic": ["certificate", "degree", "university", "marks", "grade", "cgpa", "semester", "roll no"],
"Business Card": ["ceo", "manager", "director", "email", "linkedin", "website", "@", "pvt ltd", "inc"],
}
def extract_dates(text: str) -> List[str]:
dates = []
for pattern in DATE_PATTERNS:
dates.extend(re.findall(pattern, text, re.IGNORECASE))
return list(set(dates))
def extract_amounts(text: str) -> List[str]:
amounts = []
for pattern in MONEY_PATTERNS:
amounts.extend(re.findall(pattern, text))
return list(set(amounts))
def extract_emails(text: str) -> List[str]:
return list(set(re.findall(EMAIL_PATTERN, text)))
def extract_phones(text: str) -> List[str]:
return list(set(re.findall(PHONE_PATTERN, text)))
def extract_urls(text: str) -> List[str]:
return list(set(re.findall(URL_PATTERN, text)))
def detect_pii(text: str) -> Dict[str, List[str]]:
"""Detect all PII in text and return by category."""
return {
"aadhaar": re.findall(AADHAAR_PATTERN, text),
"pan": re.findall(PAN_PATTERN, text),
"credit_card": re.findall(CREDIT_CARD_PATTERN, text),
"ssn": re.findall(SSN_PATTERN, text),
"passport": re.findall(PASSPORT_PATTERN, text),
"email": extract_emails(text),
"phone": extract_phones(text),
}
def redact_pii(text: str, redact_char: str = "β") -> tuple[str, int]:
"""
Replace detected PII in text with redaction blocks.
Returns (redacted_text, count_redacted).
"""
redacted = text
count = 0
patterns = [
AADHAAR_PATTERN,
PAN_PATTERN,
CREDIT_CARD_PATTERN,
SSN_PATTERN,
PASSPORT_PATTERN,
]
for pattern in patterns:
matches = re.findall(pattern, redacted)
for match in matches:
replacement = redact_char * len(match)
redacted = redacted.replace(match, replacement)
count += 1
return redacted, count
def extract_invoice_fields(text: str) -> Dict[str, str]:
"""Extract structured key-value invoice fields."""
fields = {}
stopwords = {"se", "no", "is", "of", "to", "in", "on", "at", "by", "or", "and", "the", "for"}
for field_name, pattern in INVOICE_FIELDS.items():
match = re.search(pattern, text)
if match:
val = match.group(1).strip() if match.lastindex else match.group(0).strip()
# Eliminate trivial noise and punctuation-only values
clean_val = val.strip(" :#-.")
if len(clean_val) >= 2 and clean_val.lower() not in stopwords:
fields[field_name] = clean_val
return fields
def classify_document(text: str) -> str:
"""Classify the document type based on keyword analysis."""
text_lower = text.lower()
scores = {}
for doc_type, keywords in DOCUMENT_TYPE_KEYWORDS.items():
score = sum(1 for kw in keywords if kw in text_lower)
if score > 0:
scores[doc_type] = score
if not scores:
return "General Document"
return max(scores, key=scores.get)
def extract_all(text: str) -> Dict[str, Any]:
"""Run all extractors and return a consolidated entities dict."""
return {
"document_type": classify_document(text),
"dates": extract_dates(text),
"amounts": extract_amounts(text),
"emails": extract_emails(text),
"phones": extract_phones(text),
"urls": extract_urls(text),
"pii": detect_pii(text),
"invoice_fields": extract_invoice_fields(text),
}
|