File size: 6,532 Bytes
54b2662 11133c9 b91b0a5 54b2662 9348624 54b2662 11133c9 54b2662 b91b0a5 54b2662 b91b0a5 11133c9 b91b0a5 11133c9 9348624 b91b0a5 11133c9 54b2662 b91b0a5 11133c9 b91b0a5 11133c9 c429a2d 11133c9 b91b0a5 54b2662 11133c9 b91b0a5 11133c9 b91b0a5 11133c9 b91b0a5 54b2662 b91b0a5 11133c9 54b2662 b91b0a5 54b2662 11133c9 54b2662 b91b0a5 11133c9 b91b0a5 11133c9 54b2662 b91b0a5 54b2662 b91b0a5 54b2662 11133c9 b91b0a5 11133c9 b91b0a5 54b2662 11133c9 b91b0a5 11133c9 b91b0a5 11133c9 b91b0a5 11133c9 b91b0a5 11133c9 b91b0a5 11133c9 b91b0a5 54b2662 b91b0a5 11133c9 |
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 |
import os
import re
import gc
import sys
import signal
import logging
from datetime import datetime
from pathlib import Path
from docling.document_converter import DocumentConverter, FormatOption
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions, TableStructureOptions, EasyOcrOptions, TableFormerMode
from docling.backend.pypdfium2_backend import PyPdfiumDocumentBackend
from docling.pipeline.standard_pdf_pipeline import StandardPdfPipeline
# Thêm project root vào path để import HashProcessor
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from core.hash_file.hash_file import HashProcessor
class DoclingProcessor:
"""Chuyển đổi PDF sang Markdown bằng Docling."""
def __init__(self, output_dir: str, use_ocr: bool = True, timeout: int = 300, images_scale: float = 3.0):
"""Khởi tạo processor với cấu hình OCR và table extraction."""
self.output_dir = output_dir
self.timeout = timeout
self.logger = logging.getLogger(__name__)
self.hasher = HashProcessor(verbose=False)
os.makedirs(output_dir, exist_ok=True)
# File lưu hash index
self.hash_index_path = Path(output_dir) / "docling_hash_index.json"
self.hash_index = self.hasher.load_processed_index(str(self.hash_index_path))
# Cấu hình pipeline PDF
opts = PdfPipelineOptions(do_ocr=use_ocr, do_table_structure=True)
opts.table_structure_options = TableStructureOptions(do_cell_matching=True, mode=TableFormerMode.ACCURATE)
opts.images_scale = images_scale
# Cấu hình OCR tiếng Việt
if use_ocr:
ocr = EasyOcrOptions()
ocr.lang = ["vi"]
ocr.force_full_page_ocr = False
opts.ocr_options = ocr
self.converter = DocumentConverter(format_options={
InputFormat.PDF: FormatOption(backend=PyPdfiumDocumentBackend, pipeline_cls=StandardPdfPipeline, pipeline_options=opts)
})
self.logger.info(f"Docling | OCR={use_ocr} | Table=accurate | Scale={images_scale} | timeout={timeout}s")
def clean_markdown(self, text: str) -> str:
"""Xóa số trang và khoảng trắng thừa."""
text = re.sub(r'\n\s*Trang\s+\d+\s*\n', '\n', text)
return re.sub(r'\n{3,}', '\n\n', text).strip()
def _should_process(self, pdf_path: str, output_path: Path) -> bool:
"""Kiểm tra xem file PDF có cần xử lý lại không (dựa trên hash)."""
# Nếu output chưa tồn tại -> cần xử lý
if not output_path.exists():
return True
# Tính hash file PDF hiện tại
current_hash = self.hasher.get_file_hash(pdf_path)
if not current_hash:
return True
# So sánh với hash đã lưu
saved_hash = self.hash_index.get(pdf_path, {}).get("hash")
return current_hash != saved_hash
def _save_hash(self, pdf_path: str, file_hash: str) -> None:
"""Lưu hash của file đã xử lý vào index."""
self.hash_index[pdf_path] = {
"hash": file_hash,
"processed_at": self.hasher.get_current_timestamp()
}
def parse_document(self, file_path: str) -> str | None:
"""Chuyển đổi 1 file PDF sang Markdown với timeout."""
if not os.path.exists(file_path):
return None
filename = os.path.basename(file_path)
try:
# Đặt timeout để tránh treo
signal.signal(signal.SIGALRM, lambda s, f: (_ for _ in ()).throw(TimeoutError()))
signal.alarm(self.timeout)
result = self.converter.convert(file_path)
md = result.document.export_to_markdown(image_placeholder="")
signal.alarm(0)
md = self.clean_markdown(md)
# Thêm frontmatter metadata
return f"---\nfilename: {filename}\nfilepath: {file_path}\npage_count: {len(result.document.pages)}\nprocessed_at: {datetime.now().isoformat()}\n---\n\n{md}"
except TimeoutError:
self.logger.warning(f"Timeout: {filename}")
signal.alarm(0)
return None
except Exception as e:
self.logger.error(f"Lỗi: {filename}: {e}")
signal.alarm(0)
return None
def parse_directory(self, source_dir: str) -> dict:
"""Xử lý toàn bộ thư mục PDF, bỏ qua file không thay đổi (dựa trên hash)."""
source_path = Path(source_dir)
pdf_files = list(source_path.rglob("*.pdf"))
self.logger.info(f"Tìm thấy {len(pdf_files)} file PDF trong {source_dir}")
results = {"total": len(pdf_files), "parsed": 0, "skipped": 0, "errors": 0}
for i, fp in enumerate(pdf_files):
try:
rel = fp.relative_to(source_path)
except ValueError:
rel = Path(fp.name)
out = Path(self.output_dir) / rel.with_suffix(".md")
out.parent.mkdir(parents=True, exist_ok=True)
pdf_path = str(fp)
# Kiểm tra hash để quyết định có cần xử lý không
if not self._should_process(pdf_path, out):
results["skipped"] += 1
continue
# Tính hash trước khi xử lý
file_hash = self.hasher.get_file_hash(pdf_path)
md = self.parse_document(pdf_path)
if md:
out.write_text(md, encoding="utf-8")
results["parsed"] += 1
# Lưu hash sau khi xử lý thành công
if file_hash:
self._save_hash(pdf_path, file_hash)
else:
results["errors"] += 1
# Dọn memory mỗi 10 files
if (i + 1) % 10 == 0:
gc.collect()
self.logger.info(f"{i+1}/{len(pdf_files)} (bỏ qua: {results['skipped']})")
# Lưu hash index sau khi xử lý xong
self.hasher.save_processed_index(str(self.hash_index_path), self.hash_index)
self.logger.info(f"Xong: {results['parsed']} đã xử lý, {results['skipped']} bỏ qua, {results['errors']} lỗi")
return results
|