import_staff_data.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 导入教职工数据到临时表
  5. 用法:
  6. python import_staff_data.py
  7. 依赖:
  8. pip install pandas sqlalchemy pymysql openpyxl xlrd
  9. 说明:
  10. - 从同目录下的 db_config.py 读取 DB_CONFIG
  11. - 将 `人员清单列表1@正式员工.xlsx` 导入到临时表 `temp_staff_data`
  12. - 自动创建临时表(所有列使用 TEXT 类型)
  13. """
  14. from __future__ import annotations
  15. import os
  16. import sys
  17. import math
  18. import argparse
  19. import importlib.util
  20. from typing import List, Dict, Any
  21. import pandas as pd
  22. from sqlalchemy import create_engine, types as sql_types, text
  23. # Optional, used for xls/xlsx low-level reading
  24. try:
  25. import xlrd # for .xls
  26. except Exception:
  27. xlrd = None
  28. try:
  29. import openpyxl # for .xlsx
  30. except Exception:
  31. openpyxl = None
  32. def load_db_config_from_file(config_path: str) -> dict:
  33. """从配置文件加载数据库配置"""
  34. spec = importlib.util.spec_from_file_location("db_config", config_path)
  35. module = importlib.util.module_from_spec(spec)
  36. spec.loader.exec_module(module) # type: ignore
  37. if not hasattr(module, "DB_CONFIG"):
  38. raise RuntimeError(f"No DB_CONFIG found in {config_path}")
  39. return getattr(module, "DB_CONFIG")
  40. def sanitize_columns(header: List[str]) -> List[str]:
  41. """清理和标准化列名"""
  42. seen: Dict[str, int] = {}
  43. out: List[str] = []
  44. for i, col in enumerate(header):
  45. if col is None:
  46. name = f"col_{i+1}"
  47. else:
  48. name = str(col).strip()
  49. if name == "":
  50. name = f"col_{i+1}"
  51. # replace problematic characters
  52. name = name.replace(" ", "_").replace("\n", "_").replace("\r", "_").replace("(", "_").replace(")", "_")
  53. # ensure starts with letter or underscore
  54. if not name[0].isalpha() and name[0] != "_":
  55. name = f"c_{name}"
  56. base = name
  57. cnt = seen.get(base, 0)
  58. if cnt:
  59. name = f"{base}_{cnt}"
  60. seen[base] = cnt + 1
  61. out.append(name)
  62. return out
  63. def create_temp_table(engine, table_name: str, columns: List[str]):
  64. """创建临时表"""
  65. print(f"创建临时表: {table_name}")
  66. # 构建建表SQL
  67. lines: List[str] = []
  68. lines.append(f"DROP TABLE IF EXISTS `{table_name}`;")
  69. lines.append(f"CREATE TABLE `{table_name}` (")
  70. lines.append(" `id` BIGINT(20) NOT NULL AUTO_INCREMENT,")
  71. # 添加Excel列(TEXT类型)
  72. for col in columns:
  73. lines.append(f" `{col}` TEXT DEFAULT NULL,")
  74. lines.append(" `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP,")
  75. lines.append(" PRIMARY KEY (`id`)")
  76. lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;")
  77. create_sql = "\n".join(lines)
  78. try:
  79. with engine.begin() as conn:
  80. conn.execute(text(create_sql))
  81. print(f"临时表 {table_name} 创建成功")
  82. except Exception as e:
  83. print(f"创建临时表失败: {e}")
  84. raise
  85. def df_to_mysql_append(engine, df: pd.DataFrame, table_name: str, first_write: bool):
  86. """将DataFrame追加到MySQL表"""
  87. # ensure all columns are strings (TEXT)
  88. df = df.where(pd.notnull(df), None)
  89. if first_write:
  90. # 追加数据,不创建表(表已预先创建)
  91. df.to_sql(name=table_name, con=engine, index=False, if_exists="append", chunksize=1000, method="multi")
  92. else:
  93. df.to_sql(name=table_name, con=engine, index=False, if_exists="append", chunksize=1000, method="multi")
  94. def process_xlsx_staff(filepath: str, engine, table_name: str, chunk_size: int = 2000):
  95. """处理教职工Excel文件(.xlsx格式)"""
  96. if openpyxl is None:
  97. raise RuntimeError("Missing openpyxl library to read .xlsx files. Install from requirements.")
  98. print(f"开始读取教职工数据文件: {filepath}")
  99. wb = openpyxl.load_workbook(filename=filepath, read_only=True, data_only=True)
  100. sheet = wb.active
  101. rows = sheet.iter_rows(values_only=True)
  102. try:
  103. header_row = next(rows)
  104. except StopIteration:
  105. print(f"{filepath} 是空文件")
  106. return
  107. # 处理表头
  108. header = [str(c).strip() if c is not None else "" for c in header_row]
  109. header = sanitize_columns(header)
  110. print(f"表头列: {header}")
  111. # 创建临时表
  112. create_temp_table(engine, table_name, header)
  113. # 处理数据行
  114. batch: List[Dict[str, Any]] = []
  115. first_write = True
  116. written = 0
  117. for row in rows:
  118. # 转换NaN值为None
  119. row = [None if (isinstance(x, float) and math.isnan(x)) else x for x in row]
  120. row_dict = {col: row[idx] if idx < len(row) else None for idx, col in enumerate(header)}
  121. batch.append(row_dict)
  122. if len(batch) >= chunk_size:
  123. df = pd.DataFrame(batch)
  124. df_to_mysql_append(engine, df, table_name, first_write)
  125. first_write = False
  126. written += len(batch)
  127. batch = []
  128. print(f"已写入 {written} 行到 {table_name}")
  129. if batch:
  130. df = pd.DataFrame(batch)
  131. df_to_mysql_append(engine, df, table_name, first_write)
  132. written += len(batch)
  133. print(f"写入剩余 {len(batch)} 行到 {table_name},总计写入 {written} 行")
  134. def process_file_staff(filepath: str, engine, table_name: str, chunk_size: int = 2000):
  135. """处理教职工数据文件"""
  136. ext = os.path.splitext(filepath)[1].lower()
  137. if ext in (".xlsx",):
  138. process_xlsx_staff(filepath, engine, table_name, chunk_size=chunk_size)
  139. else:
  140. # 尝试使用pandas fallback
  141. print(f"未知扩展名 {ext},尝试使用 pandas 直接读取(可能占用大量内存)")
  142. df = pd.read_excel(filepath, engine="openpyxl" if ext == ".xlsx" else None)
  143. df.columns = sanitize_columns(list(df.columns))
  144. # 创建临时表
  145. create_temp_table(engine, table_name, list(df.columns))
  146. # 导入数据
  147. df_to_mysql_append(engine, df, table_name, first_write=True)
  148. def build_engine_from_config(db_config: dict):
  149. """根据配置构建数据库引擎"""
  150. user = db_config.get("user")
  151. password = db_config.get("password")
  152. host = db_config.get("host", "localhost")
  153. port = db_config.get("port", 3306)
  154. database = db_config.get("database")
  155. charset = db_config.get("charset", "utf8mb4")
  156. uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
  157. engine = create_engine(uri, pool_pre_ping=True)
  158. return engine
  159. def main(args=None):
  160. """主函数"""
  161. base_dir = os.path.dirname(os.path.abspath(__file__))
  162. # 查找数据库配置文件
  163. config_path = os.path.join(base_dir, "db_config.py")
  164. if not os.path.exists(config_path):
  165. # 尝试从上级目录查找
  166. config_path = os.path.join(os.path.dirname(base_dir), "学校历史数据迁移", "db_config.py")
  167. if not os.path.exists(config_path):
  168. print(f"找不到配置文件: {config_path}")
  169. sys.exit(1)
  170. print(f"使用配置文件: {config_path}")
  171. db_config = load_db_config_from_file(config_path)
  172. engine = build_engine_from_config(db_config)
  173. # 教职工数据文件路径
  174. staff_file = os.path.join(base_dir, "人员清单列表1@正式员工.xlsx")
  175. if args and args.file:
  176. staff_file = args.file
  177. table_name = "temp_staff_data"
  178. if args and args.table:
  179. table_name = args.table
  180. if not os.path.exists(staff_file):
  181. print(f"找不到教职工数据文件: {staff_file}")
  182. sys.exit(1)
  183. print(f"开始导入教职工数据到临时表 {table_name} ...")
  184. try:
  185. process_file_staff(staff_file, engine, table_name, chunk_size=1000)
  186. print(f"教职工数据导入完成 -> {table_name}")
  187. # 验证导入结果
  188. with engine.begin() as conn:
  189. result = conn.execute(text(f"SELECT COUNT(*) as count FROM {table_name}"))
  190. count = result.fetchone()[0]
  191. print(f"临时表 {table_name} 共导入 {count} 条记录")
  192. # 显示前几行数据
  193. result = conn.execute(text(f"SELECT * FROM {table_name} LIMIT 5"))
  194. columns = result.keys()
  195. print(f"\n前5行数据预览:")
  196. print("-" * 80)
  197. print(f"{' | '.join(columns)}")
  198. print("-" * 80)
  199. for row in result:
  200. print(f"{' | '.join(str(cell) if cell is not None else '' for cell in row)}")
  201. except Exception as e:
  202. print(f"导入过程中发生错误: {e}")
  203. sys.exit(1)
  204. print("\n导入完成!")
  205. if __name__ == "__main__":
  206. parser = argparse.ArgumentParser(description="导入教职工数据到临时表")
  207. parser.add_argument("--file", type=str, help="教职工数据文件路径")
  208. parser.add_argument("--table", type=str, default="temp_staff_data", help="临时表名")
  209. args = parser.parse_args()
  210. main(args)</contents>
  211. </xai:function_call name="todo_write">
  212. <parameter name="merge">true