extract_docs.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #!/usr/bin/env python3
  2. """
  3. Multi-format document extractor.
  4. Supports: PDF (with tables), DOCX, PPTX, XLSX, MD, TXT
  5. Recursively scans subdirectories.
  6. Usage:
  7. python extract_docs.py
  8. # Reads paths from config.py (agent-config.yml)
  9. """
  10. import os
  11. import re
  12. import sys
  13. from pathlib import Path
  14. from config import cfg
  15. RAW_DIR = cfg.raw_dir
  16. OUT_DIR = cfg.processed_dir
  17. OUT_DIR.mkdir(parents=True, exist_ok=True)
  18. SKIP_EXTENSIONS = {'.mp4', '.zip', '.rar', '.7z', '.avi', '.mov', '.wav', '.mp3', '.DS_Store'}
  19. success = 0
  20. failed = 0
  21. skipped = 0
  22. tables_found = 0
  23. def safe_filename(name, max_len=200):
  24. """Convert path to safe flat filename"""
  25. name = name.replace('/', '_').replace('\\', '_')
  26. name = re.sub(r'[<>:"|?*]', '_', name)
  27. return name[:max_len]
  28. # ── PDF extraction (with table support) ──
  29. def extract_pdf(pdf_path):
  30. global tables_found
  31. import fitz
  32. doc = fitz.open(str(pdf_path))
  33. text_parts = []
  34. for page_num in range(len(doc)):
  35. page = doc[page_num]
  36. # Detect tables
  37. page_tables = page.find_tables()
  38. table_rects = []
  39. if page_tables.tables:
  40. for tab in page_tables.tables:
  41. table_data = tab.extract()
  42. if table_data and len(table_data) > 1:
  43. md_table = _table_to_markdown(table_data)
  44. if md_table:
  45. text_parts.append(f'[TABLE_START]\n{md_table}\n[TABLE_END]')
  46. tables_found += 1
  47. table_rects.append(tab.bbox)
  48. # Get text excluding table areas
  49. if not table_rects:
  50. page_text = page.get_text()
  51. if page_text.strip():
  52. text_parts.append(page_text)
  53. else:
  54. blocks = page.get_text("blocks")
  55. for block in blocks:
  56. bx0, by0, bx1, by1, block_text = block[:5]
  57. if not block_text.strip():
  58. continue
  59. in_table = False
  60. for trect in table_rects:
  61. tx0, ty0, tx1, ty1 = trect
  62. bcx, bcy = (bx0 + bx1) / 2, (by0 + by1) / 2
  63. if tx0 <= bcx <= tx1 and ty0 <= bcy <= ty1:
  64. in_table = True
  65. break
  66. if not in_table:
  67. text_parts.append(block_text)
  68. doc.close()
  69. return '\n'.join(text_parts)
  70. def _table_to_markdown(table_data):
  71. if not table_data or not table_data[0]:
  72. return ''
  73. rows = []
  74. for row in table_data:
  75. cells = [str(c).replace('\n', ' ').strip() if c else '' for c in row]
  76. rows.append(cells)
  77. if not rows:
  78. return ''
  79. max_cols = max(len(r) for r in rows)
  80. for r in rows:
  81. while len(r) < max_cols:
  82. r.append('')
  83. lines = ['| ' + ' | '.join(rows[0]) + ' |']
  84. lines.append('| ' + ' | '.join(['---'] * max_cols) + ' |')
  85. for row in rows[1:]:
  86. lines.append('| ' + ' | '.join(row) + ' |')
  87. return '\n'.join(lines)
  88. # ── DOCX extraction ──
  89. def extract_docx(docx_path):
  90. from docx import Document
  91. doc = Document(str(docx_path))
  92. parts = []
  93. for para in doc.paragraphs:
  94. text = para.text.strip()
  95. if text:
  96. # Preserve heading structure
  97. if para.style and para.style.name.startswith('Heading'):
  98. level = para.style.name.replace('Heading ', '').replace('Heading', '1')
  99. try:
  100. level = int(level)
  101. except:
  102. level = 1
  103. parts.append('#' * level + ' ' + text)
  104. else:
  105. parts.append(text)
  106. # Extract tables
  107. for table in doc.tables:
  108. rows = []
  109. for row in table.rows:
  110. cells = [cell.text.strip().replace('\n', ' ') for cell in row.cells]
  111. rows.append(cells)
  112. if rows:
  113. md = _table_to_markdown(rows)
  114. if md:
  115. parts.append(f'[TABLE_START]\n{md}\n[TABLE_END]')
  116. return '\n\n'.join(parts)
  117. # ── PPTX extraction ──
  118. def extract_pptx(pptx_path):
  119. from pptx import Presentation
  120. prs = Presentation(str(pptx_path))
  121. parts = []
  122. for slide_num, slide in enumerate(prs.slides, 1):
  123. slide_texts = [f'--- Slide {slide_num} ---']
  124. for shape in slide.shapes:
  125. if shape.has_text_frame:
  126. for para in shape.text_frame.paragraphs:
  127. text = para.text.strip()
  128. if text:
  129. slide_texts.append(text)
  130. if shape.has_table:
  131. rows = []
  132. for row in shape.table.rows:
  133. cells = [cell.text.strip().replace('\n', ' ') for cell in row.cells]
  134. rows.append(cells)
  135. if rows:
  136. md = _table_to_markdown(rows)
  137. if md:
  138. slide_texts.append(f'[TABLE_START]\n{md}\n[TABLE_END]')
  139. if len(slide_texts) > 1:
  140. parts.append('\n'.join(slide_texts))
  141. return '\n\n'.join(parts)
  142. # ── XLSX extraction ──
  143. def extract_xlsx(xlsx_path):
  144. from openpyxl import load_workbook
  145. wb = load_workbook(str(xlsx_path), read_only=True, data_only=True)
  146. parts = []
  147. for sheet_name in wb.sheetnames:
  148. ws = wb[sheet_name]
  149. rows = []
  150. for row in ws.iter_rows(values_only=True):
  151. cells = [str(c).strip() if c is not None else '' for c in row]
  152. if any(cells):
  153. rows.append(cells)
  154. if rows:
  155. parts.append(f'## Sheet: {sheet_name}')
  156. md = _table_to_markdown(rows)
  157. if md:
  158. parts.append(f'[TABLE_START]\n{md}\n[TABLE_END]')
  159. wb.close()
  160. return '\n\n'.join(parts)
  161. # ── TXT/MD extraction ──
  162. def extract_text(txt_path):
  163. with open(txt_path, 'r', encoding='utf-8', errors='ignore') as f:
  164. return f.read()
  165. # ── Main ──
  166. def main():
  167. global success, failed, skipped
  168. # Recursively find all files
  169. all_files = sorted(RAW_DIR.rglob('*'))
  170. files = [f for f in all_files if f.is_file()]
  171. total = len(files)
  172. print(f"Raw dir: {RAW_DIR}")
  173. print(f"Output dir: {OUT_DIR}")
  174. print(f"Total files: {total}")
  175. print("=" * 60)
  176. for i, file_path in enumerate(files):
  177. ext = file_path.suffix.lower()
  178. # Relative path for output filename
  179. rel_path = file_path.relative_to(RAW_DIR)
  180. out_name = safe_filename(str(rel_path).replace(ext, '')) + '.txt'
  181. out_path = OUT_DIR / out_name
  182. # Skip already processed
  183. if out_path.exists() and out_path.stat().st_size > 0:
  184. skipped += 1
  185. continue
  186. # Skip unsupported
  187. if ext in SKIP_EXTENSIONS or file_path.name.startswith('.'):
  188. skipped += 1
  189. continue
  190. try:
  191. if ext == '.pdf':
  192. text = extract_pdf(file_path)
  193. elif ext == '.docx':
  194. text = extract_docx(file_path)
  195. elif ext == '.pptx':
  196. text = extract_pptx(file_path)
  197. elif ext == '.xlsx':
  198. text = extract_xlsx(file_path)
  199. elif ext in ('.txt', '.md'):
  200. text = extract_text(file_path)
  201. else:
  202. skipped += 1
  203. continue
  204. if text and text.strip():
  205. with open(out_path, 'w', encoding='utf-8') as f:
  206. f.write(f'# Source: {rel_path}\n\n')
  207. f.write(text)
  208. success += 1
  209. else:
  210. failed += 1
  211. except Exception as e:
  212. failed += 1
  213. print(f" ERROR [{i+1}/{total}] {rel_path}: {e}", file=sys.stderr)
  214. if (i + 1) % 50 == 0:
  215. print(f" Progress: {i+1}/{total} (ok={success} skip={skipped} fail={failed})")
  216. print(f"\nDone! success={success} skipped={skipped} failed={failed} tables={tables_found}")
  217. if __name__ == '__main__':
  218. main()