build_fault_package.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import csv
  2. import json
  3. import os
  4. import subprocess
  5. import sys
  6. from datetime import datetime, timedelta
  7. import pymysql
  8. from docx import Document
  9. from docx.shared import Inches
  10. sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "backend"))
  11. from app.db import get_connection # noqa: E402
  12. from app.services.data_service import DataService, PHASES # noqa: E402
  13. ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "故障分析包")
  14. ECHARTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend", "node_modules", "echarts", "dist", "echarts.min.js")
  15. CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
  16. SIDES = ("压力盖侧", "压力轴侧")
  17. DATASETS = [
  18. {
  19. "key": "6号机_20260422_气阀故障_一级3缸",
  20. "unit": "6",
  21. "device_code": "6#",
  22. "device_part": "6号机组三缸",
  23. "cylinder": "三缸(一级3缸)",
  24. "event_date": "2026-04-22",
  25. "fault_desc": "6#机一级3缸排气温度高",
  26. "pks_item": "YSJ6_22",
  27. "pks_col": "YSJ_22",
  28. "pks_desc": "6#压缩机一级3#缸排气温度",
  29. "thr_high": 115.0,
  30. "thr_highhigh": 125.0,
  31. "raw_dir": r"E:\RAW\exp_6#_2026.1.1-2026.7.31_20260101000000_20260731235959",
  32. "segments": {
  33. "低": ("2026-04-12 13:44:00", "2026-04-12 15:31:00"),
  34. "高": ("2026-04-20 11:59:00", "2026-04-20 13:46:00"),
  35. "修复后": ("2026-04-25 09:59:00", "2026-04-25 11:46:00"),
  36. },
  37. },
  38. {
  39. "key": "4号机_20260610_气阀故障_一级2缸",
  40. "unit": "4",
  41. "device_code": "4#",
  42. "device_part": "4号机组二缸",
  43. "cylinder": "二缸(一级2缸)",
  44. "event_date": "2026-06-10",
  45. "fault_desc": "4#压缩机一级2#3#缸温度异常",
  46. "pks_item": "YSJ4_10",
  47. "pks_col": "YSJ_10",
  48. "pks_desc": "4#压缩机一级2#缸排气温度",
  49. "thr_high": 130.0,
  50. "thr_highhigh": 140.0,
  51. "raw_dir": r"E:\RAW\exp_4#_2026.1.1-2026.7.31_20260101000000_20260731235959",
  52. "segments": {
  53. "低": ("2026-06-08 01:59:00", "2026-06-08 03:46:00"),
  54. "高": ("2026-06-10 05:59:00", "2026-06-10 07:46:00"),
  55. "修复后": ("2026-06-11 18:59:00", "2026-06-11 20:46:00"),
  56. },
  57. },
  58. {
  59. "key": "4号机_20260610_气阀故障_一级3缸",
  60. "unit": "4",
  61. "device_code": "4#",
  62. "device_part": "4号机组三缸",
  63. "cylinder": "三缸(一级3缸)",
  64. "event_date": "2026-06-10",
  65. "fault_desc": "4#压缩机一级2#3#缸温度异常",
  66. "pks_item": "YSJ4_11",
  67. "pks_col": "YSJ_11",
  68. "pks_desc": "4#压缩机一级3#缸排气温度",
  69. "thr_high": 130.0,
  70. "thr_highhigh": 140.0,
  71. "raw_dir": r"E:\RAW\exp_4#_2026.1.1-2026.7.31_20260101000000_20260731235959",
  72. "segments": {
  73. "低": ("2026-06-08 01:59:00", "2026-06-08 03:46:00"),
  74. "高": ("2026-06-10 05:59:00", "2026-06-10 07:46:00"),
  75. "修复后": ("2026-06-11 18:59:00", "2026-06-11 20:46:00"),
  76. },
  77. },
  78. ]
  79. SVC = DataService()
  80. COPY_LIST = []
  81. def ensure_dirs(ds):
  82. base = os.path.join(ROOT, ds["key"])
  83. subs = {name: os.path.join(base, name) for name in
  84. ("01_原始首周期数据", "02_PKS温度数据", "03_功图", "04_曲线图")}
  85. for path in subs.values():
  86. os.makedirs(path, exist_ok=True)
  87. return base, subs
  88. def query_wave_files(device_part, side, start, end):
  89. with get_connection() as connection:
  90. with connection.cursor() as cursor:
  91. cursor.execute(
  92. "SELECT id, sample_time, file_name, rpm, cycle_start, cycle_end "
  93. "FROM wave_file WHERE device_part=%s AND device_point=%s "
  94. "AND sample_time>=%s AND sample_time<%s ORDER BY sample_time LIMIT 8",
  95. (device_part, side, start, end),
  96. )
  97. return cursor.fetchall()
  98. def query_pks(device_code, column, start, end):
  99. with get_connection() as connection:
  100. with connection.cursor() as cursor:
  101. cursor.execute(
  102. f"SELECT sample_time, `{column}` AS value FROM pks_long_sample "
  103. "WHERE device_code=%s AND sample_time>=%s AND sample_time<%s ORDER BY sample_time",
  104. (device_code, start, end),
  105. )
  106. return cursor.fetchall()
  107. def write_first_cycle_csv(path, samples):
  108. with open(path, "w", newline="", encoding="utf-8-sig") as handle:
  109. writer = csv.writer(handle)
  110. writer.writerow(["sample_index", "signal_value", "second_value"])
  111. for row in samples:
  112. writer.writerow([int(row[0]), float(row[1]), "" if row[2] != row[2] else float(row[2])])
  113. def write_pks_csv(path, rows):
  114. with open(path, "w", newline="", encoding="utf-8-sig") as handle:
  115. writer = csv.writer(handle)
  116. writer.writerow(["sample_time", "value"])
  117. for row in rows:
  118. writer.writerow([row["sample_time"].strftime("%Y-%m-%d %H:%M:%S"), row["value"]])
  119. def screenshot(html_path, png_path):
  120. subprocess.run(
  121. [CHROME, "--headless=new", "--disable-gpu", "--hide-scrollbars",
  122. "--window-size=1180,660", "--force-device-scale-factor=2",
  123. "--virtual-time-budget=2000", f"--screenshot={png_path}",
  124. "file://" + html_path],
  125. check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
  126. )
  127. def render_html(html_path, option, title):
  128. html = f"""<!doctype html><html><head><meta charset="utf-8"><title>{title}</title>
  129. <style>html,body{{margin:0;background:#fff}}#c{{width:1160px;height:620px}}</style>
  130. <script src="../../echarts.min.js"></script></head>
  131. <body><div id="c"></div><script>var chart=echarts.init(document.getElementById('c'));
  132. chart.setOption({json.dumps(option, ensure_ascii=False)});</script></body></html>"""
  133. with open(html_path, "w", encoding="utf-8") as handle:
  134. handle.write(html)
  135. def pv_option(detail, side, title):
  136. volume = detail["volume"]
  137. series = []
  138. for name, color, start, end in PHASES:
  139. last = name == PHASES[-1][0]
  140. data = []
  141. for index, angle in enumerate(detail["angles360"]):
  142. x = volume[index]
  143. if x is None:
  144. continue
  145. in_phase = angle >= start if last else (start <= angle < end)
  146. if in_phase:
  147. data.append([x, detail["pressure"][index]])
  148. series.append({"name": name, "type": "line", "showSymbol": False,
  149. "lineStyle": {"color": color, "width": 2}, "itemStyle": {"color": color}, "data": data})
  150. return {
  151. "title": {"text": title, "left": "center", "textStyle": {"fontSize": 14}},
  152. "animation": False,
  153. "grid": {"left": 70, "right": 40, "top": 50, "bottom": 60},
  154. "legend": {"top": 22, "textStyle": {"fontSize": 12}},
  155. "tooltip": {"trigger": "axis"},
  156. "xAxis": {"type": "value", "name": "气缸体积 (L)", "nameLocation": "middle", "nameGap": 30},
  157. "yAxis": {"type": "value", "name": "压力原始值", "nameLocation": "middle", "nameGap": 50},
  158. "series": series,
  159. }
  160. def ptheta_option(detail, side, title):
  161. series = []
  162. for name, color, start, end in PHASES:
  163. last = name == PHASES[-1][0]
  164. data = []
  165. for index, angle360 in enumerate(detail["angles360"]):
  166. in_phase = angle360 >= start if last else (start <= angle360 < end)
  167. if in_phase:
  168. data.append([detail["angles"][index], detail["pressure"][index]])
  169. series.append({"name": name, "type": "line", "showSymbol": False,
  170. "lineStyle": {"color": color, "width": 2}, "itemStyle": {"color": color}, "data": data})
  171. return {
  172. "title": {"text": title, "left": "center", "textStyle": {"fontSize": 14}},
  173. "animation": False,
  174. "grid": {"left": 70, "right": 40, "top": 50, "bottom": 60},
  175. "legend": {"top": 22, "textStyle": {"fontSize": 12}},
  176. "tooltip": {"trigger": "axis"},
  177. "xAxis": {"type": "value", "name": "曲轴角度 (°)", "nameLocation": "middle", "nameGap": 30, "min": 0, "max": 180},
  178. "yAxis": {"type": "value", "name": "压力原始值", "nameLocation": "middle", "nameGap": 50},
  179. "series": series,
  180. }
  181. def pressure_overlay_option(series_map, title):
  182. series = []
  183. colors = {"压力盖侧": "#e05252", "压力轴侧": "#287f9e"}
  184. for side, values in series_map.items():
  185. series.append({"name": side, "type": "line", "showSymbol": False,
  186. "lineStyle": {"color": colors[side], "width": 1.6},
  187. "itemStyle": {"color": colors[side]}, "data": values})
  188. return {
  189. "title": {"text": title, "left": "center", "textStyle": {"fontSize": 14}},
  190. "animation": False,
  191. "grid": {"left": 70, "right": 40, "top": 50, "bottom": 60},
  192. "legend": {"top": 22, "textStyle": {"fontSize": 12}},
  193. "tooltip": {"trigger": "axis"},
  194. "xAxis": {"type": "value", "name": "首周期采样点序号", "nameLocation": "middle", "nameGap": 30},
  195. "yAxis": {"type": "value", "name": "压力原始值", "nameLocation": "middle", "nameGap": 50},
  196. "series": series,
  197. }
  198. def temp_option(times, values, title, thr_high, thr_highhigh):
  199. data = [[time, value] for time, value in zip(times, values) if value is not None]
  200. return {
  201. "title": {"text": title, "left": "center", "textStyle": {"fontSize": 14}},
  202. "animation": False,
  203. "grid": {"left": 70, "right": 60, "top": 50, "bottom": 60},
  204. "tooltip": {"trigger": "axis"},
  205. "xAxis": {"type": "time", "axisLabel": {"fontSize": 10, "hideOverlap": True},
  206. "name": "时间", "nameLocation": "middle", "nameGap": 30},
  207. "yAxis": {"type": "value", "name": "温度 (℃)", "nameLocation": "middle", "nameGap": 50},
  208. "series": [{
  209. "name": "温度", "type": "line", "showSymbol": False,
  210. "lineStyle": {"color": "#c62828", "width": 1.6}, "itemStyle": {"color": "#c62828"},
  211. "data": data,
  212. "markLine": {"silent": True, "symbol": "none", "data": [
  213. {"yAxis": thr_high, "label": {"formatter": f"高报 {thr_high:g}"}, "lineStyle": {"color": "#e6a23c"}},
  214. {"yAxis": thr_highhigh, "label": {"formatter": f"高高报 {thr_highhigh:g}"}, "lineStyle": {"color": "#f56c6c"}},
  215. ]},
  216. }],
  217. }
  218. def temp_compare_option(series_list, title):
  219. series = []
  220. colors = {"低": "#409eff", "高": "#f56c6c", "修复后": "#67c23a"}
  221. for segment, values in series_list:
  222. series.append({"name": segment, "type": "line", "showSymbol": False,
  223. "lineStyle": {"color": colors[segment], "width": 2},
  224. "itemStyle": {"color": colors[segment]}, "data": values})
  225. return {
  226. "title": {"text": title, "left": "center", "textStyle": {"fontSize": 14}},
  227. "animation": False,
  228. "grid": {"left": 70, "right": 40, "top": 50, "bottom": 60},
  229. "legend": {"top": 22, "textStyle": {"fontSize": 12}},
  230. "tooltip": {"trigger": "axis"},
  231. "xAxis": {"type": "category", "data": [f"{m}" for m in range(0, 121, 5)], "name": "段内分钟"},
  232. "yAxis": {"type": "value", "name": "温度 (℃)", "nameLocation": "middle", "nameGap": 50},
  233. "series": series,
  234. }
  235. def process_dataset(ds):
  236. base, subs = ensure_dirs(ds)
  237. temp_windows = {}
  238. for segment, (start, end) in ds["segments"].items():
  239. tag = f"{ds['unit']}号机_{ds['cylinder'].split('(')[0]}_{segment}"
  240. first_times = None
  241. last_times = None
  242. overlay_map = {}
  243. for side in SIDES:
  244. rows = query_wave_files(ds["device_part"], side, start, end)
  245. if not rows:
  246. print(f"WARN no wave files: {ds['key']} {segment} {side}")
  247. continue
  248. side_tag = "盖侧" if side == "压力盖侧" else "轴侧"
  249. if first_times is None:
  250. first_times = rows[0]["sample_time"]
  251. last_times = rows[-1]["sample_time"]
  252. overlay_values = []
  253. for row in rows:
  254. meta, samples = SVC._load_db_wave(int(row["id"]))
  255. time_label = row["sample_time"].strftime("%Y%m%d%H%M%S")
  256. csv_name = f"提取首周期_{tag}_{side_tag}_{time_label}.csv"
  257. write_first_cycle_csv(os.path.join(subs["01_原始首周期数据"], csv_name), samples)
  258. detail = DataService._build_period_detail(meta, samples, 1)
  259. if side == "压力盖侧":
  260. overlay_values = [[i, float(v)] for i, v in enumerate(samples[:, 1])]
  261. pv = pv_option(detail, side, f"{ds['device_part']} {segment} {side} P-V 功图 ({row['sample_time']:%Y-%m-%d %H:%M})")
  262. pt = ptheta_option(detail, side, f"{ds['device_part']} {segment} {side} 压力-角度图 ({row['sample_time']:%Y-%m-%d %H:%M})")
  263. pv_html = os.path.join(subs["03_功图"], f"PV_{tag}_{side_tag}_{time_label}.html")
  264. pt_html = os.path.join(subs["03_功图"], f"Ptheta_{tag}_{side_tag}_{time_label}.html")
  265. render_html(pv_html, pv, "PV")
  266. render_html(pt_html, pt, "Ptheta")
  267. screenshot(pv_html, pv_html.replace(".html", ".png"))
  268. screenshot(pt_html, pt_html.replace(".html", ".png"))
  269. os.remove(pv_html)
  270. os.remove(pt_html)
  271. if side == "压力盖侧":
  272. overlay_map["压力盖侧"] = overlay_values
  273. else:
  274. overlay_map["压力轴侧"] = [[i, float(v)] for i, v in enumerate(samples[:, 1])]
  275. COPY_LIST.append({
  276. "数据集": ds["key"], "段": segment, "机组": f"{ds['unit']}号机",
  277. "缸": ds["cylinder"], "测点": side, "wave_file_id": int(row["id"]),
  278. "device_part": ds["device_part"], "device_point": side,
  279. "sample_time": row["sample_time"].strftime("%Y-%m-%d %H:%M:%S"),
  280. "原始文件相对路径": row["file_name"],
  281. "原始目录": ds["raw_dir"],
  282. "完整路径": ds["raw_dir"] + "\\" + str(row["file_name"]),
  283. })
  284. if first_times and last_times:
  285. temp_start = first_times
  286. temp_end = last_times + timedelta(minutes=15)
  287. temp_rows = query_pks(ds["device_code"], ds["pks_col"], temp_start, temp_end)
  288. temp_csv = os.path.join(subs["02_PKS温度数据"], f"PKS温度_{tag}_{temp_start:%Y%m%d%H%M}-{temp_end:%Y%m%d%H%M}.csv")
  289. write_pks_csv(temp_csv, temp_rows)
  290. times = [r["sample_time"].strftime("%Y-%m-%d %H:%M:%S") for r in temp_rows]
  291. values = [None if r["value"] is None else round(float(r["value"]), 3) for r in temp_rows]
  292. temp_option_json = temp_option(times, values, f"{ds['device_part']} {segment} {ds['pks_desc']}", ds["thr_high"], ds["thr_highhigh"])
  293. html = os.path.join(subs["04_曲线图"], f"温度曲线_{tag}.html")
  294. render_html(html, temp_option_json, "temp")
  295. screenshot(html, html.replace(".html", ".png"))
  296. os.remove(html)
  297. step = max(1, len(values) // 24)
  298. temp_windows[segment] = values[::step][:25]
  299. if overlay_map:
  300. overlay_html = os.path.join(subs["04_曲线图"], f"首周期压力叠图_{tag}.html")
  301. render_html(overlay_html, pressure_overlay_option(overlay_map, f"{ds['device_part']} {segment} 首周期压力(盖侧/轴侧)"), "overlay")
  302. screenshot(overlay_html, overlay_html.replace(".html", ".png"))
  303. os.remove(overlay_html)
  304. if len(temp_windows) == 3:
  305. compare = temp_compare_option(
  306. [(seg, temp_windows[seg]) for seg in ("低", "高", "修复后")],
  307. f"{ds['device_part']} {ds['pks_desc']} 三段对比",
  308. )
  309. html = os.path.join(subs["04_曲线图"], f"温度三段对比_{ds['key']}.html")
  310. render_html(html, compare, "compare")
  311. screenshot(html, html.replace(".html", ".png"))
  312. os.remove(html)
  313. return base
  314. def build_word(bases):
  315. document = Document()
  316. document.add_heading("压缩机气阀故障分析数据包说明", 0)
  317. document.add_paragraph(
  318. "本数据包围绕两起气阀故障(6号机 2026-04-22 一级3缸排气温度高;"
  319. "4号机 2026-06-10 一级2#3#缸温度异常)整理,覆盖 3 个气缸数据集。"
  320. "每个数据集按“温度低 / 温度高 / 停机修复后”三段各取约 2 小时(8 个首周期)的"
  321. "压力盖侧与压力轴侧数据,并配套 PKS 温度数据、功图与曲线图。"
  322. )
  323. document.add_heading("一、目录结构", level=1)
  324. document.add_paragraph("每个数据集文件夹包含 4 个子目录:")
  325. for name, desc in [
  326. ("01_原始首周期数据", "从数据库 wave_sample_one 导出的首周期采样(sample_index/signal_value/second_value)。原始 CSV 需按《原始文件拷贝清单.csv》从线下目录复制到此。"),
  327. ("02_PKS温度数据", "该段 2 小时窗口内的 PKS 温度原始数据(5 秒采样,DCS 系统)。"),
  328. ("03_功图", "每个首周期的压力-体积功图(P-V)与压力-角度图(P-θ),盖侧/轴侧分别成图,一幅图一个文件。"),
  329. ("04_曲线图", "PKS 温度曲线(含报警阈值线)、三段温度对比、首周期压力盖侧/轴侧叠图。"),
  330. ]:
  331. document.add_paragraph(f"{name}:{desc}", style="List Bullet")
  332. document.add_heading("二、数据集与时间窗", level=1)
  333. for ds in DATASETS:
  334. document.add_heading(f"{ds['key']}({ds['device_part']})", level=2)
  335. document.add_paragraph(f"故障:{ds['fault_desc']}({ds['event_date']})")
  336. document.add_paragraph(f"PKS 温度点:{ds['pks_item']} {ds['pks_desc']};报警阈值:高报 {ds['thr_high']:g}℃ / 高高报 {ds['thr_highhigh']:g}℃")
  337. table = document.add_table(rows=1, cols=3)
  338. table.style = "Light Grid Accent 1"
  339. header = table.rows[0].cells
  340. header[0].text, header[1].text, header[2].text = "段", "起始", "结束"
  341. for segment, (start, end) in ds["segments"].items():
  342. cells = table.add_row().cells
  343. cells[0].text, cells[1].text, cells[2].text = segment, start, end
  344. document.add_heading("三、文件命名规则", level=1)
  345. for line in [
  346. "提取首周期_<机组>_<缸>_<段>_<盖侧|轴侧>_<时间YYYYMMDDHHMMSS>.csv",
  347. "PKS温度_<机组>_<缸>_<段>_<起>-<止>.csv",
  348. "PV_<机组>_<缸>_<段>_<盖侧|轴侧>_<时间>.png / Ptheta_同.png",
  349. "温度曲线_<机组>_<缸>_<段>.png / 温度三段对比_<数据集>.png / 首周期压力叠图_<机组>_<缸>_<段>.png",
  350. ]:
  351. document.add_paragraph(line, style="List Bullet")
  352. document.add_heading("四、CSV 字段说明", level=1)
  353. def add_field_table(title, fields):
  354. document.add_paragraph(title, style="List Bullet")
  355. table = document.add_table(rows=1, cols=2)
  356. table.style = "Light Grid Accent 1"
  357. table.rows[0].cells[0].text = "字段"
  358. table.rows[0].cells[1].text = "说明"
  359. for name, desc in fields:
  360. cells = table.add_row().cells
  361. cells[0].text, cells[1].text = name, desc
  362. add_field_table("01_原始首周期数据/提取首周期_*.csv(每个波形文件该周期内的原始采样)", [
  363. ("sample_index", "该波形文件内的原始采样点序号(从 0 开始,连续)。"),
  364. ("signal_value", "传感器信号原始值(压力盖侧/压力轴侧的压力值,未做标定换算)。"),
  365. ("second_value", "键相信号(二值与/或多值),用于识别每转脉冲与周期起点,>=30 视为脉冲触发。"),
  366. ])
  367. add_field_table("02_PKS温度数据/PKS温度_*.csv(该段 2 小时窗口)", [
  368. ("sample_time", "PKS 温度采样时刻,5 秒一条。"),
  369. ("value", "对应温度测点(见各数据集说明)的原始值,单位 ℃。"),
  370. ])
  371. add_field_table("原始文件拷贝清单.csv(供复制线下原始波形文件)", [
  372. ("数据集", "所属数据集(机组_日期_故障_缸)。"),
  373. ("段", "低 / 高 / 修复后。"),
  374. ("机组", "4号机 / 6号机。"),
  375. ("缸", "对应气缸(如 三缸(一级3缸))。"),
  376. ("测点", "压力盖侧 / 压力轴侧。"),
  377. ("wave_file_id", "数据库 wave_file 主键 id。"),
  378. ("device_part", "机组与部位,如 6号机组三缸。"),
  379. ("device_point", "测点,压力盖侧 / 压力轴侧。"),
  380. ("sample_time", "该波形文件的采样时刻。"),
  381. ("原始文件相对路径", "wave_file.file_name(相对路径)。"),
  382. ("原始目录", "该批次导入目录(compressor_data_import.directory_path)。"),
  383. ("完整路径", "原始目录 + 相对路径,可直接用于拷贝。"),
  384. ])
  385. for base in bases:
  386. document.add_heading(f"五、文件清单:{os.path.basename(base)}", level=1)
  387. for sub in ("01_原始首周期数据", "02_PKS温度数据", "03_功图", "04_曲线图"):
  388. folder = os.path.join(base, sub)
  389. files = sorted(os.listdir(folder)) if os.path.isdir(folder) else []
  390. document.add_paragraph(f"{sub}({len(files)} 个文件)", style="List Bullet")
  391. doc_path = os.path.join(ROOT, "故障分析说明.docx")
  392. document.save(doc_path)
  393. return doc_path
  394. def main():
  395. os.makedirs(ROOT, exist_ok=True)
  396. with open(ECHARTS, "rb") as src, open(os.path.join(ROOT, "echarts.min.js"), "wb") as dst:
  397. dst.write(src.read())
  398. bases = []
  399. for ds in DATASETS:
  400. print("== processing", ds["key"])
  401. bases.append(process_dataset(ds))
  402. if COPY_LIST:
  403. list_path = os.path.join(ROOT, "原始文件拷贝清单.csv")
  404. with open(list_path, "w", newline="", encoding="utf-8-sig") as handle:
  405. writer = csv.DictWriter(handle, fieldnames=list(COPY_LIST[0].keys()))
  406. writer.writeheader()
  407. writer.writerows(COPY_LIST)
  408. print("copy list rows:", len(COPY_LIST))
  409. build_word(bases)
  410. print("DONE ->", ROOT)
  411. if __name__ == "__main__":
  412. main()