test.py 76 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782
  1. from pathlib import Path
  2. import pandas as pd
  3. from tqdm import tqdm
  4. import time
  5. import re
  6. from typing import List, Optional, Tuple
  7. import subprocess
  8. import os
  9. # 导入 pdfplumber 用于提取表格
  10. try:
  11. import pdfplumber
  12. PDFPLUMBER_AVAILABLE = True
  13. except ImportError:
  14. PDFPLUMBER_AVAILABLE = False
  15. print("⚠ pdfplumber 未安装,无法提取表格")
  16. print(" 安装命令: pip install pdfplumber")
  17. # 导入 PyMuPDF 用于提取文本
  18. try:
  19. import fitz # PyMuPDF
  20. PYMUPDF_AVAILABLE = True
  21. except ImportError:
  22. PYMUPDF_AVAILABLE = False
  23. print("⚠ PyMuPDF 未安装,无法提取表格前的文本")
  24. print(" 安装命令: pip install PyMuPDF")
  25. # ==================== 配置区域 ====================
  26. pdf_path = '/home/hws/workspace/GitLab/Clerk2.5/pdf_converter_v2/3-数据/鄂电司发展〔2024〕124号 国网湖北省电力有限公司关于襄阳连云220千伏输变电工程可行性研究报告的批复.pdf'
  27. output_dir = Path('extracted_tables') # 原始表格输出目录(包含表格前文本)
  28. merged_output_dir = Path('merged_tables') # 合并后的表格输出目录(已剔除表格前文本)
  29. filtered_output_dir = Path('filtered_tables') # 筛选后的表格输出目录
  30. # CEB 转 PDF 配置
  31. AUTO_CONVERT_CEB = True # 是否自动尝试转换 CEB 文件
  32. CEB_CONVERTED_DIR = Path('converted_pdfs') # CEB 转换后的 PDF 存放目录
  33. # 表头规则配置:根据PDF文件名匹配对应的表头规则(固定匹配模式)
  34. # 每个规则可以包含多个表头定义,表头必须包含所有关键词才匹配
  35. TABLE_HEADER_RULES = {
  36. # 规则1: 结算报告类
  37. "9-(结算报告)山西晋城周村220kV输变电工程结算审计报告.pdf": [
  38. {
  39. "name": "审定结算汇总表",
  40. "keywords": ["序号", "审计内容", "送审金额(含税)", "审定金额(含税)", "审定金额(不含税)", "增减金额", "备注"],
  41. "match_mode": "all" # 表头必须包含所有关键词才匹配(固定匹配)
  42. },
  43. {
  44. "name": "合同执行情况",
  45. "keywords": ["施工单位", "中标通知书金额", "中标通知书编号", "合同金额", "结算送审金额", "差额"],
  46. "match_mode": "all" # 表头必须包含所有关键词才匹配(固定匹配)
  47. },
  48. {
  49. "name": "赔偿合同",
  50. "keywords": ["合同对方", "赔偿事项", "合同金额", "结算送审金额", "差额"],
  51. "match_mode": "all" # 表头必须包含所有关键词才匹配(固定匹配)
  52. },
  53. {
  54. "name": "物资采购合同1",
  55. "keywords": ["物料名称", "合同数量", "施工图数量", "单价(不含税)", "差额"],
  56. "match_mode": "all" # 表头必须包含所有关键词才匹配(固定匹配)
  57. },
  58. {
  59. "name": "物资采购合同2",
  60. "keywords": ["物料名称", "合同金额(不含税)", "入账金额", "差额", "备注"],
  61. "match_mode": "all" # 表头必须包含所有关键词才匹配(固定匹配)
  62. },
  63. {
  64. "name": "其他服务类合同",
  65. "keywords": ["服务商", "中标通知书", "合同金额", "送审金额", "结算金额"],
  66. "match_mode": "all" # 表头必须包含所有关键词才匹配(固定匹配)
  67. }
  68. ],
  69. "4-(初设评审)中电联电力建设技术经济咨询中心技经〔2019〕201号关于山西周村220kV输变电工程初步设计的评审意见.pdf":[
  70. {
  71. "name": "初设评审的概算投资",
  72. "keywords": ["序号", "工程名称", "建设规模", "静态投资", "其中:建设场地征用及清理费", "动态投资"],
  73. "match_mode": "all" # 表头必须包含所有关键词才匹配(固定匹配)
  74. },
  75. ]
  76. # 可以添加更多文件的规则
  77. # "其他文件名.pdf": [
  78. # {Ctrl+Shift+P
  79. # "name": "表头名称",
  80. # "keywords": ["关键词1", "关键词2"],
  81. # "match_mode": "all" # "all" 表示必须包含所有关键词(固定匹配)
  82. # }
  83. # ]
  84. }
  85. # 是否启用表头过滤(如果为False,则提取所有表格)
  86. ENABLE_HEADER_FILTER = False
  87. # 要排除的规则名称列表(如果某个规则匹配了不该匹配的表格,可以在这里排除)
  88. # 例如: EXCLUDE_RULES = ["物资采购合同2"] 将不会匹配该规则
  89. EXCLUDE_RULES = []
  90. # 是否启用表格内多表头检测和分割(如果一个表格中包含多个表头,自动分割)
  91. # 已禁用此功能,改用基于文本的合并策略
  92. # ENABLE_SPLIT_MULTI_HEADER_TABLES = True
  93. # 是否提取表格前的文本(用于更好地识别表格类型)
  94. EXTRACT_TEXT_BEFORE_TABLE = True
  95. # 提取表格前多少行文本(用于识别表格标题和上下文)
  96. TEXT_LINES_BEFORE_TABLE = 5
  97. # 是否显示所有表格的表头预览(用于帮助确定过滤条件)
  98. SHOW_HEADER_PREVIEW = True
  99. # 是否显示匹配的规则名称
  100. SHOW_MATCHED_RULE_NAME = True
  101. # 是否启用跨页表格合并(基于表格前文本判断)
  102. ENABLE_MERGE_CROSS_PAGE_TABLES = True
  103. # 跨页表格检测的页面范围(检查前后几页)
  104. CROSS_PAGE_SEARCH_RANGE = 10 # 增加范围以支持长表格
  105. # 是否从已生成的xlsx文件中过滤(而不是从PDF提取)
  106. FILTER_FROM_EXISTING_XLSX = True # 设置为True
  107. XLSX_INPUT_DIR = 'extracted_tables' # 初始读取目录(会在流程中更新为 merged_tables)
  108. # ==================================================
  109. def check_table_header(table_df: pd.DataFrame, rule: dict) -> Tuple[bool, str]:
  110. """
  111. 检查表格是否匹配指定的表头规则(固定匹配:必须包含所有关键词)
  112. 支持处理表头换行的情况(合并前几行作为表头)
  113. Args:
  114. table_df: 表格DataFrame
  115. rule: 表头规则字典,包含 name, keywords, match_mode
  116. Returns:
  117. tuple: (是否匹配, 规则名称)
  118. """
  119. if table_df.empty:
  120. return False, ""
  121. rule_name = rule.get("name", "未知规则")
  122. # 检查是否在排除列表中
  123. if rule_name in EXCLUDE_RULES:
  124. return False, ""
  125. # 检查第一行是否是文本信息(以 "[表格前文本]" 开头)
  126. start_row = 0
  127. if len(table_df) > 0:
  128. first_cell = str(table_df.iloc[0, 0]).strip()
  129. if first_cell.startswith("[表格前文本]"):
  130. start_row = 1 # 跳过第一行
  131. # 处理表头换行:合并前几行作为完整表头(通常表头可能占1-3行)
  132. # 检查前3行,合并所有非空单元格内容
  133. header_rows_to_check = min(3, len(table_df) - start_row)
  134. header_text_parts = []
  135. for row_idx in range(start_row, start_row + header_rows_to_check):
  136. if row_idx >= len(table_df):
  137. break
  138. row = table_df.iloc[row_idx].astype(str).str.strip()
  139. # 收集该行的所有非空单元格内容
  140. for cell in row:
  141. cell_text = str(cell).strip()
  142. # 过滤掉空值、NaN等
  143. if cell_text and cell_text.lower() not in ['nan', 'none', '']:
  144. # 将单元格内的换行符替换为空格(处理xlsx中的换行)
  145. cell_text = cell_text.replace('\n', ' ').replace('\r', ' ')
  146. header_text_parts.append(cell_text)
  147. # 合并所有表头文本(去除换行符和多余空格)
  148. header_text = " ".join(header_text_parts)
  149. # 进一步清理:将多个连续空格替换为单个空格(包括换行符转换后的空格)
  150. header_text = re.sub(r'\s+', ' ', header_text).strip()
  151. # 创建一个无空格的版本用于匹配(处理换行导致的空格问题)
  152. header_text_no_space = re.sub(r'\s+', '', header_text)
  153. keywords = rule.get("keywords", [])
  154. match_mode = rule.get("match_mode", "all") # 默认使用 "all"(固定匹配)
  155. if not keywords:
  156. return False, ""
  157. # 固定匹配:必须包含所有关键词
  158. # 匹配时同时检查原文本和无空格版本,以处理换行导致的空格问题
  159. if match_mode == "all":
  160. all_match = True
  161. for keyword in keywords:
  162. # 同时检查原文本和无空格版本
  163. keyword_no_space = re.sub(r'\s+', '', keyword)
  164. if keyword in header_text or keyword_no_space in header_text_no_space:
  165. continue
  166. else:
  167. all_match = False
  168. break
  169. if all_match:
  170. return True, rule_name
  171. elif match_mode == "any":
  172. # 保留此选项,但不推荐使用(模糊匹配)
  173. for keyword in keywords:
  174. keyword_no_space = re.sub(r'\s+', '', keyword)
  175. if keyword in header_text or keyword_no_space in header_text_no_space:
  176. return True, rule_name
  177. return False, ""
  178. def has_table_header(table_df: pd.DataFrame, header_rules: List[dict]) -> Tuple[bool, str]:
  179. """
  180. 检查表格是否有表头(匹配任何一个规则)
  181. Args:
  182. table_df: 表格DataFrame
  183. header_rules: 表头规则列表
  184. Returns:
  185. tuple: (是否有表头, 匹配的规则名称)
  186. """
  187. for rule in header_rules:
  188. is_match, rule_name = check_table_header(table_df, rule)
  189. if is_match:
  190. return True, rule_name
  191. return False, ""
  192. def has_text_before_table(table_df: pd.DataFrame) -> Tuple[bool, str]:
  193. """
  194. 检查表格是否有前文本(判断是否是新表格的开始)
  195. Args:
  196. table_df: 表格DataFrame
  197. Returns:
  198. tuple: (是否有前文本, 前文本内容)
  199. """
  200. if table_df.empty or len(table_df) == 0:
  201. return False, ""
  202. first_cell = str(table_df.iloc[0, 0]).strip()
  203. if first_cell.startswith("[表格前文本]"):
  204. # 提取文本内容(去掉标记)
  205. text_content = first_cell.replace("[表格前文本]", "").strip()
  206. return True, text_content
  207. return False, ""
  208. def remove_text_row(table_df: pd.DataFrame) -> pd.DataFrame:
  209. """
  210. 移除表格第一行的文本信息(如果存在)
  211. Args:
  212. table_df: 表格DataFrame
  213. Returns:
  214. pd.DataFrame: 移除文本行后的表格
  215. """
  216. has_text, _ = has_text_before_table(table_df)
  217. if has_text:
  218. return table_df.iloc[1:].reset_index(drop=True)
  219. return table_df
  220. def convert_ceb_to_pdf(ceb_path: str) -> Optional[str]:
  221. """
  222. 尝试将 CEB 文件转换为 PDF
  223. CEB 是中国电子公文格式,需要专门的工具转换。
  224. 此函数会检查是否已有转换后的 PDF,如果没有则提示用户手动转换。
  225. Args:
  226. ceb_path: CEB 文件路径
  227. Returns:
  228. str: 转换后的 PDF 文件路径,如果转换失败返回 None
  229. """
  230. ceb_path = Path(ceb_path)
  231. if not ceb_path.exists():
  232. print(f"⚠ CEB 文件不存在: {ceb_path}")
  233. return None
  234. # 创建转换目录
  235. converted_dir = CEB_CONVERTED_DIR
  236. converted_dir.mkdir(parents=True, exist_ok=True)
  237. # 生成 PDF 文件名
  238. pdf_filename = ceb_path.stem + '.pdf'
  239. pdf_path = converted_dir / pdf_filename
  240. # 如果已经转换过,直接返回
  241. if pdf_path.exists():
  242. print(f"✓ 发现已转换的 PDF: {pdf_path}")
  243. return str(pdf_path)
  244. print(f"\n检测到 CEB 文件: {ceb_path.name}")
  245. print("=" * 60)
  246. print("CEB 是中国电子公文格式,需要手动转换为 PDF")
  247. print("=" * 60)
  248. print("\n推荐转换方法:")
  249. print("1. 使用方正 Apabi Reader (官方工具)")
  250. print(" - 下载: http://www.apabi.com/")
  251. print(" - 打开 CEB 文件后,选择 '文件' -> '另存为' -> 'PDF'")
  252. print("\n2. 使用在线转换工具")
  253. print(" - https://convertio.co/zh/ceb-pdf/")
  254. print(" - https://www.aconvert.com/cn/document/ceb-to-pdf/")
  255. print("\n3. 使用 CEB 阅读器导出")
  256. print(" - 安装任意支持 CEB 格式的阅读器")
  257. print(" - 打开文件后导出为 PDF")
  258. print("\n" + "=" * 60)
  259. print(f"请将转换后的 PDF 文件放到: {converted_dir.absolute()}")
  260. print(f"文件名: {pdf_filename}")
  261. print("=" * 60)
  262. return None
  263. def check_and_convert_file(file_path: str) -> str:
  264. """
  265. 检查文件类型,如果是 CEB 则尝试转换为 PDF
  266. Args:
  267. file_path: 文件路径
  268. Returns:
  269. str: PDF 文件路径
  270. """
  271. file_path = Path(file_path)
  272. # 检查文件扩展名
  273. if file_path.suffix.lower() == '.ceb':
  274. if AUTO_CONVERT_CEB:
  275. pdf_path = convert_ceb_to_pdf(str(file_path))
  276. if pdf_path:
  277. return pdf_path
  278. else:
  279. print("\n请手动转换 CEB 文件后重新运行脚本")
  280. exit(1)
  281. else:
  282. print(f"⚠ 检测到 CEB 文件,但自动转换已禁用: {file_path}")
  283. print(" 请设置 AUTO_CONVERT_CEB = True 或手动转换为 PDF")
  284. exit(1)
  285. return str(file_path)
  286. def clean_newlines(table_df: pd.DataFrame) -> pd.DataFrame:
  287. """
  288. 清理单元格内的换行符
  289. Args:
  290. table_df: 表格DataFrame
  291. Returns:
  292. pd.DataFrame: 清理后的表格
  293. """
  294. if table_df.empty:
  295. return table_df
  296. df = table_df.copy()
  297. # 先转换为 object 类型避免 dtype 警告
  298. df = df.astype(object)
  299. # 遍历所有单元格,移除换行符
  300. for i in range(len(df)):
  301. for j in range(len(df.columns)):
  302. cell_val = df.iloc[i, j]
  303. if cell_val is not None and not pd.isna(cell_val):
  304. # 移除换行符
  305. df.iloc[i, j] = str(cell_val).replace('\n', '').replace('\r', '')
  306. return df
  307. def merge_broken_rows(table_df: pd.DataFrame, header_rows: int = 1, debug: bool = False) -> pd.DataFrame:
  308. """
  309. 合并被截断的行(跨页时一行被分成多行)
  310. 检测规则:如果某一行前N列有内容,但后面的列大部分是空的(超过50%),
  311. 则认为是上一行的延续,合并到上一行对应的列
  312. Args:
  313. table_df: 表格DataFrame
  314. header_rows: 表头行数,跳过表头不处理
  315. debug: 是否输出调试信息
  316. Returns:
  317. pd.DataFrame: 合并后的表格
  318. """
  319. if table_df.empty or len(table_df) <= header_rows:
  320. return table_df
  321. df = table_df.copy()
  322. rows_to_remove = []
  323. if debug:
  324. print(f" [DEBUG] merge_broken_rows: 检查 {len(df)} 行,跳过前 {header_rows} 行表头")
  325. # 从表头后开始检查
  326. for i in range(header_rows, len(df)):
  327. # 统计有内容的列和空列
  328. non_empty_cols = []
  329. empty_cols = []
  330. for j in range(len(df.columns)):
  331. val = df.iloc[i, j]
  332. val_str = str(val).strip()
  333. is_empty = val is None or pd.isna(val) or not val_str or val_str.lower() in ['nan', 'none', '']
  334. if is_empty:
  335. empty_cols.append(j)
  336. else:
  337. non_empty_cols.append(j)
  338. # 如果没有非空列,跳过
  339. if not non_empty_cols:
  340. continue
  341. # 计算空列比例
  342. empty_ratio = len(empty_cols) / len(df.columns)
  343. if debug:
  344. print(f" [DEBUG] 行 {i}: 非空列={non_empty_cols}, 空列比例={empty_ratio:.2f}")
  345. # 如果空列超过50%,且不是第一行数据,认为是被截断的行
  346. if empty_ratio > 0.5 and i > header_rows:
  347. # 检查上一行对应位置是否有内容
  348. can_merge = True
  349. for col_idx in non_empty_cols:
  350. prev_val = df.iloc[i-1, col_idx]
  351. if prev_val is None or pd.isna(prev_val) or str(prev_val).strip() == '':
  352. can_merge = False
  353. break
  354. if can_merge:
  355. if debug:
  356. print(f" [DEBUG] ✓ 合并行 {i} 到行 {i-1}")
  357. # 合并每个非空列到上一行对应的列
  358. for col_idx in non_empty_cols:
  359. prev_val = df.iloc[i-1, col_idx]
  360. curr_val = df.iloc[i, col_idx]
  361. df.iloc[i-1, col_idx] = str(prev_val) + str(curr_val)
  362. if debug:
  363. print(f" [DEBUG] 列 {col_idx}: '{prev_val}' + '{curr_val}'")
  364. rows_to_remove.append(i)
  365. else:
  366. if debug:
  367. print(f" [DEBUG] ✗ 上一行对应列为空,跳过合并")
  368. else:
  369. if debug and empty_ratio > 0:
  370. print(f" [DEBUG] ✗ 空列比例不足50%或是第一行数据,跳过")
  371. # 删除已合并的行
  372. if rows_to_remove:
  373. df = df.drop(rows_to_remove).reset_index(drop=True)
  374. if debug:
  375. print(f" ✓ 合并了 {len(rows_to_remove)} 个被截断的行")
  376. else:
  377. if debug:
  378. print(f" ℹ 没有发现需要合并的被截断行")
  379. return df
  380. def fix_broken_cells(table_df: pd.DataFrame, header_row_count: int = 1) -> pd.DataFrame:
  381. """
  382. 修复被错误分割的单元格(一个单元格的内容被识别成多行)
  383. 检测规则:
  384. 1. 如果某一行的第一列有内容,但其他列全部为空
  385. 2. 则认为当前行是上一行第一列内容的延续,需要合并
  386. Args:
  387. table_df: 表格DataFrame
  388. header_row_count: 表头行数,跳过表头不处理
  389. Returns:
  390. pd.DataFrame: 修复后的表格
  391. """
  392. if table_df.empty or len(table_df) <= header_row_count:
  393. return table_df
  394. df = table_df.copy()
  395. rows_to_remove = []
  396. # 从表头后开始检查
  397. for i in range(header_row_count, len(df)):
  398. # 获取当前行
  399. current_row = df.iloc[i]
  400. # 检查第一列是否有内容
  401. first_col = str(current_row.iloc[0]).strip()
  402. if not first_col or first_col.lower() in ['nan', 'none', '']:
  403. continue
  404. # 检查其他列是否全部为空
  405. other_cols = current_row.iloc[1:]
  406. all_empty = all(str(val).strip() in ['', 'nan', 'None', 'NaN'] for val in other_cols)
  407. if all_empty and i > header_row_count:
  408. # 其他列全部为空,合并到上一行的第一列
  409. prev_first_col = str(df.iloc[i-1, 0]).strip()
  410. if prev_first_col and prev_first_col.lower() not in ['nan', 'none', '']:
  411. # 合并到上一行的第一列(移除换行符)
  412. df.iloc[i-1, 0] = prev_first_col + first_col.replace('\n', '').replace('\r', '')
  413. rows_to_remove.append(i)
  414. # 删除已合并的行
  415. if rows_to_remove:
  416. df = df.drop(rows_to_remove).reset_index(drop=True)
  417. print(f" 修复了 {len(rows_to_remove)} 个被错误分割的单元格(跨行)")
  418. return df
  419. def fix_split_cells_across_columns(table_df: pd.DataFrame, header_row_count: int = 1) -> pd.DataFrame:
  420. """
  421. 修复被错误分割到多列的单元格
  422. 例如:"3" 和 "工程物资" 被分成两列,应该合并为 "3 工程物资"
  423. 检测规则:
  424. 1. 如果某一行的前两列都有内容,但第一列内容很短(1-2个字符)
  425. 2. 且第二列看起来像是第一列的延续(都是文字)
  426. 3. 则合并这两列
  427. Args:
  428. table_df: 表格DataFrame
  429. header_row_count: 表头行数,跳过表头不处理
  430. Returns:
  431. pd.DataFrame: 修复后的表格
  432. """
  433. if table_df.empty or len(table_df) <= header_row_count or len(table_df.columns) < 2:
  434. return table_df
  435. df = table_df.copy()
  436. merge_count = 0
  437. # 从表头后开始检查
  438. for i in range(header_row_count, len(df)):
  439. first_col = str(df.iloc[i, 0]).strip()
  440. second_col = str(df.iloc[i, 1]).strip()
  441. # 检查第一列和第二列是否都有内容
  442. if (first_col and first_col.lower() not in ['nan', 'none', ''] and
  443. second_col and second_col.lower() not in ['nan', 'none', '']):
  444. # 检查第一列是否很短(1-3个字符,通常是序号或简短文字)
  445. # 且第三列及之后的列是否有数据(说明这不是正常的多列数据)
  446. if len(first_col) <= 3:
  447. # 检查第三列及之后是否有数据
  448. has_data_after = False
  449. if len(df.columns) > 2:
  450. for col_idx in range(2, len(df.columns)):
  451. val = str(df.iloc[i, col_idx]).strip()
  452. if val and val.lower() not in ['nan', 'none', '']:
  453. has_data_after = True
  454. break
  455. # 如果第三列及之后有数据,说明这可能是被错误分割的
  456. # 或者如果第一列是纯数字(序号),也可能需要合并
  457. if has_data_after or first_col.isdigit():
  458. # 合并第一列和第二列
  459. df.iloc[i, 1] = first_col + ' ' + second_col
  460. df.iloc[i, 0] = ''
  461. merge_count += 1
  462. if merge_count > 0:
  463. print(f" 修复了 {merge_count} 个被错误分割的单元格(跨列)")
  464. return df
  465. def remove_empty_columns(table_df: pd.DataFrame, header_row_count: int = 1, empty_threshold: float = 0.8) -> pd.DataFrame:
  466. """
  467. 移除空列(整列都是空值或大部分为空)
  468. Args:
  469. table_df: 表格DataFrame
  470. header_row_count: 表头行数
  471. empty_threshold: 空值比例阈值(超过此比例认为是空列)
  472. Returns:
  473. pd.DataFrame: 移除空列后的表格
  474. """
  475. if table_df.empty:
  476. return table_df
  477. df = table_df.copy()
  478. cols_to_remove = []
  479. # 检查每一列
  480. for col_idx in range(len(df.columns)):
  481. # 获取该列的所有值(包括表头)
  482. col_values = df.iloc[:, col_idx]
  483. # 计算空值比例(检查整列)
  484. empty_count = sum(1 for val in col_values if str(val).strip() in ['', 'nan', 'None', 'NaN'])
  485. empty_ratio = empty_count / len(col_values) if len(col_values) > 0 else 1.0
  486. # 如果空值比例超过阈值,标记为待删除
  487. if empty_ratio >= empty_threshold:
  488. cols_to_remove.append(col_idx)
  489. # 删除空列
  490. if cols_to_remove:
  491. df = df.drop(df.columns[cols_to_remove], axis=1)
  492. df.columns = range(len(df.columns)) # 重置列索引
  493. print(f" 移除了 {len(cols_to_remove)} 个空列")
  494. return df
  495. def clean_cell_text(table_df: pd.DataFrame) -> pd.DataFrame:
  496. """
  497. 清理单元格内的文本(移除换行符等)
  498. Args:
  499. table_df: 表格DataFrame
  500. Returns:
  501. pd.DataFrame: 清理后的表格
  502. """
  503. if table_df.empty:
  504. return table_df
  505. df = table_df.copy()
  506. # 先将整个 DataFrame 转换为 object 类型(字符串类型),避免 dtype 警告
  507. df = df.astype(object)
  508. # 遍历所有单元格,移除换行符
  509. for i in range(len(df)):
  510. for j in range(len(df.columns)):
  511. cell_val = str(df.iloc[i, j])
  512. if cell_val and cell_val.lower() not in ['nan', 'none', '']:
  513. # 移除换行符,用空字符串替换
  514. cleaned_val = cell_val.replace('\n', '').replace('\r', '')
  515. df.iloc[i, j] = cleaned_val
  516. return df
  517. def fix_merged_header_cells(table_df: pd.DataFrame, expected_keywords: list = None) -> pd.DataFrame:
  518. """
  519. 修复表头中被错误合并的单元格
  520. 例如:"施工图数量 单价(不含税)" 应该分成两列:"施工图数量" 和 "单价(不含税)"
  521. 注意:这个函数会增加列数,但不会自动调整数据行。
  522. 建议在调用此函数前先移除空列。
  523. Args:
  524. table_df: 表格DataFrame
  525. expected_keywords: 期望的关键词列表,用于检测是否需要分割
  526. Returns:
  527. pd.DataFrame: 修复后的表格
  528. """
  529. if table_df.empty or len(table_df) == 0:
  530. return table_df
  531. df = table_df.copy()
  532. # 检查第一行(表头)是否有需要分割的单元格
  533. header_row = df.iloc[0]
  534. cols_to_split = []
  535. for col_idx in range(len(header_row)):
  536. cell_val = str(header_row.iloc[col_idx]).strip()
  537. # 检查是否包含多个关键词(用空格分隔)
  538. # 常见的分割模式:包含多个中文词组,且中间有空格
  539. if ' ' in cell_val and cell_val not in ['nan', 'None', 'NaN', '']:
  540. # 尝试分割
  541. parts = cell_val.split(' ')
  542. # 如果分割后有多个非空部分,且每个部分都有实际内容
  543. valid_parts = [p.strip() for p in parts if p.strip() and len(p.strip()) > 1]
  544. if len(valid_parts) >= 2:
  545. cols_to_split.append((col_idx, valid_parts))
  546. # 如果没有需要分割的列,直接返回
  547. if not cols_to_split:
  548. return df
  549. # 从后往前处理,避免索引变化
  550. for col_idx, parts in reversed(cols_to_split):
  551. print(f" 分割表头列 {col_idx}: '{header_row.iloc[col_idx]}' -> {parts}")
  552. # 获取原列的数据
  553. original_col = df.iloc[:, col_idx].tolist()
  554. # 删除原列
  555. df = df.drop(df.columns[col_idx], axis=1)
  556. # 创建新列并插入
  557. for part_idx, part in enumerate(parts):
  558. new_col_data = []
  559. for row_idx in range(len(original_col)):
  560. if row_idx == 0:
  561. # 表头行:使用分割后的部分
  562. new_col_data.append(part)
  563. else:
  564. # 数据行:第一个新列保留原数据,其他新列为空
  565. if part_idx == 0:
  566. new_col_data.append(original_col[row_idx])
  567. else:
  568. new_col_data.append('')
  569. # 使用唯一的临时列名插入
  570. temp_col_name = f'temp_col_{col_idx}_{part_idx}'
  571. df.insert(col_idx + part_idx, temp_col_name, new_col_data)
  572. # 重置列索引
  573. df.columns = range(len(df.columns))
  574. return df
  575. def extract_text_before_table(pdf_path: str, page_num: int, table_bbox: tuple, num_lines: int = 5) -> str:
  576. """
  577. 提取表格前的文本
  578. Args:
  579. pdf_path: PDF文件路径
  580. page_num: 页码(从1开始)
  581. table_bbox: 表格边界框 (x0, top, x1, bottom),pdfplumber格式
  582. num_lines: 提取多少行文本
  583. Returns:
  584. str: 表格前的文本
  585. """
  586. if not PYMUPDF_AVAILABLE or not EXTRACT_TEXT_BEFORE_TABLE:
  587. return ""
  588. try:
  589. doc = fitz.open(pdf_path)
  590. page = doc[page_num - 1] # PyMuPDF 页码从0开始
  591. # pdfplumber 的 bbox 格式: (x0, top, x1, bottom),坐标系原点在左上角
  592. # PyMuPDF 的坐标系也是原点在左上角,可以直接使用
  593. table_top = table_bbox[1] # top
  594. # 获取页面所有文本块
  595. blocks = page.get_text("dict")["blocks"]
  596. # 筛选表格上方的文本块
  597. text_blocks_above = []
  598. for block in blocks:
  599. if block["type"] == 0: # 文本块
  600. block_bottom = block["bbox"][3] # block 的底部
  601. # 如果文本块在表格上方
  602. if block_bottom < table_top:
  603. # 提取文本
  604. block_text = ""
  605. for line in block["lines"]:
  606. line_text = ""
  607. for span in line["spans"]:
  608. line_text += span["text"]
  609. block_text += line_text.strip() + "\n"
  610. text_blocks_above.append({
  611. "y": block["bbox"][1], # 使用 top 坐标排序
  612. "text": block_text.strip()
  613. })
  614. # 按 y 坐标排序(从上到下)
  615. text_blocks_above.sort(key=lambda x: x["y"])
  616. # 取最后 num_lines 行(最接近表格的文本)
  617. recent_texts = [block["text"] for block in text_blocks_above[-num_lines:]]
  618. doc.close()
  619. return "\n".join(recent_texts)
  620. except Exception as e:
  621. print(f"⚠ 提取表格前文本失败: {e}")
  622. return ""
  623. def extract_tables_with_pdfplumber(pdf_path: str, pages: str = 'all') -> List[Tuple[int, pd.DataFrame, tuple]]:
  624. """
  625. 使用 pdfplumber 提取 PDF 中的表格
  626. Args:
  627. pdf_path: PDF文件路径
  628. pages: 页面范围,'all' 表示所有页面
  629. Returns:
  630. List[Tuple[int, pd.DataFrame, tuple]]: [(页码, DataFrame, bbox), ...]
  631. """
  632. if not PDFPLUMBER_AVAILABLE:
  633. raise ImportError("pdfplumber 未安装")
  634. tables_data = []
  635. with pdfplumber.open(pdf_path) as pdf:
  636. # 确定要处理的页面
  637. if pages == 'all':
  638. pages_to_process = pdf.pages
  639. else:
  640. # 解析页面范围(如 "1-5,7,9-10")
  641. page_numbers = []
  642. for part in pages.split(','):
  643. if '-' in part:
  644. start, end = map(int, part.split('-'))
  645. page_numbers.extend(range(start, end + 1))
  646. else:
  647. page_numbers.append(int(part))
  648. pages_to_process = [pdf.pages[i-1] for i in page_numbers if 0 < i <= len(pdf.pages)]
  649. # 提取每一页的表格
  650. for page in pages_to_process:
  651. page_num = page.page_number
  652. # 使用 pdfplumber 的表格识别
  653. # table_settings 可以调整识别精度
  654. table_settings = {
  655. "vertical_strategy": "lines", # 使用线条识别垂直边界
  656. "horizontal_strategy": "lines", # 使用线条识别水平边界
  657. "intersection_tolerance": 3, # 交叉点容差
  658. "min_words_vertical": 1, # 最少垂直单词数
  659. "min_words_horizontal": 1, # 最少水平单词数
  660. }
  661. tables = page.find_tables(table_settings=table_settings)
  662. for table in tables:
  663. # 提取表格数据
  664. table_data = table.extract()
  665. if table_data and len(table_data) > 0:
  666. # 转换为 DataFrame
  667. df = pd.DataFrame(table_data)
  668. # 获取表格边界框
  669. bbox = table.bbox # (x0, top, x1, bottom)
  670. tables_data.append((page_num, df, bbox))
  671. return tables_data
  672. def is_likely_header_only(table_df: pd.DataFrame, min_data_rows: int = 2) -> bool:
  673. """
  674. 判断表格是否只包含表头(没有数据行或数据行很少)
  675. Args:
  676. table_df: 表格DataFrame
  677. min_data_rows: 最少数据行数,少于这个数认为是只有表头
  678. Returns:
  679. bool: 是否只包含表头
  680. """
  681. if table_df.empty:
  682. return True
  683. # 如果总行数少于等于表头行数(假设表头占1-3行)+ 最小数据行数
  684. header_rows = min(3, len(table_df))
  685. return len(table_df) <= header_rows + min_data_rows
  686. def has_similar_structure(table1_df: pd.DataFrame, table2_df: pd.DataFrame,
  687. tolerance: int = 1) -> bool:
  688. """
  689. 判断两个表格是否有相似的结构(列数相近)
  690. Args:
  691. table1_df: 第一个表格DataFrame
  692. table2_df: 第二个表格DataFrame
  693. tolerance: 允许的列数差异
  694. Returns:
  695. bool: 是否有相似结构
  696. """
  697. if table1_df.empty or table2_df.empty:
  698. return False
  699. cols1 = len(table1_df.columns)
  700. cols2 = len(table2_df.columns)
  701. return abs(cols1 - cols2) <= tolerance
  702. def merge_cross_page_tables(matched_tables: List[Tuple[int, object]],
  703. tables: object, header_rules: List[dict]) -> List[Tuple[int, pd.DataFrame, str]]:
  704. """
  705. 合并跨页表格
  706. 处理两种情况:
  707. 1. 表头在上一页,内容在下一页
  708. 2. 表格在上一页未显示完,下一页继续(包括未匹配到规则的表格)
  709. Args:
  710. matched_tables: 已匹配的表格列表 [(原始索引, table对象), ...]
  711. tables: 所有表格对象
  712. header_rules: 表头规则列表
  713. Returns:
  714. List[Tuple[int, pd.DataFrame, str]]: 合并后的表格列表 [(原始索引, 合并后的DataFrame, 规则名称), ...]
  715. """
  716. if not ENABLE_MERGE_CROSS_PAGE_TABLES:
  717. # 不启用跨页合并,直接返回原表格
  718. result = []
  719. for orig_idx, table in matched_tables:
  720. result.append((orig_idx, table.df, ""))
  721. return result
  722. print("\n正在检测和合并跨页表格...")
  723. # 构建页面索引映射:page -> [(table_index, table, rule_name), ...]
  724. # 同时记录所有表格(包括未匹配的),用于跨页合并
  725. page_to_tables = {}
  726. page_to_all_tables = {} # 所有表格(包括未匹配的)
  727. table_to_rule = {}
  728. matched_indices = set() # 已匹配的表格索引
  729. # 先记录所有表格
  730. for i, table in enumerate(tables):
  731. page = table.page
  732. if page not in page_to_all_tables:
  733. page_to_all_tables[page] = []
  734. page_to_all_tables[page].append((i, table))
  735. # 记录已匹配的表格
  736. for orig_idx, table in matched_tables:
  737. page = table.page
  738. matched_indices.add(orig_idx)
  739. if page not in page_to_tables:
  740. page_to_tables[page] = []
  741. # 找到匹配的规则名称
  742. rule_name = ""
  743. for rule in header_rules:
  744. is_match, name = check_table_header(table.df, rule)
  745. if is_match:
  746. rule_name = name
  747. break
  748. page_to_tables[page].append((orig_idx, table, rule_name))
  749. table_to_rule[orig_idx] = rule_name
  750. merged_results = []
  751. processed_indices = set()
  752. # 按页面顺序处理
  753. sorted_pages = sorted(page_to_tables.keys())
  754. for page_idx, current_page in enumerate(sorted_pages):
  755. for orig_idx, table, rule_name in page_to_tables[current_page]:
  756. if orig_idx in processed_indices:
  757. continue
  758. # 检查是否是只有表头的表格(可能在上一页结尾)
  759. is_header_only = is_likely_header_only(table.df)
  760. current_df = table.df.copy()
  761. # 情况1: 当前表格只有表头,检查下一页是否有内容
  762. # 限制:最多只能跨1页(只检查下一页)
  763. if is_header_only:
  764. # 只检查下一页(页面号相差1),不检查更多页
  765. if page_idx + 1 < len(sorted_pages):
  766. next_page = sorted_pages[page_idx + 1]
  767. # 严格检查:页面号必须相差1
  768. if abs(next_page - current_page) == 1:
  769. # 先检查已匹配的表格
  770. if next_page in page_to_tables:
  771. for next_orig_idx, next_table, next_rule_name in page_to_tables[next_page]:
  772. if next_orig_idx in processed_indices:
  773. continue
  774. # 检查是否有相似结构且规则名称匹配
  775. if (has_similar_structure(current_df, next_table.df) and
  776. rule_name == next_rule_name):
  777. # 合并:保留当前表格的表头,添加下一页的数据
  778. # 假设表头占前3行
  779. header_rows = min(3, len(current_df))
  780. header_df = current_df.iloc[:header_rows].copy()
  781. # 下一页的数据(跳过可能的重复表头)
  782. next_data_df = next_table.df.copy()
  783. # 如果下一页第一行看起来像表头,跳过
  784. if len(next_data_df) > 0:
  785. first_row_text = " ".join(next_data_df.iloc[0].astype(str).str.strip().tolist())
  786. # 简单判断:如果第一行包含很多关键词,可能是表头
  787. keyword_count = sum(1 for kw in header_rules[0].get('keywords', []) if kw in first_row_text) if header_rules else 0
  788. if keyword_count >= 2:
  789. next_data_df = next_data_df.iloc[1:].copy()
  790. # 合并
  791. merged_df = pd.concat([header_df, next_data_df], ignore_index=True)
  792. merged_results.append((orig_idx, merged_df, rule_name))
  793. processed_indices.add(orig_idx)
  794. processed_indices.add(next_orig_idx)
  795. print(f" ✓ 合并跨页表格: 页面 {current_page} (表头) + 页面 {next_page} (内容) -> 规则: {rule_name}")
  796. break
  797. if orig_idx in processed_indices:
  798. break
  799. # 如果已匹配的表格没有找到,检查所有表格(包括未匹配的)
  800. if orig_idx not in processed_indices and next_page in page_to_all_tables:
  801. for next_orig_idx, next_table in page_to_all_tables[next_page]:
  802. if next_orig_idx in processed_indices:
  803. continue
  804. # 检查是否有相似结构(列数相同或相近)
  805. # 同时检查下一页表格是否匹配相同的规则(防止不同规则的表格被合并)
  806. next_table_rule_name = ""
  807. if header_rules:
  808. for rule in header_rules:
  809. is_match, name = check_table_header(next_table.df, rule)
  810. if is_match:
  811. next_table_rule_name = name
  812. break
  813. # 只有规则名称匹配且结构相似时,才考虑合并
  814. if (has_similar_structure(current_df, next_table.df) and
  815. (rule_name == next_table_rule_name or next_table_rule_name == "")):
  816. # 检查下一页第一行是否像表头
  817. next_first_row_text = ""
  818. if not next_table.df.empty:
  819. next_first_row_text = " ".join(next_table.df.iloc[0].astype(str).str.strip().tolist())
  820. # 检查是否包含表头关键词
  821. keyword_count = 0
  822. if header_rules and rule_name:
  823. for rule in header_rules:
  824. if rule.get('name') == rule_name:
  825. keywords = rule.get('keywords', [])
  826. keyword_count = sum(1 for kw in keywords if kw in next_first_row_text)
  827. break
  828. # 如果下一页第一行不像表头(关键词少于2个),认为是表格的继续
  829. # 但如果下一页匹配了不同的规则,则不合并
  830. if keyword_count < 2 and (rule_name == next_table_rule_name or next_table_rule_name == ""):
  831. # 合并:保留当前表格的表头,添加下一页的数据
  832. header_rows = min(3, len(current_df))
  833. header_df = current_df.iloc[:header_rows].copy()
  834. next_data_df = next_table.df.copy()
  835. # 如果列数不同,尝试对齐列
  836. if len(header_df.columns) != len(next_data_df.columns):
  837. # 如果下一页列数少,添加空列
  838. if len(next_data_df.columns) < len(header_df.columns):
  839. for i in range(len(next_data_df.columns), len(header_df.columns)):
  840. next_data_df[len(next_data_df.columns)] = ""
  841. merged_df = pd.concat([header_df, next_data_df], ignore_index=True)
  842. merged_results.append((orig_idx, merged_df, rule_name))
  843. processed_indices.add(orig_idx)
  844. processed_indices.add(next_orig_idx)
  845. print(f" ✓ 合并跨页表格: 页面 {current_page} (表头) + 页面 {next_page} (内容,未匹配) -> 规则: {rule_name}")
  846. break
  847. if orig_idx in processed_indices:
  848. break
  849. # 情况2: 当前表格有内容,检查上一页是否有表头
  850. # 限制:最多只能跨1页(只检查上一页)
  851. if orig_idx not in processed_indices and not is_header_only:
  852. # 只检查上一页(页面号相差1),不检查更多页
  853. if page_idx > 0:
  854. prev_page = sorted_pages[page_idx - 1]
  855. # 严格检查:页面号必须相差1
  856. if abs(prev_page - current_page) == 1:
  857. if prev_page in page_to_tables:
  858. for prev_orig_idx, prev_table, prev_rule_name in page_to_tables[prev_page]:
  859. if prev_orig_idx in processed_indices:
  860. continue
  861. # 检查上一页是否只有表头,且结构相似
  862. if (is_likely_header_only(prev_table.df) and
  863. has_similar_structure(prev_table.df, current_df) and
  864. rule_name == prev_rule_name):
  865. # 合并:使用上一页的表头 + 当前页的数据
  866. header_rows = min(3, len(prev_table.df))
  867. header_df = prev_table.df.iloc[:header_rows].copy()
  868. # 当前页的数据(跳过可能的重复表头)
  869. current_data_df = current_df.copy()
  870. if len(current_data_df) > 0:
  871. first_row_text = " ".join(current_data_df.iloc[0].astype(str).str.strip().tolist())
  872. keyword_count = sum(1 for kw in header_rules[0].get('keywords', []) if kw in first_row_text) if header_rules else 0
  873. if keyword_count >= 2:
  874. current_data_df = current_data_df.iloc[1:].copy()
  875. merged_df = pd.concat([header_df, current_data_df], ignore_index=True)
  876. merged_results.append((orig_idx, merged_df, rule_name))
  877. processed_indices.add(orig_idx)
  878. processed_indices.add(prev_orig_idx)
  879. print(f" ✓ 合并跨页表格: 页面 {prev_page} (表头) + 页面 {current_page} (内容) -> 规则: {rule_name}")
  880. break
  881. if orig_idx in processed_indices:
  882. break
  883. # 情况3: 表格跨页继续(上一页未显示完,下一页继续)
  884. # 检查相邻页面的所有表格(包括未匹配到规则的),如果列数相同且第一行不像表头,则合并
  885. # 限制:最多只能跨1页(只检查下一页,不检查更多页)
  886. if orig_idx not in processed_indices:
  887. # 只检查下一页(页面号相差1),不检查更多页
  888. if page_idx + 1 < len(sorted_pages):
  889. next_page = sorted_pages[page_idx + 1]
  890. # 严格检查:页面号必须相差1
  891. if abs(next_page - current_page) == 1:
  892. # 检查下一页的所有表格(包括未匹配的)
  893. if next_page in page_to_all_tables:
  894. for next_orig_idx, next_table in page_to_all_tables[next_page]:
  895. if next_orig_idx in processed_indices:
  896. continue
  897. # 检查结构是否相似(列数相同或相近)
  898. # 同时检查下一页表格是否匹配相同的规则(防止不同规则的表格被合并)
  899. next_table_rule_name = ""
  900. if header_rules:
  901. for rule in header_rules:
  902. is_match, name = check_table_header(next_table.df, rule)
  903. if is_match:
  904. next_table_rule_name = name
  905. break
  906. # 只有规则名称匹配且结构相似时,才考虑合并
  907. if (has_similar_structure(current_df, next_table.df) and
  908. (rule_name == next_table_rule_name or next_table_rule_name == "")):
  909. # 检查下一页第一行是否像表头
  910. next_first_row_text = ""
  911. if not next_table.df.empty:
  912. next_first_row_text = " ".join(next_table.df.iloc[0].astype(str).str.strip().tolist())
  913. # 检查是否包含表头关键词
  914. keyword_count = 0
  915. if header_rules and rule_name:
  916. for rule in header_rules:
  917. if rule.get('name') == rule_name:
  918. keywords = rule.get('keywords', [])
  919. keyword_count = sum(1 for kw in keywords if kw in next_first_row_text)
  920. break
  921. # 检查合并后的表格是否包含新的表头(防止跨多页合并)
  922. # 检查下一页的数据中是否包含其他规则的表头
  923. has_other_header = False
  924. if len(next_table.df) > 0:
  925. # 检查前几行是否包含其他规则的关键词
  926. for check_row_idx in range(min(3, len(next_table.df))):
  927. check_row_text = " ".join(next_table.df.iloc[check_row_idx].astype(str).str.strip().tolist())
  928. # 检查是否包含其他规则的关键词
  929. for other_rule in header_rules:
  930. if other_rule.get('name') != rule_name:
  931. other_keywords = other_rule.get('keywords', [])
  932. other_keyword_count = sum(1 for kw in other_keywords if kw in check_row_text)
  933. if other_keyword_count >= 2:
  934. has_other_header = True
  935. break
  936. if has_other_header:
  937. break
  938. # 如果下一页第一行不像表头(关键词少于2个),且不包含其他表头,且规则匹配,认为是表格的继续
  939. # 注意:如果下一页第一行是表头(keyword_count >= 2),或匹配了不同的规则,则不合并
  940. if (keyword_count < 2 and not has_other_header and
  941. (rule_name == next_table_rule_name or next_table_rule_name == "")):
  942. # 合并:当前表格 + 下一页的数据
  943. next_data_df = next_table.df.copy()
  944. merged_df = pd.concat([current_df, next_data_df], ignore_index=True)
  945. merged_results.append((orig_idx, merged_df, rule_name))
  946. processed_indices.add(orig_idx)
  947. processed_indices.add(next_orig_idx)
  948. next_rule_display = rule_name if next_orig_idx in matched_indices else "未匹配"
  949. print(f" ✓ 合并跨页表格: 页面 {current_page} + 页面 {next_page} (继续) -> 规则: {rule_name} (下一页表格: {next_rule_display})")
  950. break
  951. if orig_idx in processed_indices:
  952. break
  953. # 如果没有被合并,单独添加
  954. if orig_idx not in processed_indices:
  955. merged_results.append((orig_idx, current_df, rule_name))
  956. processed_indices.add(orig_idx)
  957. print(f"✓ 跨页表格合并完成,共 {len(merged_results)} 个表格(含合并后的)")
  958. return merged_results
  959. def load_tables_from_xlsx(xlsx_dir: Path) -> List[Tuple[int, pd.DataFrame, int]]:
  960. """
  961. 从xlsx文件中加载表格,按页面和表格顺序排序
  962. Args:
  963. xlsx_dir: xlsx文件所在目录
  964. Returns:
  965. List[Tuple[int, pd.DataFrame, int]]: [(索引, DataFrame, 页面号), ...]
  966. """
  967. xlsx_files = list(xlsx_dir.glob("*.xlsx"))
  968. if not xlsx_files:
  969. print(f"⚠ 在目录 {xlsx_dir} 中未找到xlsx文件")
  970. return []
  971. # 解析文件名并排序:先按页码,再按表格序号
  972. file_info = []
  973. for xlsx_file in xlsx_files:
  974. # 支持两种格式:
  975. # 1. 新格式:table_page8_1of3.xlsx -> page=8, table_num=1, total=3
  976. # 2. 旧格式:table_page8_3.xlsx -> page=8, table_num=3
  977. match_new = re.search(r'page(\d+)_(\d+)of(\d+)', xlsx_file.stem)
  978. match_old = re.search(r'page(\d+)_(\d+)', xlsx_file.stem)
  979. if match_new:
  980. page_num = int(match_new.group(1))
  981. table_num = int(match_new.group(2))
  982. file_info.append((xlsx_file, page_num, table_num))
  983. elif match_old:
  984. page_num = int(match_old.group(1))
  985. table_num = int(match_old.group(2))
  986. file_info.append((xlsx_file, page_num, table_num))
  987. else:
  988. # 如果无法解析,尝试只提取页码
  989. page_match = re.search(r'page(\d+)', xlsx_file.stem)
  990. if page_match:
  991. page_num = int(page_match.group(1))
  992. file_info.append((xlsx_file, page_num, 0))
  993. else:
  994. # 完全无法解析,使用默认值
  995. file_info.append((xlsx_file, 9999, 0))
  996. # 按页码和表格序号排序
  997. file_info.sort(key=lambda x: (x[1], x[2]))
  998. tables = []
  999. for idx, (xlsx_file, page_num, table_num) in enumerate(file_info):
  1000. try:
  1001. df = pd.read_excel(xlsx_file, header=None)
  1002. tables.append((idx, df, page_num))
  1003. except Exception as e:
  1004. print(f"⚠ 读取文件 {xlsx_file.name} 失败: {e}")
  1005. return tables
  1006. def get_header_rules_for_file(pdf_path: str) -> List[dict]:
  1007. """
  1008. 根据PDF文件名获取对应的表头规则(精确匹配文件名)
  1009. Args:
  1010. pdf_path: PDF文件路径
  1011. Returns:
  1012. List[dict]: 表头规则列表
  1013. """
  1014. pdf_filename = Path(pdf_path).name
  1015. # 精确匹配文件名(固定匹配,不进行模糊匹配)
  1016. if pdf_filename in TABLE_HEADER_RULES:
  1017. return TABLE_HEADER_RULES[pdf_filename]
  1018. return []
  1019. def preview_table_headers(tables, max_preview: int = 10):
  1020. """预览表格表头,帮助用户确定过滤条件(支持多行表头)"""
  1021. print("\n" + "=" * 60)
  1022. print("表格表头预览(前几个表格,包含多行表头):")
  1023. print("=" * 60)
  1024. preview_count = min(max_preview, tables.n)
  1025. for i in range(preview_count):
  1026. table = tables[i]
  1027. if table.df.empty:
  1028. print(f"表格 {i+1} (页面 {table.page}): [空表格]")
  1029. continue
  1030. # 合并前3行作为表头预览(处理换行情况)
  1031. header_rows_to_check = min(3, len(table.df))
  1032. header_parts = []
  1033. for row_idx in range(header_rows_to_check):
  1034. row = table.df.iloc[row_idx].astype(str).str.strip()
  1035. for cell in row[:5]: # 只显示前5列
  1036. cell_text = str(cell).strip()
  1037. if cell_text and cell_text.lower() not in ['nan', 'none', '']:
  1038. header_parts.append(cell_text)
  1039. header_text = " | ".join(header_parts[:10]) # 最多显示10个部分
  1040. if len(header_parts) > 10:
  1041. header_text += " ..."
  1042. print(f"表格 {i+1} (页面 {table.page}): {header_text}")
  1043. if tables.n > max_preview:
  1044. print(f"... 还有 {tables.n - max_preview} 个表格未显示")
  1045. print("=" * 60 + "\n")
  1046. # 检查并转换文件(如果是 CEB)
  1047. pdf_path = check_and_convert_file(pdf_path)
  1048. print(f"处理文件: {pdf_path}\n")
  1049. # 创建输出目录
  1050. output_dir.mkdir(exist_ok=True)
  1051. # 判断是从PDF提取还是从xlsx文件过滤
  1052. if FILTER_FROM_EXISTING_XLSX and XLSX_INPUT_DIR:
  1053. xlsx_input_path = Path(XLSX_INPUT_DIR)
  1054. # 检查xlsx文件是否存在
  1055. xlsx_files = list(xlsx_input_path.glob("*.xlsx")) if xlsx_input_path.exists() else []
  1056. if not xlsx_files:
  1057. print("=" * 60)
  1058. print("未找到xlsx文件,先从PDF生成xlsx文件...")
  1059. print("=" * 60)
  1060. # 先创建目录
  1061. xlsx_input_path.mkdir(parents=True, exist_ok=True)
  1062. # 从PDF提取所有表格
  1063. print("\n正在读取PDF文件并提取表格,请稍候...")
  1064. start_time = time.time()
  1065. # 使用 pdfplumber 提取表格
  1066. tables_data = extract_tables_with_pdfplumber(pdf_path, pages='all')
  1067. extract_time = time.time() - start_time
  1068. print(f"\n✓ 表格提取完成!共提取到 {len(tables_data)} 个表格 (耗时: {extract_time:.2f}秒)")
  1069. # 保存到 extracted_tables 目录(包含表格前文本)
  1070. extracted_output_dir = Path('extracted_tables')
  1071. extracted_output_dir.mkdir(parents=True, exist_ok=True)
  1072. print(f"\n正在保存所有表格为xlsx文件到: {extracted_output_dir}")
  1073. # 先统计每页的表格数量
  1074. page_table_counts = {}
  1075. for page_num, df, bbox in tables_data:
  1076. if page_num not in page_table_counts:
  1077. page_table_counts[page_num] = 0
  1078. page_table_counts[page_num] += 1
  1079. # 为每个页面维护一个计数器
  1080. page_current_index = {}
  1081. # 保存所有表格为xlsx文件,使用新的命名格式
  1082. saved_count = 0
  1083. for i, (page_num, df, bbox) in tqdm(enumerate(tables_data), total=len(tables_data),
  1084. desc="保存xlsx", unit="个", ncols=80):
  1085. # 更新该页面的当前索引
  1086. if page_num not in page_current_index:
  1087. page_current_index[page_num] = 0
  1088. page_current_index[page_num] += 1
  1089. current_num = page_current_index[page_num]
  1090. total_num = page_table_counts[page_num]
  1091. # 使用新的命名格式: table_page{page}_{current}of{total}.xlsx
  1092. xlsx_file = extracted_output_dir / f"table_page{page_num}_{current_num}of{total_num}.xlsx"
  1093. try:
  1094. # 提取表格前的文本
  1095. text_before = ""
  1096. if EXTRACT_TEXT_BEFORE_TABLE and PYMUPDF_AVAILABLE:
  1097. text_before = extract_text_before_table(pdf_path, page_num, bbox, TEXT_LINES_BEFORE_TABLE)
  1098. # 如果有文本,将其添加到表格的第一行
  1099. if text_before:
  1100. # 创建一个新的 DataFrame,第一行是文本信息
  1101. text_row = pd.DataFrame([[f"[表格前文本] {text_before}"]], columns=[0])
  1102. # 将原表格的列数调整为与文本行一致
  1103. table_df = df.copy()
  1104. # 如果表格列数大于1,在文本行后面填充空值
  1105. if len(table_df.columns) > 1:
  1106. for col_idx in range(1, len(table_df.columns)):
  1107. text_row[col_idx] = ""
  1108. # 合并文本行和表格
  1109. combined_df = pd.concat([text_row, table_df], ignore_index=True)
  1110. combined_df.to_excel(str(xlsx_file), index=False, header=False)
  1111. else:
  1112. # 没有文本,直接保存表格
  1113. df.to_excel(str(xlsx_file), index=False, header=False)
  1114. saved_count += 1
  1115. except Exception as e:
  1116. tqdm.write(f"⚠ 表格 {i+1} (页面 {page_num}) 保存失败: {e}")
  1117. print(f"\n✓ 已保存 {saved_count} 个xlsx文件到 {extracted_output_dir}")
  1118. # 更新 xlsx_input_path 指向 extracted_tables
  1119. xlsx_input_path = extracted_output_dir
  1120. print(f"\n✓ 已保存 {saved_count} 个xlsx文件到 {extracted_output_dir}")
  1121. # 更新 xlsx_input_path 指向 extracted_tables
  1122. xlsx_input_path = extracted_output_dir
  1123. print("=" * 60)
  1124. print("现在开始合并跨页表格...")
  1125. print("=" * 60)
  1126. # ========== 步骤2: 合并跨页表格 ==========
  1127. # 从 extracted_tables 读取,合并后保存到 merged_tables(不进行过滤)
  1128. # 检查 merged_tables 是否已经存在
  1129. merged_files_exist = list(merged_output_dir.glob("*.xlsx")) if merged_output_dir.exists() else []
  1130. if not merged_files_exist:
  1131. print(f"\n正在从 {xlsx_input_path} 读取表格...")
  1132. start_time = time.time()
  1133. extracted_tables = load_tables_from_xlsx(xlsx_input_path)
  1134. load_time = time.time() - start_time
  1135. if not extracted_tables:
  1136. print("⚠ 未找到任何xlsx文件")
  1137. exit(1)
  1138. print(f"✓ 加载完成!共加载 {len(extracted_tables)} 个xlsx文件 (耗时: {load_time:.2f}秒)")
  1139. # 执行合并(合并所有表格,不进行过滤)
  1140. print("\n正在合并跨页表格(基于表格前文本)...")
  1141. merged_tables_list = []
  1142. i = 0
  1143. while i < len(extracted_tables):
  1144. idx, df, page = extracted_tables[i]
  1145. # 检查是否有表格前文本
  1146. has_text, text_content = has_text_before_table(df)
  1147. if not has_text:
  1148. # 没有前文本,跳过(孤立数据)
  1149. print(f" ⚠ 跳过表格 {idx} (页面 {page}): 没有表格前文本")
  1150. i += 1
  1151. continue
  1152. # 有前文本,这是一个新表格的开始
  1153. # 移除文本行,获取纯表格数据
  1154. merged_df = remove_text_row(df)
  1155. # 判断是否是 table_3(用于调试)
  1156. is_table_3 = (len(merged_tables_list) == 2) # table_3 是第3个表格(索引2)
  1157. # 先合并被截断的行(在清理换行符之前,这样可以保留原始的 NaN 值)
  1158. if is_table_3:
  1159. print(f"\n[DEBUG] 处理 table_3 (索引 {len(merged_tables_list)})")
  1160. merged_df = merge_broken_rows(merged_df, header_rows=1, debug=is_table_3)
  1161. # 再清理换行符
  1162. merged_df = clean_newlines(merged_df)
  1163. merged_count = 0
  1164. merged_pages = [page]
  1165. merged_indices = [idx]
  1166. j = i + 1
  1167. # 检查后续表格是否需要合并
  1168. while j < len(extracted_tables):
  1169. next_idx, next_df, next_page = extracted_tables[j]
  1170. # 检查下一个表格是否有前文本
  1171. next_has_text, _ = has_text_before_table(next_df)
  1172. if next_has_text:
  1173. # 下一个表格有前文本,是新表格的开始,停止合并
  1174. break
  1175. # 下一个表格没有前文本,检查是否应该合并
  1176. page_diff = next_page - merged_pages[-1]
  1177. next_df_clean = remove_text_row(next_df)
  1178. # 先合并被截断的行
  1179. next_df_clean = merge_broken_rows(next_df_clean, header_rows=0)
  1180. # 再清理换行符
  1181. next_df_clean = clean_newlines(next_df_clean)
  1182. if page_diff <= CROSS_PAGE_SEARCH_RANGE and has_similar_structure(merged_df, next_df_clean):
  1183. # 合并
  1184. merged_df = pd.concat([merged_df, next_df_clean], ignore_index=True)
  1185. merged_count += 1
  1186. merged_pages.append(next_page)
  1187. merged_indices.append(next_idx)
  1188. print(f" ✓ 合并: 页面 {page} + 页面 {next_page} [文本: {text_content[:40]}...]")
  1189. j += 1
  1190. else:
  1191. # 页面距离太远或结构不同,停止合并
  1192. break
  1193. # 跨页合并完成后,再次处理被截断的行
  1194. if merged_count > 0:
  1195. if is_table_3:
  1196. print(f"\n[DEBUG] table_3 跨页合并后,再次检查被截断的行")
  1197. merged_df = merge_broken_rows(merged_df, header_rows=1, debug=is_table_3)
  1198. # 添加合并后的表格(保存所有表格,不进行过滤)
  1199. merged_tables_list.append((idx, merged_df, page, text_content))
  1200. i = j if merged_count > 0 else i + 1
  1201. print(f"\n✓ 合并完成!从 {len(extracted_tables)} 个原始表格合并为 {len(merged_tables_list)} 个表格")
  1202. # 保存合并后的表格到 merged_tables 目录(保存所有表格)
  1203. merged_output_dir.mkdir(parents=True, exist_ok=True)
  1204. print(f"\n正在保存合并后的表格到: {merged_output_dir}...")
  1205. saved_count = 0
  1206. for idx, (orig_idx, merged_df, page, text_content) in tqdm(enumerate(merged_tables_list),
  1207. total=len(merged_tables_list),
  1208. desc="保存合并表格", unit="个", ncols=80):
  1209. # 使用简单的命名: table_1.xlsx, table_2.xlsx, ...
  1210. xlsx_file = merged_output_dir / f"table_{idx+1}.xlsx"
  1211. try:
  1212. # 保存纯表格数据(已经移除了文本行)
  1213. merged_df.to_excel(str(xlsx_file), index=False, header=False)
  1214. saved_count += 1
  1215. except Exception as e:
  1216. tqdm.write(f"⚠ 表格 {idx+1} 保存失败: {e}")
  1217. print(f"\n✓ 已保存 {saved_count} 个合并后的表格到 {merged_output_dir}")
  1218. else:
  1219. print(f"\n✓ 发现已存在的合并表格,跳过合并步骤")
  1220. print("=" * 60)
  1221. print("现在开始从合并表格中过滤...")
  1222. print("=" * 60)
  1223. # ========== 步骤3: 从 merged_tables 加载用于过滤 ==========
  1224. # 从 merged_tables 读取(只从这一个目录读取)
  1225. print(f"\n正在从 {merged_output_dir} 读取合并后的表格...")
  1226. start_time = time.time()
  1227. xlsx_tables = load_tables_from_xlsx(merged_output_dir)
  1228. load_time = time.time() - start_time
  1229. if not xlsx_tables:
  1230. print("⚠ 未找到任何xlsx文件")
  1231. exit(1)
  1232. print(f"\n✓ 加载完成!共加载 {len(xlsx_tables)} 个xlsx文件 (耗时: {load_time:.2f}秒)")
  1233. # 验证 merged_tables 中没有文本行
  1234. text_row_count = 0
  1235. for idx, df, page in xlsx_tables:
  1236. has_text, _ = has_text_before_table(df)
  1237. if has_text:
  1238. text_row_count += 1
  1239. print(f" ⚠ 警告: 表格 {idx+1} 仍包含 [表格前文本] 行")
  1240. if text_row_count > 0:
  1241. print(f"\n⚠ 发现 {text_row_count} 个表格仍包含文本行,这不应该发生!")
  1242. print(" merged_tables 应该只包含纯表格数据")
  1243. else:
  1244. print(f"✓ 验证通过:所有表格都不包含 [表格前文本] 行")
  1245. # 转换为类似camelot tables的对象结构
  1246. class XlsxTable:
  1247. def __init__(self, df, page):
  1248. self.df = df
  1249. self.page = page
  1250. # 创建模拟的tables对象
  1251. class XlsxTables:
  1252. def __init__(self, xlsx_tables):
  1253. self.n = len(xlsx_tables)
  1254. self._tables = [XlsxTable(df, page) for _, df, page in xlsx_tables]
  1255. def __getitem__(self, idx):
  1256. return self._tables[idx]
  1257. def __iter__(self):
  1258. return iter(self._tables)
  1259. tables = XlsxTables(xlsx_tables)
  1260. else:
  1261. print("=" * 60)
  1262. print("开始提取PDF表格...")
  1263. print("=" * 60)
  1264. # 提取表格
  1265. print("\n正在读取PDF文件并提取表格,请稍候...")
  1266. start_time = time.time()
  1267. # 使用 pdfplumber 提取表格
  1268. tables_data = extract_tables_with_pdfplumber(pdf_path, pages='all')
  1269. extract_time = time.time() - start_time
  1270. print(f"\n✓ 表格提取完成!共提取到 {len(tables_data)} 个表格 (耗时: {extract_time:.2f}秒)")
  1271. # 转换为类似的对象结构以兼容后续代码
  1272. class PdfPlumberTable:
  1273. def __init__(self, df, page):
  1274. self.df = df
  1275. self.page = page
  1276. class PdfPlumberTables:
  1277. def __init__(self, tables_data):
  1278. self.n = len(tables_data)
  1279. self._tables = [PdfPlumberTable(df, page) for page, df, bbox in tables_data]
  1280. def __getitem__(self, idx):
  1281. return self._tables[idx]
  1282. def __iter__(self):
  1283. return iter(self._tables)
  1284. tables = PdfPlumberTables(tables_data)
  1285. # 显示表头预览
  1286. if SHOW_HEADER_PREVIEW:
  1287. preview_table_headers(tables)
  1288. # 获取当前文件对应的表头规则
  1289. if FILTER_FROM_EXISTING_XLSX:
  1290. # 从xlsx过滤时,使用PDF文件名来匹配规则(如果xlsx是从该PDF生成的)
  1291. header_rules = get_header_rules_for_file(pdf_path)
  1292. else:
  1293. header_rules = get_header_rules_for_file(pdf_path)
  1294. # 过滤表格
  1295. if ENABLE_HEADER_FILTER:
  1296. if not header_rules:
  1297. print(f"\n⚠ 警告: 未找到文件 '{Path(pdf_path).name}' 对应的表头规则!")
  1298. print("请在 TABLE_HEADER_RULES 中添加该文件的规则配置")
  1299. print("或者设置 ENABLE_HEADER_FILTER = False 来提取所有表格")
  1300. user_input = input("\n是否继续提取所有表格?(y/n): ").strip().lower()
  1301. if user_input != 'y':
  1302. exit(0)
  1303. # 根据数据源类型处理
  1304. if FILTER_FROM_EXISTING_XLSX and xlsx_tables:
  1305. matched_tables = [(idx, df, page) for idx, df, page in xlsx_tables]
  1306. else:
  1307. matched_tables = [(i, table.df, table.page) for i, table in enumerate(tables)]
  1308. print(f"✓ 将处理所有 {len(matched_tables)} 个表格")
  1309. merged_tables_final = [(idx, df, "") for idx, df, page in matched_tables]
  1310. else:
  1311. print(f"\n找到 {len(header_rules)} 个表头规则:")
  1312. for rule in header_rules:
  1313. print(f" - {rule['name']}: {', '.join(rule['keywords'][:3])}...")
  1314. print("\n正在根据表头规则过滤表格...")
  1315. # 根据数据源类型过滤
  1316. if FILTER_FROM_EXISTING_XLSX and xlsx_tables:
  1317. # 从 merged_tables 文件过滤 - 这些表格已经合并过了,不需要再合并
  1318. all_tables_info = [] # [(索引, DataFrame, 页面号, 是否匹配, 规则名称), ...]
  1319. for idx, df, page in xlsx_tables:
  1320. # 检查是否匹配任何规则(merged_tables 中已经没有文本行了)
  1321. matched = False
  1322. matched_rule = ""
  1323. for rule in header_rules:
  1324. is_match, rule_name = check_table_header(df, rule)
  1325. if is_match:
  1326. matched = True
  1327. matched_rule = rule_name
  1328. break
  1329. all_tables_info.append((idx, df, page, matched, matched_rule))
  1330. print(f"✓ 检查了 {len(all_tables_info)} 个表格")
  1331. # 只保留匹配的表格(不需要合并,因为已经在 merged_tables 中合并过了)
  1332. merged_tables_final = [(idx, df, rule_name)
  1333. for idx, df, page, matched, rule_name in all_tables_info if matched]
  1334. if len(merged_tables_final) == 0:
  1335. print("\n⚠ 警告: 没有找到匹配的表格!")
  1336. print("请检查 TABLE_HEADER_RULES 配置")
  1337. exit(1)
  1338. else:
  1339. # 从 PDF 提取
  1340. matched_tables = []
  1341. matched_rules_info = {} # 记录每个表格匹配的规则
  1342. for i, table in enumerate(tables):
  1343. for rule in header_rules:
  1344. is_match, rule_name = check_table_header(table.df, rule)
  1345. if is_match:
  1346. matched_tables.append((i, table))
  1347. matched_rules_info[i] = rule_name
  1348. break # 匹配到一个规则就停止
  1349. print(f"✓ 匹配到 {len(matched_tables)} 个符合条件的表格")
  1350. if len(matched_tables) == 0:
  1351. print("\n⚠ 警告: 没有找到匹配的表格!")
  1352. print("请检查 TABLE_HEADER_RULES 配置")
  1353. print("如果 SHOW_HEADER_PREVIEW=True,可以查看上面的表头预览来调整配置")
  1354. exit(1)
  1355. # 合并跨页表格(从 PDF)
  1356. # TODO: 实现 PDF 的跨页合并(暂时不实现)
  1357. merged_tables_final = [(orig_idx, table.df, matched_rules_info.get(orig_idx, ""))
  1358. for orig_idx, table in matched_tables]
  1359. # 显示匹配的表格信息(支持多行表头)
  1360. print("\n匹配的表格:")
  1361. for idx, (orig_idx, table_df, rule_name) in enumerate(merged_tables_final):
  1362. if table_df.empty:
  1363. header_preview = "[空表格]"
  1364. else:
  1365. # 合并前3行作为表头预览(处理换行情况)
  1366. header_rows_to_check = min(3, len(table_df))
  1367. header_parts = []
  1368. for row_idx in range(header_rows_to_check):
  1369. row = table_df.iloc[row_idx].astype(str).str.strip()
  1370. for cell in row[:3]: # 只显示前3列
  1371. cell_text = str(cell).strip()
  1372. if cell_text and cell_text.lower() not in ['nan', 'none', '']:
  1373. # 去除换行符
  1374. cell_text = cell_text.replace('\n', '').replace('\r', '')
  1375. header_parts.append(cell_text)
  1376. header_preview = " | ".join(header_parts[:6]) # 最多显示6个部分
  1377. if len(header_parts) > 6:
  1378. header_preview += " ..."
  1379. if SHOW_MATCHED_RULE_NAME:
  1380. print(f" {idx+1}. 原表格 {orig_idx+1} [{rule_name}]: {header_preview} (行数: {len(table_df)})")
  1381. else:
  1382. print(f" {idx+1}. 原表格 {orig_idx+1}: {header_preview} (行数: {len(table_df)})")
  1383. # 更新matched_tables为合并后的结果,保留页面信息
  1384. # 需要从原始 xlsx_tables 中获取页面信息
  1385. matched_tables = []
  1386. for orig_idx, table_df, _ in merged_tables_final:
  1387. # 查找原始表格的页面信息
  1388. page = 0
  1389. if isinstance(orig_idx, str):
  1390. # 如果是分割后的表格(格式:idx_subidx)
  1391. base_idx = int(orig_idx.split('_')[0])
  1392. else:
  1393. base_idx = orig_idx
  1394. # 从 xlsx_tables 中查找页面号
  1395. for idx, df, p in xlsx_tables:
  1396. if idx == base_idx:
  1397. page = p
  1398. break
  1399. matched_tables.append((orig_idx, table_df, page))
  1400. else:
  1401. # 不启用表头过滤
  1402. if FILTER_FROM_EXISTING_XLSX and xlsx_tables:
  1403. matched_tables = [(idx, df, page) for idx, df, page in xlsx_tables]
  1404. else:
  1405. matched_tables = [(i, table.df, table.page) for i, table in enumerate(tables)]
  1406. print(f"✓ 未启用表头过滤,将处理所有 {len(matched_tables)} 个表格")
  1407. # 确定输出目录:如果启用了表头过滤,使用筛选后的目录,否则使用原始目录
  1408. if ENABLE_HEADER_FILTER:
  1409. final_output_dir = filtered_output_dir
  1410. print(f"\n开始保存筛选后的表格文件到: {final_output_dir}...\n")
  1411. else:
  1412. final_output_dir = output_dir
  1413. print(f"\n开始保存表格文件到: {final_output_dir}...\n")
  1414. # 创建输出目录
  1415. final_output_dir.mkdir(parents=True, exist_ok=True)
  1416. # 保存为Excel,使用tqdm显示进度
  1417. success_count = 0
  1418. error_count = 0
  1419. for idx, (orig_idx, table_df, page) in tqdm(enumerate(matched_tables), total=len(matched_tables),
  1420. desc="处理表格", unit="个", ncols=80):
  1421. # 使用简单的命名格式: table_1.xlsx, table_2.xlsx, ...
  1422. excel_path = final_output_dir / f"table_{idx+1}.xlsx"
  1423. try:
  1424. # 直接使用DataFrame的to_excel方法
  1425. table_df.to_excel(str(excel_path), index=False, header=False)
  1426. success_count += 1
  1427. except Exception as e:
  1428. tqdm.write(f"⚠ 表格 {idx+1} (原表格 {orig_idx}) Excel保存失败: {e}")
  1429. error_count += 1
  1430. total_time = time.time() - start_time
  1431. print("\n" + "=" * 60)
  1432. print("处理完成!")
  1433. print("=" * 60)
  1434. if ENABLE_HEADER_FILTER:
  1435. if FILTER_FROM_EXISTING_XLSX and xlsx_tables:
  1436. print(f"📊 原始表格总数: {len(xlsx_tables)}")
  1437. else:
  1438. print(f"📊 原始表格总数: {tables.n}")
  1439. print(f"✅ 匹配的表格数: {len(matched_tables)}")
  1440. print(f"✓ 成功保存: {success_count} 个表格")
  1441. if error_count > 0:
  1442. print(f"⚠ 保存失败: {error_count} 个表格")
  1443. print(f"📁 输出目录: {final_output_dir.absolute()}")
  1444. if ENABLE_HEADER_FILTER:
  1445. print(f"📁 原始表格目录: {output_dir.absolute()}")
  1446. print(f"⏱ 总耗时: {total_time:.2f}秒")
  1447. print("=" * 60)