main.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. #拉小分表工具(即分类汇总)
  2. import sys
  3. import pandas as pd
  4. from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton,
  5. QFileDialog, QTableWidget, QTableWidgetItem, QSpinBox,
  6. QLabel, QProgressBar, QCheckBox, QLineEdit, QHBoxLayout, QComboBox)
  7. from PyQt5.QtCore import Qt
  8. from PyQt5.QtGui import QColor
  9. import matplotlib.pyplot as plt
  10. import ast
  11. plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
  12. plt.rcParams['axes.unicode_minus'] = False
  13. class ExcelProcessor(QWidget):
  14. def __init__(self):
  15. super().__init__()
  16. # 先定义属性,再初始化UI
  17. self.df = pd.DataFrame()
  18. self.threshold = 10
  19. self.ignore_cols = [1,2,3,8,9] # 默认忽略列索引
  20. self.current_file_path = None # 添加当前文件路径属性
  21. self.color_groups = [] # 存储班级颜色分组
  22. self.current_result_df = None # 存储当前处理结果
  23. self.current_group_col = None # 存储当前分组列名
  24. self.initUI()
  25. def initUI(self):
  26. self.setWindowTitle('Excel数据处理工具')
  27. self.setGeometry(300, 300, 800, 600)
  28. layout = QVBoxLayout()
  29. # 文件选择
  30. self.btn_open = QPushButton('选择Excel文件', self)
  31. self.btn_open.clicked.connect(self.open_file)
  32. layout.addWidget(self.btn_open)
  33. # 文件路径显示
  34. self.lbl_file_path = QLabel('未选择文件', self)
  35. layout.addWidget(self.lbl_file_path)
  36. # 忽略列设置
  37. self.ignore_layout = QHBoxLayout()
  38. self.lbl_ignore = QLabel('忽略列索引:', self)
  39. self.txt_ignore = QLineEdit('1,2,3,8,9', self)
  40. self.ignore_layout.addWidget(self.lbl_ignore)
  41. self.ignore_layout.addWidget(self.txt_ignore)
  42. layout.addLayout(self.ignore_layout)
  43. # 分组列选择
  44. self.group_layout = QHBoxLayout()
  45. self.lbl_group = QLabel('分组列:', self)
  46. self.cmb_group = QComboBox(self)
  47. self.group_layout.addWidget(self.lbl_group)
  48. self.group_layout.addWidget(self.cmb_group)
  49. layout.addLayout(self.group_layout)
  50. # 阈值设置
  51. self.threshold_layout = QVBoxLayout()
  52. self.lbl_threshold = QLabel('总分阈值:', self)
  53. self.spin_threshold = QSpinBox(self)
  54. self.spin_threshold.setValue(self.threshold) # 现在self.threshold已经定义了
  55. self.threshold_layout.addWidget(self.lbl_threshold)
  56. self.threshold_layout.addWidget(self.spin_threshold)
  57. layout.addLayout(self.threshold_layout)
  58. # 班级颜色分组设置
  59. self.color_layout = QVBoxLayout()
  60. self.lbl_color = QLabel('班级颜色分组 (格式: [1,2,30,31],[3,4,5,6,32,33,34,35],...):', self)
  61. self.txt_color = QLineEdit('[1,2,30,31],[3,4,5,6,32,33,34,35],[7,8,9,10,36,37,38,39],[12,13,14,42,43,44],[23,24,41]', self)
  62. self.color_layout.addWidget(self.lbl_color)
  63. self.color_layout.addWidget(self.txt_color)
  64. layout.addLayout(self.color_layout)
  65. # 处理选项
  66. self.chk_header = QCheckBox('包含表头', self)
  67. self.chk_header.setChecked(True)
  68. self.chk_header.stateChanged.connect(self.on_header_changed) # 添加状态改变信号连接
  69. layout.addWidget(self.chk_header)
  70. # 处理按钮
  71. self.btn_process = QPushButton('开始处理', self)
  72. self.btn_process.clicked.connect(self.process_data)
  73. self.btn_process.setEnabled(False) # 初始时禁用处理按钮
  74. layout.addWidget(self.btn_process)
  75. # 进度条
  76. self.progress = QProgressBar(self)
  77. self.progress.setValue(0) # 初始化为0
  78. layout.addWidget(self.progress)
  79. # 结果显示
  80. self.table_result = QTableWidget(self)
  81. layout.addWidget(self.table_result)
  82. self.setLayout(layout)
  83. def open_file(self):
  84. path, _ = QFileDialog.getOpenFileName(self, '选择Excel文件', '', 'Excel Files (*.xlsx)')
  85. if path:
  86. self.current_file_path = path # 保存文件路径
  87. self.load_file()
  88. def load_file(self):
  89. """加载文件的方法,供open_file和on_header_changed调用"""
  90. if self.current_file_path:
  91. try:
  92. # 根据复选框状态决定读取方式
  93. if self.chk_header.isChecked():
  94. # 包含表头:忽略第一行,将第二行视为标题行,第三行开始为数据
  95. self.df = pd.read_excel(self.current_file_path, header=1) # header=1 表示第二行作为标题
  96. else:
  97. # 不包含表头:第一行视为标题行,第二行开始为数据
  98. self.df = pd.read_excel(self.current_file_path, header=0) # header=0 表示第一行作为标题
  99. self.lbl_file_path.setText(f'已选择文件: {self.current_file_path.split("/")[-1]}') # 只显示文件名
  100. self.progress.setValue(0) # 重置进度条
  101. # 更新分组列下拉框
  102. self.update_group_combo()
  103. # 启用处理按钮
  104. self.btn_process.setEnabled(True)
  105. except Exception as e:
  106. print(f"文件读取错误: {str(e)}")
  107. def on_header_changed(self):
  108. """当包含表头复选框状态改变时调用"""
  109. if self.current_file_path: # 如果已经选择了文件
  110. self.load_file() # 重新加载文件
  111. def update_group_combo(self):
  112. """更新分组列下拉框选项"""
  113. self.cmb_group.clear()
  114. if not self.df.empty:
  115. # 添加所有列名
  116. for col in self.df.columns:
  117. self.cmb_group.addItem(str(col))
  118. # 尝试自动选择"班级"列
  119. group_keywords = ['班级', '班', 'class', 'Class', 'CLASSS']
  120. for keyword in group_keywords:
  121. for i in range(self.cmb_group.count()):
  122. if keyword in self.cmb_group.itemText(i):
  123. self.cmb_group.setCurrentIndex(i)
  124. return
  125. def parse_color_groups(self):
  126. """解析班级颜色分组设置"""
  127. color_text = self.txt_color.text().strip()
  128. if not color_text:
  129. return []
  130. try:
  131. # 使用ast.literal_eval安全地解析字符串
  132. # 首先将整个字符串包装在列表中,然后解析
  133. parsed = ast.literal_eval(f'[{color_text}]')
  134. # 确保结果是列表的列表
  135. color_groups = []
  136. for item in parsed:
  137. if isinstance(item, list):
  138. color_groups.append(item)
  139. elif isinstance(item, int):
  140. color_groups.append([item])
  141. else:
  142. print(f"忽略无效的分组项: {item}")
  143. return color_groups
  144. except Exception as e:
  145. print(f"解析班级颜色分组时出错: {str(e)}")
  146. return []
  147. def get_class_color(self, class_value):
  148. """根据班级值获取对应的颜色索引"""
  149. # 尝试将班级值转换为整数进行比较
  150. try:
  151. class_num = int(class_value)
  152. except (ValueError, TypeError):
  153. # 如果无法转换为整数,尝试提取数字
  154. try:
  155. import re
  156. numbers = re.findall(r'\d+', str(class_value))
  157. if numbers:
  158. class_num = int(numbers[0])
  159. else:
  160. class_num = None
  161. except:
  162. class_num = None
  163. # 查找班级所属的颜色组
  164. color_index = -1
  165. if class_num is not None:
  166. for i, group in enumerate(self.color_groups):
  167. if class_num in group:
  168. color_index = i
  169. break
  170. return color_index
  171. def process_data(self):
  172. if self.df.empty:
  173. return
  174. # 重置进度条
  175. self.progress.setValue(0)
  176. # 解析忽略列
  177. try:
  178. self.ignore_cols = list(map(int, self.txt_ignore.text().split(',')))
  179. except:
  180. self.ignore_cols = [1,2,3,8,9]
  181. # 解析班级颜色分组
  182. self.color_groups = self.parse_color_groups()
  183. # 数据清洗 - 先找到用户选择的分组列的位置
  184. selected_group_col = self.cmb_group.currentText()
  185. group_col_index = None
  186. for i, col in enumerate(self.df.columns):
  187. if col == selected_group_col:
  188. group_col_index = i
  189. break
  190. # 确保分组列不被忽略
  191. if group_col_index is not None and group_col_index in self.ignore_cols:
  192. self.ignore_cols.remove(group_col_index)
  193. valid_cols = [i for i in range(self.df.shape[1]) if i not in self.ignore_cols]
  194. self.df = self.df.iloc[:, valid_cols]
  195. # 更新分组列下拉框(因为列索引可能已改变)
  196. self.update_group_combo()
  197. # 重新获取分组列名(因为列索引可能已改变)
  198. selected_group_col = self.cmb_group.currentText()
  199. # 删除低分行
  200. score_col = [col for col in self.df.columns if '总分' in str(col)]
  201. if score_col:
  202. self.df = self.df[pd.to_numeric(self.df[score_col[0]], errors='coerce').notnull()]
  203. self.df = self.df[self.df[score_col[0]] >= self.spin_threshold.value()]
  204. self.progress.setValue(40)
  205. # 分类汇总
  206. if len(valid_cols) == 0:
  207. return
  208. # 使用用户选择的分组列
  209. group_col_name = selected_group_col
  210. # 如果用户选择的列不存在,使用备选方案
  211. if group_col_name not in self.df.columns:
  212. # 查找可能的分组列(班级、班、class等)
  213. group_keywords = ['班级', '班', 'class', 'Class', 'CLASSS']
  214. group_col_name = None
  215. for keyword in group_keywords:
  216. for col in self.df.columns:
  217. if keyword in str(col):
  218. group_col_name = col
  219. break
  220. if group_col_name:
  221. break
  222. # 如果还是找不到,使用第一列作为分组列
  223. if group_col_name is None:
  224. group_col_name = self.df.columns[0]
  225. print(f"警告:未找到班级列,使用第一列 '{group_col_name}' 进行分组")
  226. numeric_cols = self.df.select_dtypes(include='number').columns.tolist()
  227. # 确保分组列不在数值列中
  228. if group_col_name in numeric_cols:
  229. numeric_cols.remove(group_col_name)
  230. if len(numeric_cols) == 0:
  231. print("错误:没有找到数值列进行汇总")
  232. return
  233. grouped = self.df.groupby(group_col_name)[numeric_cols].mean().reset_index()
  234. # 保留两位小数
  235. grouped[numeric_cols] = grouped[numeric_cols].round(2)
  236. grouped = grouped.sort_values(by=group_col_name)
  237. self.progress.setValue(60)
  238. # 保存处理结果
  239. self.current_result_df = grouped
  240. self.current_group_col = group_col_name
  241. # 显示结果
  242. self.display_result(grouped, group_col_name)
  243. def display_result(self, df, group_col_name):
  244. self.table_result.setRowCount(len(df))
  245. self.table_result.setColumnCount(len(df.columns))
  246. # 设置表头
  247. self.table_result.setHorizontalHeaderLabels(df.columns)
  248. # 预定义颜色列表
  249. colors = [
  250. QColor(255, 200, 200), # 浅红色
  251. QColor(200, 255, 200), # 浅绿色
  252. QColor(200, 200, 255), # 浅蓝色
  253. QColor(255, 255, 200), # 浅黄色
  254. QColor(255, 200, 255), # 浅紫色
  255. QColor(200, 255, 255), # 浅青色
  256. QColor(255, 220, 200), # 浅橙色
  257. QColor(220, 200, 255), # 浅紫罗兰色
  258. ]
  259. # 填充数据并设置颜色
  260. for row in range(len(df)):
  261. class_value = df.iloc[row][group_col_name]
  262. color_index = self.get_class_color(class_value)
  263. for col in range(len(df.columns)):
  264. item = QTableWidgetItem(str(df.iloc[row, col]))
  265. # 如果班级在颜色分组中,设置背景色
  266. if color_index >= 0 and color_index < len(colors):
  267. item.setBackground(colors[color_index % len(colors)])
  268. self.table_result.setItem(row, col, item)
  269. # 完成进度
  270. self.progress.setValue(100)
  271. # 导出按钮
  272. if hasattr(self, 'btn_export'):
  273. self.layout().removeWidget(self.btn_export)
  274. self.btn_export.deleteLater()
  275. self.btn_export = QPushButton('导出结果', self)
  276. self.btn_export.clicked.connect(lambda: self.export_data(df))
  277. self.layout().addWidget(self.btn_export)
  278. def export_data(self, df):
  279. path, _ = QFileDialog.getSaveFileName(self, '保存结果', '',
  280. 'Excel Files (*.xlsx);;图片文件 (*.png)')
  281. if path:
  282. if path.endswith('.xlsx'):
  283. self.export_to_excel(df, path)
  284. elif path.endswith('.png'):
  285. self.save_as_image(df, path)
  286. def export_to_excel(self, df, path):
  287. """导出到Excel并应用颜色和边框"""
  288. try:
  289. # 检查是否安装了openpyxl
  290. try:
  291. import openpyxl
  292. from openpyxl.styles import PatternFill, Border, Side
  293. except ImportError:
  294. print("未安装openpyxl,无法导出带颜色的Excel文件")
  295. df.to_excel(path, index=False)
  296. print(f"已导出无颜色的Excel文件: {path}")
  297. return
  298. # 使用openpyxl引擎,以便设置单元格样式
  299. with pd.ExcelWriter(path, engine='openpyxl') as writer:
  300. df.to_excel(writer, index=False, sheet_name='Sheet1')
  301. # 获取工作表
  302. workbook = writer.book
  303. worksheet = writer.sheets['Sheet1']
  304. # 定义颜色映射(RGB值)
  305. colors_rgb = [
  306. 'FFFFC8C8', # 浅红色
  307. 'FFC8FFC8', # 浅绿色
  308. 'FFC8C8FF', # 浅蓝色
  309. 'FFFFFFC8', # 浅黄色
  310. 'FFFFC8FF', # 浅紫色
  311. 'FFC8FFFF', # 浅青色
  312. 'FFFFDCC8', # 浅橙色
  313. 'FFDCC8FF', # 浅紫罗兰色
  314. ]
  315. # 定义边框样式
  316. thin_border = Border(
  317. left=Side(style='thin'),
  318. right=Side(style='thin'),
  319. top=Side(style='thin'),
  320. bottom=Side(style='thin')
  321. )
  322. # 为每一行设置背景色和边框
  323. for row_idx in range(1, len(df) + 2): # 从第1行(表头)开始到数据行结束
  324. # 设置背景色(仅对数据行,不包括表头)
  325. if row_idx >= 2: # 数据行从第2行开始
  326. class_value = df.iloc[row_idx-2][self.current_group_col]
  327. color_index = self.get_class_color(class_value)
  328. if color_index >= 0 and color_index < len(colors_rgb):
  329. fill_color = colors_rgb[color_index % len(colors_rgb)]
  330. # 为这一行的所有单元格设置背景色
  331. for col_idx in range(1, len(df.columns) + 1):
  332. cell = worksheet.cell(row=row_idx, column=col_idx)
  333. cell.fill = PatternFill(start_color=fill_color,
  334. end_color=fill_color,
  335. fill_type='solid')
  336. # 为所有单元格设置边框(包括表头和数据行)
  337. for col_idx in range(1, len(df.columns) + 1):
  338. cell = worksheet.cell(row=row_idx, column=col_idx)
  339. cell.border = thin_border
  340. # 自动调整列宽
  341. for column in worksheet.columns:
  342. max_length = 0
  343. column_letter = column[0].column_letter
  344. for cell in column:
  345. try:
  346. if len(str(cell.value)) > max_length:
  347. max_length = len(str(cell.value))
  348. except:
  349. pass
  350. adjusted_width = (max_length + 2)
  351. worksheet.column_dimensions[column_letter].width = adjusted_width
  352. print(f"带颜色和边框的Excel文件已保存到: {path}")
  353. except Exception as e:
  354. print(f"导出带颜色的Excel时出错: {str(e)}")
  355. # 如果带颜色的导出失败,尝试普通导出
  356. try:
  357. df.to_excel(path, index=False)
  358. print(f"Excel文件已保存到: {path} (无颜色和边框)")
  359. except Exception as e2:
  360. print(f"普通Excel导出也失败: {str(e2)}")
  361. def save_as_image(self, df, path):
  362. import matplotlib.pyplot as plt
  363. import numpy as np
  364. fig, ax = plt.subplots(figsize=(12, 8))
  365. ax.axis('off')
  366. # 获取数据
  367. cell_text = []
  368. for i in range(len(df)):
  369. row_data = []
  370. for j in range(len(df.columns)):
  371. row_data.append(str(df.iloc[i, j]))
  372. cell_text.append(row_data)
  373. col_labels = [str(col) for col in df.columns]
  374. # 预定义颜色
  375. colors_mpl = [
  376. [1.0, 0.8, 0.8], # 浅红色
  377. [0.8, 1.0, 0.8], # 浅绿色
  378. [0.8, 0.8, 1.0], # 浅蓝色
  379. [1.0, 1.0, 0.8], # 浅黄色
  380. [1.0, 0.8, 1.0], # 浅紫色
  381. [0.8, 1.0, 1.0], # 浅青色
  382. [1.0, 0.86, 0.8], # 浅橙色
  383. [0.86, 0.8, 1.0], # 浅紫罗兰色
  384. ]
  385. # 构建颜色数组 - 关键修复:只包含数据行,不包括表头
  386. cell_colors = []
  387. # 只添加数据行颜色(不包括表头)
  388. for i in range(len(df)):
  389. class_value = df.iloc[i][self.current_group_col]
  390. color_index = self.get_class_color(class_value)
  391. if 0 <= color_index < len(colors_mpl):
  392. row_color = colors_mpl[color_index]
  393. else:
  394. row_color = [1, 1, 1] # 默认白色
  395. row_colors = [row_color for _ in range(len(df.columns))]
  396. cell_colors.append(row_colors)
  397. # 调试信息
  398. print(f"数据行数: {len(df)}")
  399. print(f"cell_text 行数: {len(cell_text)}")
  400. print(f"cell_colors 行数: {len(cell_colors)}")
  401. # 确保行数匹配 - 现在应该都是7行
  402. if len(cell_colors) != len(cell_text):
  403. print(f"警告: 颜色行数 ({len(cell_colors)}) 与数据行数 ({len(cell_text)}) 不匹配")
  404. # 强制调整颜色数组大小
  405. if len(cell_colors) > len(cell_text):
  406. cell_colors = cell_colors[:len(cell_text)]
  407. else:
  408. while len(cell_colors) < len(cell_text):
  409. cell_colors.append([[1, 1, 1] for _ in range(len(df.columns))])
  410. # 创建表格 - 关键修复:使用colColours参数单独设置表头颜色
  411. table = ax.table(
  412. cellText=cell_text,
  413. colLabels=col_labels,
  414. cellLoc='center',
  415. loc='center',
  416. cellColours=cell_colors,
  417. colColours=[[1, 1, 1] for _ in range(len(df.columns))] # 表头颜色设为白色
  418. )
  419. table.auto_set_font_size(False)
  420. table.set_fontsize(10)
  421. table.scale(1, 1.5)
  422. # 设置单元格边框
  423. for key, cell in table.get_celld().items():
  424. cell.set_linewidth(0.5)
  425. cell.set_edgecolor('black')
  426. plt.savefig(path, bbox_inches='tight', dpi=300)
  427. plt.close()
  428. print(f"图片已保存到: {path}")
  429. if __name__ == '__main__':
  430. app = QApplication(sys.argv)
  431. ex = ExcelProcessor()
  432. ex.show()
  433. sys.exit(app.exec_())