dedup_reports.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. # -*- coding: utf-8 -*-
  2. """
  3. 去重重复报告:找出 姓名+生日+测评日期+报告类型 完全相同的记录,
  4. 保留格式规范的最新文件,删除旧格式的重复PDF。
  5. 用法: python scripts/reports/dedup_reports.py
  6. """
  7. import os, csv, re, shutil
  8. from collections import Counter
  9. PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  10. REPORT_DIR = os.path.join(PROJECT_DIR, '报告')
  11. CSV_PATH = os.path.join(PROJECT_DIR, '素材库', '测评数据_优化提取.csv')
  12. BACKUP_PATH = CSV_PATH.replace('.csv', '_before_dedup.csv')
  13. def is_proper_format(filename):
  14. """判断是否为规范格式的文件名(保留此类)。
  15. 规范格式:
  16. - 带时间戳: "B4 核心认知能力和成长型思维_姓名_202403231742254603.pdf"
  17. - 无时间戳: "B6_生涯规划(高中版)_姓名.pdf"
  18. - 核心特征: 文件名不包含 "DAN素质能力测评报告" 等旧格式标记,
  19. 且前缀类型与文件名其他内容之间不含旧命名痕迹
  20. 旧格式: "A1_DAN素质能力测评报告_姓名*.pdf",
  21. "A2_B6生涯规划(高中版)_姓名*.pdf",
  22. "B6_DAN素质能力测评报告-姓名*.pdf"
  23. """
  24. # 旧格式特征:包含 DAN素质能力测评报告 字样
  25. if 'DAN素质能力测评报告' in filename:
  26. return False
  27. # 旧格式特征:A2_B6 开头的旧生涯规划文件
  28. if filename.startswith('A2_B6'):
  29. return False
  30. # 规范格式特征:
  31. # 1. {类型}{空格}{中文描述}_{姓名}_{时间戳(14-18位)}.pdf
  32. if re.match(r'^[A-Z]\d\s+\S+?_\S+?_\d{14,}\.pdf$', filename):
  33. return True
  34. # 2. {类型}_{中文描述}_{姓名}.pdf(无时间戳,有中文描述字符≥3)
  35. if re.match(r'^[A-Z]\d_\S{3,}_.+\.pdf$', filename):
  36. return True
  37. return False
  38. def main():
  39. # 1. 读取CSV
  40. with open(CSV_PATH, 'r', encoding='utf-8-sig') as f:
  41. reader = csv.DictReader(f)
  42. rows = list(reader)
  43. fieldnames = reader.fieldnames
  44. print(f'总记录数: {len(rows)}')
  45. # 2. 按 姓名+生日+测评日期+报告类型 分组
  46. groups = {}
  47. for r in rows:
  48. k = (r.get('姓名', ''), r.get('生日', ''), r.get('测评日期', ''), r.get('报告类型', ''))
  49. groups.setdefault(k, []).append(r)
  50. # 3. 找出重复组
  51. dup_groups = {k: v for k, v in groups.items() if len(v) > 1}
  52. print(f'重复组数: {len(dup_groups)}')
  53. to_delete_files = []
  54. kept_rows = []
  55. for k, dup_rows in sorted(dup_groups.items(), key=lambda x: -len(x[1])):
  56. name, birthday, date, rtype = k
  57. print(f'\n组: {name} | {birthday} | {date} | {rtype} (x{len(dup_rows)})')
  58. # 按文件规范程度排序:规范格式优先保留
  59. # 如果多个规范格式,保留文件名排序最后的(时间戳最新的)
  60. proper = [r for r in dup_rows if is_proper_format(r.get('filename', ''))]
  61. old = [r for r in dup_rows if not is_proper_format(r.get('filename', ''))]
  62. if proper:
  63. # 保留规范格式中时间戳最新的
  64. proper.sort(key=lambda r: r.get('filename', ''), reverse=True)
  65. keep = proper[0]
  66. delete = proper[1:] + old
  67. else:
  68. # 没有规范格式,保留文件名排序最后的
  69. dup_rows.sort(key=lambda r: r.get('filename', ''), reverse=True)
  70. keep = dup_rows[0]
  71. delete = dup_rows[1:]
  72. print(f' 保留: {keep["filename"][:50]}')
  73. kept_rows.append(keep)
  74. for r in delete:
  75. fname = r['filename']
  76. fpath = os.path.join(REPORT_DIR, fname)
  77. print(f' 删除: {fname[:50]}')
  78. if os.path.exists(fpath):
  79. to_delete_files.append(fpath)
  80. # 4. 加入非重复的记录
  81. non_dup = [rows for k, rows in groups.items() if len(rows) == 1]
  82. for group in non_dup:
  83. kept_rows.extend(group)
  84. print(f'\n保留记录: {len(kept_rows)}')
  85. print(f'待删除文件: {len(to_delete_files)}')
  86. # 5. 备份原CSV
  87. shutil.copy2(CSV_PATH, BACKUP_PATH)
  88. print(f'备份CSV: {BACKUP_PATH}')
  89. # 6. 写入新CSV
  90. with open(CSV_PATH, 'w', encoding='utf-8-sig', newline='') as f:
  91. writer = csv.DictWriter(f, fieldnames=fieldnames)
  92. writer.writeheader()
  93. writer.writerows(kept_rows)
  94. print(f'写入新CSV: {CSV_PATH} ({len(kept_rows)} 记录)')
  95. # 7. 删除旧文件
  96. if to_delete_files:
  97. print(f'\n删除 {len(to_delete_files)} 个重复PDF...')
  98. for fpath in to_delete_files:
  99. try:
  100. os.remove(fpath)
  101. print(f' DELETED: {os.path.basename(fpath)}')
  102. except Exception as e:
  103. print(f' FAILED: {os.path.basename(fpath)} - {e}')
  104. print(f'\n完成!共去重 {sum(len(v) for v in dup_groups.values()) - len(dup_groups)} 条记录,删除 {len(to_delete_files)} 个文件')
  105. if __name__ == '__main__':
  106. main()