extract_dan_to_json.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. # -*- coding: utf-8 -*-
  2. """
  3. DAN测评报告PDF → JSON提取脚本
  4. 对标菌群报告的 extract_full_report_v5.py,将DAN测评报告(PDF)提取为结构化JSON。
  5. 8种报告类型: A1, A2, B2, B3, B4, B5, B6, C1
  6. 用法:
  7. python extract_dan_to_json.py <pdf_path> # 单文件模式 → <同名>.json
  8. python extract_dan_to_json.py # 批量模式 → 所有PDF输出JSON
  9. 依赖: extract_all_types.py, type_detector.py(同目录下)
  10. """
  11. import os
  12. import sys
  13. import json
  14. import csv
  15. from extract_all_types import (
  16. extract_all_data, extract_text_from_pdf,
  17. extract_name_from_filename, extract_birthday_from_pdf,
  18. extract_date_from_filename, extract_score_and_percentile,
  19. extract_a1_data, extract_a2_data, extract_b2_data,
  20. extract_b3_data, extract_b4_data, extract_b5_data,
  21. extract_b6_data, extract_c1_data,
  22. )
  23. from type_detector import detect_type
  24. # ==========================================
  25. # 维度定义:每个报告类型的指标分组
  26. # ==========================================
  27. TYPE_DIMENSIONS = {
  28. 'A1': {
  29. '报告类型': '儿童核心认知发展',
  30. '维度分组': {
  31. '核心认知': ['感知觉', '注意力', '记忆力', '推理能力', '空间能力', '加工速度'],
  32. }
  33. },
  34. 'A2': {
  35. '报告类型': '核心素养',
  36. '维度分组': {
  37. '认知维度': ['感知觉', '记忆力', '注意力'],
  38. '情绪状态': ['自卑-自信', '抑郁-安详', '焦虑-安详', '无力感-掌控感', '情绪总分'],
  39. '大五人格': ['开放性', '宜人性', '责任心', '外倾性', '神经质'],
  40. '社会关系': ['与母亲信任', '与父亲信任', '与同伴信任',
  41. '与母亲沟通', '与父亲沟通', '与同伴沟通',
  42. '与母亲亲近', '与父亲亲近', '与同伴亲近'],
  43. '身体健康': ['BMI', '身高', '体重', '睡眠_小时', '饮食_小时'],
  44. }
  45. },
  46. 'B2': {
  47. '报告类型': '儿童自我与家庭教养',
  48. '维度分组': {
  49. '自我概念': ['行为表现', '能力与学校', '躯体外貌', '情绪状态', '合群', '幸福与满足'],
  50. '儿童行为': ['品行问题', '情绪问题', '学习问题', '社交问题',
  51. '生活习惯', '多动倾向', '刻板行为', '拖延行为'],
  52. '家庭环境': ['亲密', '情感表达', '和谐', '独立性', '成就向导',
  53. '文化氛围', '娱乐活动', '道德观念', '家务安排', '家庭规则'],
  54. }
  55. },
  56. 'B3': {
  57. '报告类型': '核心学习能力',
  58. '维度分组': {
  59. '执行功能': ['抑制控制', '工作记忆', '认知灵活性'],
  60. '学习动机': ['深层动机', '表面动机', '自我效能感'],
  61. '学习策略': ['深层方法与策略', '表面方法与策略', '学习自我调节'],
  62. }
  63. },
  64. 'B4': {
  65. '报告类型': '核心认知能力+自我概念+自驱力+成长型思维',
  66. '维度分组': {
  67. '核心认知': ['感知觉', '注意力', '记忆力', '推理能力', '空间能力', '加工速度'],
  68. '自我概念': ['行为表现', '能力与学校', '躯体外貌', '情绪状态', '合群', '幸福与满足'],
  69. '自驱力': ['自主性', '胜任感', '归属感'],
  70. '成长型思维': ['成长型思维'],
  71. }
  72. },
  73. 'B5': {
  74. '报告类型': '青春期挑战',
  75. '维度分组': {
  76. '情绪调节': ['认知重评', '表达抑制'],
  77. '学业压力': ['学业负担', '家庭期望', '师生关系', '自我期望', '同伴竞争'],
  78. '人际关系': ['母亲关系', '父亲关系', '师生关系', '同伴关系'],
  79. '社交能力': ['主动交往', '情感支持', '情感表达', '表达影响', '冲突解决'],
  80. '睡眠': ['时长', '效率', '主观质量', '日间功能', '催眠药物', '障碍'],
  81. '运动': ['久坐', '步行', '中等强度', '高强度'],
  82. '网络依赖': ['总分', '强迫使用', '时间管理', '戒断反应'],
  83. }
  84. },
  85. 'B6': {
  86. '报告类型': '职业发展',
  87. '维度分组': {
  88. '职业兴趣': ['艺术型', '社会型', '事业型', '常规型', '现实型', '研究型'],
  89. '多元智能': ['内省', '空间', '音乐', '人际关系', '自然', '身体运动', '语言', '逻辑数学'],
  90. '职业价值观': ['职业价值观'],
  91. }
  92. },
  93. 'C1': {
  94. '报告类型': '校园版综合',
  95. '维度分组': {
  96. '核心认知': ['感知觉', '注意力', '记忆力', '推理能力', '空间能力', '加工速度'],
  97. '大五人格': ['开放性', '宜人性', '责任心', '外倾性', '神经质'],
  98. '自驱力': ['自主性', '胜任感', '归属感'],
  99. '自我概念': ['行为表现', '能力与学校', '躯体外貌', '情绪状态', '合群', '幸福与满足'],
  100. }
  101. },
  102. }
  103. # ==========================================
  104. # 平铺行数据 → 嵌套JSON
  105. # ==========================================
  106. def flat_row_to_nested(row):
  107. """将平铺的CSV行数据转换为按类型分组的嵌套JSON"""
  108. result = {
  109. '文件名': row.get('filename', ''),
  110. '姓名': row.get('姓名', ''),
  111. '生日': row.get('生日', ''),
  112. '报告类型': row.get('报告类型', ''),
  113. '测评日期': row.get('测评日期', ''),
  114. '总分': row.get('总分', ''),
  115. '百分位': row.get('百分位', ''),
  116. }
  117. report_type = row.get('报告类型', '')
  118. type_info = TYPE_DIMENSIONS.get(report_type)
  119. if not type_info:
  120. return result
  121. result['报告类型名称'] = type_info['报告类型']
  122. result['维度数据'] = {}
  123. for group_name, dims in type_info['维度分组'].items():
  124. group_data = {}
  125. for dim in dims:
  126. # 尝试多种key匹配
  127. val = _find_value(row, dim)
  128. if val:
  129. group_data[dim] = val
  130. if group_data:
  131. result['维度数据'][group_name] = group_data
  132. # B4/B5的特殊处理:B4成长型思维是连续分数,B5有人际指南针子维度
  133. if report_type == 'B4' and row.get('成长型思维'):
  134. result['维度数据'].setdefault('成长型思维', {})['连续分数'] = row['成长型思维']
  135. if report_type == 'B5':
  136. # 收集19个指南针子维度
  137. compass_dims = {}
  138. for key, val in row.items():
  139. if key.startswith('人际_') and '_' in key and key.count('_') >= 2:
  140. compass_dims[key] = val
  141. if compass_dims:
  142. result['维度数据']['人际指南针'] = compass_dims
  143. return result
  144. def _find_value(row, dim):
  145. """从平铺行中查找维度值,尝试各种key命名规则"""
  146. # 直接匹配
  147. if row.get(dim):
  148. return row[dim]
  149. # 后缀匹配 (如 '感知觉_pct', '行为表现_score')
  150. for suffix in ['_pct', '_score', '_小时', '_min', '_频率', '_时长', '_效率', '%']:
  151. key = f'{dim}{suffix}'
  152. if row.get(key):
  153. val = row[key]
  154. return val
  155. # B2: '自我概念_行为表现', '行为_品行问题', '家庭_亲密'
  156. for prefix in ['自我概念_', '行为_', '家庭_', '学业压力_', '人际_', '社交_',
  157. '睡眠_', '运动_', '网络依赖_', '兴趣_', '能力_',
  158. '与母亲', '与父亲', '与同伴']:
  159. for key in row:
  160. if key.startswith(prefix) and dim in key:
  161. return row[key]
  162. # 反向匹配:row中的key包含dim(如 key='感知觉_pct', dim='感知觉')
  163. for key in row:
  164. if key.startswith(dim) or key.endswith(dim):
  165. return row[key]
  166. return ''
  167. # ==========================================
  168. # 单PDF → JSON
  169. # ==========================================
  170. def extract_dan_to_json(pdf_path):
  171. """输入单个PDF文件路径,输出JSON格式的结构化数据"""
  172. filename = os.path.basename(pdf_path)
  173. row = extract_all_data(pdf_path, filename)
  174. result = flat_row_to_nested(row)
  175. # 补充原始文本(调试用)
  176. text = extract_text_from_pdf(pdf_path)
  177. result['_raw_text_preview'] = text[:500] if text else ''
  178. return result
  179. # ==========================================
  180. # 批量模式
  181. # ==========================================
  182. def batch_process():
  183. """处理当前目录下所有PDF,输出JSON + CSV汇总"""
  184. script_dir = os.path.dirname(os.path.abspath(__file__))
  185. pdf_files = sorted([
  186. f for f in os.listdir(script_dir)
  187. if f.endswith('.pdf')
  188. ])
  189. if not pdf_files:
  190. print('未找到PDF文件')
  191. return
  192. all_results = []
  193. for pdf_file in pdf_files:
  194. pdf_path = os.path.join(script_dir, pdf_file)
  195. label = pdf_file.replace('.pdf', '')
  196. try:
  197. result = extract_dan_to_json(pdf_path)
  198. json_path = os.path.join(script_dir, f'{label}.json')
  199. with open(json_path, 'w', encoding='utf-8') as f:
  200. json.dump(result, f, ensure_ascii=False, indent=2)
  201. print(f' ✓ {pdf_file} → {label}.json')
  202. all_results.append(result)
  203. except Exception as e:
  204. print(f' ✗ {pdf_file}: ERROR - {e}')
  205. # CSV汇总
  206. if all_results:
  207. csv_path = os.path.join(script_dir, 'dan测评汇总.csv')
  208. # 收集所有key
  209. all_keys = set()
  210. flat_rows = []
  211. for r in all_results:
  212. flat = _flatten_json(r)
  213. flat_rows.append(flat)
  214. all_keys.update(flat.keys())
  215. with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f:
  216. sorted_keys = sorted(all_keys)
  217. w = csv.DictWriter(f, fieldnames=sorted_keys)
  218. w.writeheader()
  219. w.writerows(flat_rows)
  220. print(f'\nCSV汇总: {csv_path} ({len(all_results)} 条)')
  221. def _flatten_json(nested):
  222. """将嵌套JSON展平为扁平行"""
  223. flat = {}
  224. flat['文件名'] = nested.get('文件名', '')
  225. flat['姓名'] = nested.get('姓名', '')
  226. flat['生日'] = nested.get('生日', '')
  227. flat['报告类型'] = nested.get('报告类型', '')
  228. flat['报告类型名称'] = nested.get('报告类型名称', '')
  229. flat['测评日期'] = nested.get('测评日期', '')
  230. flat['总分'] = nested.get('总分', '')
  231. flat['百分位'] = nested.get('百分位', '')
  232. dims = nested.get('维度数据', {})
  233. for group_name, group_data in dims.items():
  234. for dim_name, dim_val in group_data.items():
  235. flat[f'{group_name}_{dim_name}'] = dim_val
  236. # 补充B5指南针
  237. if '人际指南针' in dims:
  238. for key, val in dims['人际指南针'].items():
  239. flat[key] = val
  240. return flat
  241. # ==========================================
  242. # 主入口
  243. # ==========================================
  244. def main():
  245. if len(sys.argv) >= 2:
  246. pdf_path = sys.argv[1]
  247. if not os.path.isfile(pdf_path):
  248. print(f'错误:找不到文件 {pdf_path}')
  249. sys.exit(1)
  250. result = extract_dan_to_json(pdf_path)
  251. base, _ = os.path.splitext(pdf_path)
  252. json_path = base + '.json'
  253. with open(json_path, 'w', encoding='utf-8') as f:
  254. json.dump(result, f, ensure_ascii=False, indent=2)
  255. print(f'已生成: {json_path}')
  256. print(f' 姓名: {result.get("姓名", "")}')
  257. print(f' 类型: {result.get("报告类型", "")} - {result.get("报告类型名称", "")}')
  258. print(f' 维度: {len(result.get("维度数据", {}))} 组')
  259. else:
  260. batch_process()
  261. if __name__ == '__main__':
  262. main()