Untitled
unknown
plain_text
9 months ago
49 kB
31
Indexable
# pdf_handler.py
# Utilities for extracting text sections from PDFs and chunking them for Telegram
from typing import List, Dict
from pathlib import Path
import re
from PyPDF2 import PdfReader
def _clean_line(line: str) -> str:
return line.strip().replace('\xa0', ' ')
def extract_sections_from_pdf(pdf_path: str) -> List[Dict]:
"""Extracts logical sections from a single-page or multi-page editorial PDF.
Returns a list of dicts: { 'title': str, 'text': str }
This implementation is intentionally conservative: it will try to remove
obvious headers/footers (short repeated lines), and then split the body into
sections by detecting lines that look like titles (short, uppercase/Title-case)
or large whitespace breaks.
"""
p = Path(pdf_path)
reader = PdfReader(str(p))
pages = []
for page in reader.pages:
raw = page.extract_text() or ""
pages.append(raw)
# Merge pages into single text for processing
full_text = "\n\n".join(pages)
# Heuristics: remove repeated header/footer
lines = [l for l in (l.rstrip() for l in full_text.splitlines()) if l.strip()]
# find short lines that appear on multiple pages -> header/footer candidates
counts = {}
for i, line in enumerate(lines):
key = line.strip()
if len(key) < 120:
counts[key] = counts.get(key, 0) + 1
repeated = {s for s, c in counts.items() if c > 1}
cleaned_lines = [l for l in lines if l.strip() not in repeated]
# Rejoin and split by two or more newlines or by evident title lines
cleaned_text = "\n".join(cleaned_lines)
# Candidate title regex (very short lines, many capitals or leading dates)
# We'll scan and split where appropriate.
parts = []
buffer = []
current_title = None
def flush():
nonlocal buffer, current_title
if buffer:
txt = "\n".join(buffer).strip()
if txt:
parts.append({
"title": current_title or "Untitled",
"text": txt,
})
buffer = []
current_title = None
for line in cleaned_text.splitlines():
l = line.strip()
# Heuristic for title line: short (<60), mostly uppercase or titlecase and not a normal sentence
is_title = False
if 1 <= len(l) <= 60:
# many uppercase letters or starts with a date-ish token
uppercase_ratio = sum(1 for ch in l if ch.isupper()) / max(1, len(l))
if uppercase_ratio > 0.35 or re.match(r"^\d{1,2}\s|^[A-Za-z]{3,9}\s\d{1,2}", l):
is_title = True
# Also treat lines that look like "FRONTLINE" or "EDITORIAL" as titles
if l.isupper() or (len(l.split()) <= 4 and l.istitle()):
is_title = True
if is_title and buffer:
# start of a new article
flush()
current_title = l
continue
if is_title and not buffer:
# title followed by content
current_title = l
continue
buffer.append(l)
# final flush
flush()
# If nothing found, return the whole as one section
if not parts:
txt = cleaned_text.strip()
return [{"title": "Untitled", "text": txt}]
# Trim leading/trailing junk for each part
for p in parts:
p["text"] = re.sub(r"\n{2,}", "\n\n", p["text"]).strip()
return parts
def chunk_text_for_telegram(text: str, max_chars: int = 3800) -> List[str]:
"""Split a long article text into parts appropriate for Telegram messages.
Keeps paragraphs intact where possible.
"""
if len(text) <= max_chars:
return [text]
paragraphs = [p.strip() for p in re.split(r"\n{2,}", text) if p.strip()]
chunks = []
cur = ""
for para in paragraphs:
if not cur:
cur = para
continue
if len(cur) + 2 + len(para) <= max_chars:
cur = cur + "\n\n" + para
else:
chunks.append(cur)
cur = para
if cur:
chunks.append(cur)
# As a fallback, if any chunk is still too big, break by characters
final = []
for c in chunks:
if len(c) <= max_chars:
final.append(c)
else:
# break on sentence boundaries naively
sentences = re.split(r"(?<=[.!?])\s+", c)
cur2 = ""
for s in sentences:
if not cur2:
cur2 = s
continue
if len(cur2) + 1 + len(s) <= max_chars:
cur2 = cur2 + " " + s
else:
final.append(cur2)
cur2 = s
if cur2:
final.append(cur2)
return final
})Editor is loading...
Leave a Comment