table_comparator.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. import re
  2. from typing import Dict, List
  3. # ✅ 兼容相对导入和绝对导入
  4. try:
  5. from .data_type_detector import DataTypeDetector
  6. from .similarity_calculator import SimilarityCalculator
  7. from .text_processor import TextProcessor
  8. except ImportError:
  9. from data_type_detector import DataTypeDetector
  10. from similarity_calculator import SimilarityCalculator
  11. from text_processor import TextProcessor
  12. class TableComparator:
  13. """表格数据比较"""
  14. def __init__(self):
  15. self.detector = DataTypeDetector()
  16. self.calculator = SimilarityCalculator()
  17. self.text_processor = TextProcessor()
  18. self.header_similarity_threshold = 90
  19. self.content_similarity_threshold = 95
  20. self.max_paragraph_window = 6
  21. def normalize_header_text(self, text: str) -> str:
  22. """标准化表头文本"""
  23. text = re.sub(r'[((].*?[))]', '', text)
  24. text = re.sub(r'\s+', '', text)
  25. text = re.sub(r'[^\w\u4e00-\u9fff]', '', text)
  26. return text.lower().strip()
  27. def compare_table_headers(self, headers1: List[str], headers2: List[str]) -> Dict:
  28. """比较表格表头"""
  29. result = {
  30. 'match': True,
  31. 'differences': [],
  32. 'column_mapping': {},
  33. 'similarity_scores': []
  34. }
  35. if len(headers1) != len(headers2):
  36. result['match'] = False
  37. result['differences'].append({
  38. 'type': 'table_header_critical',
  39. 'description': f'表头列数不一致: {len(headers1)} vs {len(headers2)}',
  40. 'severity': 'critical'
  41. })
  42. return result
  43. for i, (h1, h2) in enumerate(zip(headers1, headers2)):
  44. norm_h1 = self.normalize_header_text(h1)
  45. norm_h2 = self.normalize_header_text(h2)
  46. similarity = self.calculator.calculate_text_similarity(norm_h1, norm_h2)
  47. result['similarity_scores'].append({
  48. 'column_index': i,
  49. 'header1': h1,
  50. 'header2': h2,
  51. 'similarity': similarity
  52. })
  53. if similarity < self.header_similarity_threshold:
  54. result['match'] = False
  55. result['differences'].append({
  56. 'type': 'table_header_mismatch',
  57. 'column_index': i,
  58. 'header1': h1,
  59. 'header2': h2,
  60. 'similarity': similarity,
  61. 'description': f'第{i+1}列表头不匹配: "{h1}" vs "{h2}" (相似度: {similarity:.1f}%)',
  62. 'severity': 'medium' if similarity < 50 else 'high'
  63. })
  64. else:
  65. result['column_mapping'][i] = i
  66. return result
  67. def detect_table_header_row(self, table: List[List[str]]) -> int:
  68. """智能检测表格的表头行索引"""
  69. header_keywords = [
  70. '序号', '编号', '时间', '日期', '名称', '类型', '金额', '数量', '单价',
  71. '备注', '说明', '状态', '类别', '方式', '账号', '单号', '订单',
  72. '交易单号', '交易时间', '交易类型', '收/支', '支出', '收入',
  73. '交易方式', '交易对方', '商户单号', '付款方式', '收款方',
  74. 'no', 'id', 'time', 'date', 'name', 'type', 'amount', 'status'
  75. ]
  76. for row_idx, row in enumerate(table):
  77. if not row:
  78. continue
  79. keyword_count = 0
  80. for cell in row:
  81. cell_lower = cell.lower().strip()
  82. for keyword in header_keywords:
  83. if keyword in cell_lower:
  84. keyword_count += 1
  85. break
  86. if keyword_count >= len(row) * 0.4 and keyword_count >= 2:
  87. if row_idx + 1 < len(table):
  88. next_row = table[row_idx + 1]
  89. if self._is_data_row(next_row):
  90. print(f" 📍 检测到表头在第 {row_idx + 1} 行")
  91. return row_idx
  92. print(f" ⚠️ 未检测到明确表头,默认使用第1行")
  93. return 0
  94. def _is_data_row(self, row: List[str]) -> bool:
  95. """判断是否为数据行"""
  96. data_pattern_count = 0
  97. for cell in row:
  98. if not cell:
  99. continue
  100. if re.search(r'\d', cell):
  101. data_pattern_count += 1
  102. if re.search(r'\d{4}[-/年]\d{1,2}[-/月]\d{1,2}', cell):
  103. data_pattern_count += 1
  104. return data_pattern_count >= len(row) * 0.5
  105. def compare_cell_value(self, value1: str, value2: str, column_type: str,
  106. column_name: str = '') -> Dict:
  107. """比较单元格值"""
  108. result = {
  109. 'match': True,
  110. 'difference': None
  111. }
  112. v1 = self.text_processor.normalize_text(value1)
  113. v2 = self.text_processor.normalize_text(value2)
  114. if v1 == v2:
  115. return result
  116. if column_type == 'text_number':
  117. norm_v1 = self.detector.normalize_text_number(v1)
  118. norm_v2 = self.detector.normalize_text_number(v2)
  119. if norm_v1 == norm_v2:
  120. result['match'] = False
  121. result['difference'] = {
  122. 'type': 'table_text',
  123. 'value1': value1,
  124. 'value2': value2,
  125. 'description': f'文本型数字格式差异: "{value1}" vs "{value2}" (内容相同,空格不同)',
  126. 'severity': 'low'
  127. }
  128. else:
  129. result['match'] = False
  130. result['difference'] = {
  131. 'type': 'table_text',
  132. 'value1': value1,
  133. 'value2': value2,
  134. 'description': f'文本型数字不一致: {value1} vs {value2}',
  135. 'severity': 'high'
  136. }
  137. return result
  138. if column_type == 'numeric':
  139. if self.detector.is_numeric(v1) and self.detector.is_numeric(v2):
  140. num1 = self.detector.parse_number(v1)
  141. num2 = self.detector.parse_number(v2)
  142. if abs(num1 - num2) > 0.01:
  143. result['match'] = False
  144. result['difference'] = {
  145. 'type': 'table_amount',
  146. 'value1': value1,
  147. 'value2': value2,
  148. 'diff_amount': abs(num1 - num2),
  149. 'description': f'金额不一致: {value1} vs {value2}'
  150. }
  151. else:
  152. result['match'] = False
  153. result['difference'] = {
  154. 'type': 'table_text',
  155. 'value1': value1,
  156. 'value2': value2,
  157. 'description': f'长数字字符串不一致: {value1} vs {value2}'
  158. }
  159. elif column_type == 'datetime':
  160. datetime1 = self.detector.extract_datetime(v1)
  161. datetime2 = self.detector.extract_datetime(v2)
  162. if datetime1 != datetime2:
  163. result['match'] = False
  164. result['difference'] = {
  165. 'type': 'table_datetime',
  166. 'value1': value1,
  167. 'value2': value2,
  168. 'description': f'日期时间不一致: {value1} vs {value2}'
  169. }
  170. else:
  171. similarity = self.calculator.calculate_text_similarity(v1, v2)
  172. if similarity < self.content_similarity_threshold:
  173. result['match'] = False
  174. result['difference'] = {
  175. 'type': 'table_text',
  176. 'value1': value1,
  177. 'value2': value2,
  178. 'similarity': similarity,
  179. 'description': f'文本不一致: {value1} vs {value2} (相似度: {similarity:.1f}%)'
  180. }
  181. return result
  182. def compare_tables(self, table1: List[List[str]], table2: List[List[str]]) -> List[Dict]:
  183. """标准表格比较"""
  184. differences = []
  185. max_rows = max(len(table1), len(table2))
  186. for i in range(max_rows):
  187. row1 = table1[i] if i < len(table1) else []
  188. row2 = table2[i] if i < len(table2) else []
  189. max_cols = max(len(row1), len(row2))
  190. for j in range(max_cols):
  191. cell1 = row1[j] if j < len(row1) else ""
  192. cell2 = row2[j] if j < len(row2) else ""
  193. if "[图片内容-忽略]" in cell1 or "[图片内容-忽略]" in cell2:
  194. continue
  195. if cell1 != cell2:
  196. if self.detector.is_numeric(cell1) and self.detector.is_numeric(cell2):
  197. num1 = self.detector.parse_number(cell1)
  198. num2 = self.detector.parse_number(cell2)
  199. if abs(num1 - num2) > 0.001:
  200. differences.append({
  201. 'type': 'table_amount',
  202. 'position': f'行{i+1}列{j+1}',
  203. 'file1_value': cell1,
  204. 'file2_value': cell2,
  205. 'description': f'金额不一致: {cell1} vs {cell2}',
  206. 'row_index': i,
  207. 'col_index': j
  208. })
  209. else:
  210. differences.append({
  211. 'type': 'table_text',
  212. 'position': f'行{i+1}列{j+1}',
  213. 'file1_value': cell1,
  214. 'file2_value': cell2,
  215. 'description': f'文本不一致: {cell1} vs {cell2}',
  216. 'row_index': i,
  217. 'col_index': j
  218. })
  219. return differences
  220. def compare_table_flow_list(self, table1: List[List[str]], table2: List[List[str]]) -> List[Dict]:
  221. """流水列表表格比较算法"""
  222. differences = []
  223. if not table1 or not table2:
  224. return [{
  225. 'type': 'table_empty',
  226. 'description': '表格为空',
  227. 'severity': 'critical'
  228. }]
  229. print(f"\n📋 开始流水表格对比...")
  230. # 检测表头位置
  231. header_row_idx1 = self.detect_table_header_row(table1)
  232. header_row_idx2 = self.detect_table_header_row(table2)
  233. if header_row_idx1 != header_row_idx2:
  234. differences.append({
  235. 'type': 'table_header_position',
  236. 'position': '表头位置',
  237. 'file1_value': f'第{header_row_idx1 + 1}行',
  238. 'file2_value': f'第{header_row_idx2 + 1}行',
  239. 'description': f'表头位置不一致: 文件1在第{header_row_idx1 + 1}行,文件2在第{header_row_idx2 + 1}行',
  240. 'severity': 'high'
  241. })
  242. # 比对表头前的内容
  243. if header_row_idx1 > 0 or header_row_idx2 > 0:
  244. print(f"\n📝 对比表头前的内容...")
  245. pre_header_table1 = table1[:header_row_idx1] if header_row_idx1 > 0 else []
  246. pre_header_table2 = table2[:header_row_idx2] if header_row_idx2 > 0 else []
  247. if pre_header_table1 or pre_header_table2:
  248. pre_header_diffs = self.compare_tables(pre_header_table1, pre_header_table2)
  249. for diff in pre_header_diffs:
  250. diff['type'] = 'table_pre_header'
  251. diff['position'] = f"表头前{diff['position']}"
  252. diff['severity'] = 'medium'
  253. differences.extend(pre_header_diffs)
  254. # 比较表头
  255. headers1 = table1[header_row_idx1]
  256. headers2 = table2[header_row_idx2]
  257. print(f"\n📋 对比表头...")
  258. header_result = self.compare_table_headers(headers1, headers2)
  259. if not header_result['match']:
  260. print(f"\n⚠️ 表头文字存在差异")
  261. for diff in header_result['differences']:
  262. differences.append({
  263. 'type': diff.get('type', 'table_header_mismatch'),
  264. 'position': '表头',
  265. 'file1_value': diff.get('header1', ''),
  266. 'file2_value': diff.get('header2', ''),
  267. 'description': diff['description'],
  268. 'severity': diff.get('severity', 'high'),
  269. })
  270. if diff.get('severity') == 'critical':
  271. return differences
  272. # 检测列类型并比较数据行
  273. column_types1 = self._detect_column_types(table1, header_row_idx1, headers1)
  274. column_types2 = self._detect_column_types(table2, header_row_idx2, headers2)
  275. # 处理列类型不匹配
  276. mismatched_columns = self._check_column_type_mismatch(
  277. column_types1, column_types2, headers1, headers2, differences
  278. )
  279. # 合并列类型
  280. column_types = self._merge_column_types(column_types1, column_types2, mismatched_columns)
  281. # 逐行比较数据
  282. data_diffs = self._compare_data_rows(
  283. table1, table2, header_row_idx1, header_row_idx2,
  284. headers1, column_types, mismatched_columns, header_result['match']
  285. )
  286. differences.extend(data_diffs)
  287. print(f"\n✅ 流水表格对比完成,发现 {len(differences)} 个差异")
  288. return differences
  289. def _detect_column_types(self, table: List[List[str]], header_row_idx: int,
  290. headers: List[str]) -> List[str]:
  291. """检测列类型"""
  292. column_types = []
  293. for col_idx in range(len(headers)):
  294. col_values = [
  295. row[col_idx]
  296. for row in table[header_row_idx + 1:]
  297. if col_idx < len(row)
  298. ]
  299. col_type = self.detector.detect_column_type(col_values)
  300. column_types.append(col_type)
  301. return column_types
  302. def _check_column_type_mismatch(self, column_types1: List[str], column_types2: List[str],
  303. headers1: List[str], headers2: List[str],
  304. differences: List[Dict]) -> List[int]:
  305. """检查列类型不匹配"""
  306. mismatched_columns = []
  307. for col_idx in range(min(len(column_types1), len(column_types2))):
  308. if column_types1[col_idx] != column_types2[col_idx]:
  309. mismatched_columns.append(col_idx)
  310. differences.append({
  311. 'type': 'table_column_type_mismatch',
  312. 'position': f'第{col_idx + 1}列',
  313. 'file1_value': f'{headers1[col_idx]} ({column_types1[col_idx]})',
  314. 'file2_value': f'{headers2[col_idx]} ({column_types2[col_idx]})',
  315. 'description': f'列类型不一致: {column_types1[col_idx]} vs {column_types2[col_idx]}',
  316. 'severity': 'high',
  317. 'column_index': col_idx
  318. })
  319. total_columns = min(len(column_types1), len(column_types2))
  320. mismatch_ratio = len(mismatched_columns) / total_columns if total_columns > 0 else 0
  321. if mismatch_ratio > 0.5:
  322. differences.append({
  323. 'type': 'table_header_critical',
  324. 'position': '表格列类型',
  325. 'file1_value': f'{len(mismatched_columns)}列类型不一致',
  326. 'file2_value': f'共{total_columns}列',
  327. 'description': f'列类型差异过大: {len(mismatched_columns)}/{total_columns}列不匹配 ({mismatch_ratio:.1%})',
  328. 'severity': 'critical'
  329. })
  330. return mismatched_columns
  331. def _merge_column_types(self, column_types1: List[str], column_types2: List[str],
  332. mismatched_columns: List[int]) -> List[str]:
  333. """合并列类型"""
  334. column_types = []
  335. for col_idx in range(max(len(column_types1), len(column_types2))):
  336. if col_idx >= len(column_types1):
  337. column_types.append(column_types2[col_idx])
  338. elif col_idx >= len(column_types2):
  339. column_types.append(column_types1[col_idx])
  340. elif col_idx in mismatched_columns:
  341. type1 = column_types1[col_idx]
  342. type2 = column_types2[col_idx]
  343. if type1 == 'text' or type2 == 'text':
  344. column_types.append('text')
  345. elif type1 == 'text_number' or type2 == 'text_number':
  346. column_types.append('text_number')
  347. else:
  348. column_types.append(type1)
  349. else:
  350. column_types.append(column_types1[col_idx])
  351. return column_types
  352. def _compare_data_rows(self, table1: List[List[str]], table2: List[List[str]],
  353. header_row_idx1: int, header_row_idx2: int,
  354. headers1: List[str], column_types: List[str],
  355. mismatched_columns: List[int], header_match: bool) -> List[Dict]:
  356. """逐行比较数据"""
  357. differences = []
  358. data_rows1 = table1[header_row_idx1 + 1:]
  359. data_rows2 = table2[header_row_idx2 + 1:]
  360. max_rows = max(len(data_rows1), len(data_rows2))
  361. for row_idx in range(max_rows):
  362. row1 = data_rows1[row_idx] if row_idx < len(data_rows1) else []
  363. row2 = data_rows2[row_idx] if row_idx < len(data_rows2) else []
  364. actual_row_num = header_row_idx1 + row_idx + 2
  365. if not row1:
  366. differences.append({
  367. 'type': 'table_row_missing',
  368. 'position': f'第{actual_row_num}行',
  369. 'file1_value': '',
  370. 'file2_value': ', '.join(row2),
  371. 'description': f'文件1缺少第{actual_row_num}行',
  372. 'severity': 'high',
  373. 'row_index': actual_row_num
  374. })
  375. continue
  376. if not row2:
  377. differences.append({
  378. 'type': 'table_row_missing',
  379. 'position': f'第{actual_row_num}行',
  380. 'file1_value': ', '.join(row1),
  381. 'file2_value': '',
  382. 'description': f'文件2缺少第{actual_row_num}行',
  383. 'severity': 'high',
  384. 'row_index': actual_row_num
  385. })
  386. continue
  387. # 逐列比较
  388. max_cols = max(len(row1), len(row2))
  389. for col_idx in range(max_cols):
  390. cell1 = row1[col_idx] if col_idx < len(row1) else ''
  391. cell2 = row2[col_idx] if col_idx < len(row2) else ''
  392. if "[图片内容-忽略]" in cell1 or "[图片内容-忽略]" in cell2:
  393. continue
  394. column_type = column_types[col_idx] if col_idx < len(column_types) else 'text'
  395. column_name = headers1[col_idx] if col_idx < len(headers1) else f'列{col_idx + 1}'
  396. compare_result = self.compare_cell_value(cell1, cell2, column_type, column_name)
  397. if not compare_result['match']:
  398. diff_info = compare_result['difference']
  399. type_mismatch_note = ""
  400. if col_idx in mismatched_columns:
  401. type_mismatch_note = " [列类型冲突]"
  402. differences.append({
  403. 'type': diff_info['type'],
  404. 'position': f'第{actual_row_num}行第{col_idx + 1}列',
  405. 'file1_value': diff_info['value1'],
  406. 'file2_value': diff_info['value2'],
  407. 'description': diff_info['description'] + type_mismatch_note,
  408. 'severity': 'high' if col_idx in mismatched_columns else 'medium',
  409. 'row_index': actual_row_num,
  410. 'col_index': col_idx,
  411. 'column_name': column_name,
  412. 'column_type': column_type,
  413. 'column_type_mismatch': col_idx in mismatched_columns,
  414. })
  415. return differences