import_external_staff.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 将 `人员清单列表@外聘.xlsx` 导入到临时表 temp_external_staff
  5. 用法:
  6. python import_external_staff.py
  7. 依赖:
  8. pandas sqlalchemy pymysql openpyxl
  9. 说明:
  10. - 从同目录或上级目录读取 db_config.py 中的 DB_CONFIG
  11. - 自动根据 Excel 表头创建临时表(所有列使用 TEXT)
  12. - 支持批量写入
  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, text
  23. try:
  24. import openpyxl
  25. except Exception:
  26. openpyxl = None
  27. def load_db_config_from_file(config_path: str) -> dict:
  28. spec = importlib.util.spec_from_file_location("db_config", config_path)
  29. module = importlib.util.module_from_spec(spec)
  30. spec.loader.exec_module(module) # type: ignore
  31. if not hasattr(module, "DB_CONFIG"):
  32. raise RuntimeError(f"No DB_CONFIG found in {config_path}")
  33. return getattr(module, "DB_CONFIG")
  34. def clean_column_name(col: str) -> str:
  35. if col is None:
  36. return ""
  37. s = str(col).strip()
  38. s = s.replace("(", "_").replace(")", "_").replace("(", "_").replace(")", "_")
  39. s = s.replace("、", "_").replace(" ", "_").replace("\n", "_").replace("\r", "_")
  40. # ensure valid identifier start
  41. if not s:
  42. s = "col"
  43. if not (s[0].isalpha() or s[0] == "_"):
  44. s = f"c_{s}"
  45. return s
  46. def build_engine_from_config(db_config: dict):
  47. user = db_config.get("user")
  48. password = db_config.get("password")
  49. host = db_config.get("host", "localhost")
  50. port = db_config.get("port", 3306)
  51. database = db_config.get("database")
  52. charset = db_config.get("charset", "utf8mb4")
  53. uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
  54. return create_engine(uri, pool_pre_ping=True)
  55. def create_table(conn, table_name: str, columns: List[str]):
  56. # 先单独执行 DROP,再执行 CREATE(避免多语句执行限制导致语法错误)
  57. conn.execute(text(f"DROP TABLE IF EXISTS `{table_name}`"))
  58. lines: List[str] = []
  59. lines.append(f"CREATE TABLE `{table_name}` (")
  60. lines.append(" `id` BIGINT(20) NOT NULL AUTO_INCREMENT,")
  61. for col in columns:
  62. lines.append(f" `{col}` TEXT DEFAULT NULL,")
  63. lines.append(" `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP,")
  64. lines.append(" PRIMARY KEY (`id`)")
  65. lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;")
  66. sql = "\n".join(lines)
  67. conn.execute(text(sql))
  68. def import_excel_to_table(engine, excel_path: str, table_name: str, chunk_size: int = 1000):
  69. if not os.path.exists(excel_path):
  70. raise FileNotFoundError(f"找不到文件: {excel_path}")
  71. # 读取表头和全部数据使用 pandas(适合中小文件)
  72. df = pd.read_excel(excel_path, engine="openpyxl")
  73. raw_cols = list(df.columns)
  74. clean_cols = [clean_column_name(c) for c in raw_cols]
  75. # 重命名 DataFrame 列为清理后的列名
  76. df.columns = clean_cols
  77. with engine.begin() as conn:
  78. # 创建表
  79. create_table(conn, table_name, clean_cols)
  80. # 分块写入
  81. total = 0
  82. for start in range(0, len(df), chunk_size):
  83. chunk = df.iloc[start:start+chunk_size].where(pd.notnull(df), None)
  84. chunk.to_sql(name=table_name, con=conn, index=False, if_exists="append", method="multi", chunksize=chunk_size)
  85. total += len(chunk)
  86. return total
  87. def main():
  88. parser = argparse.ArgumentParser(description="将人员清单(外聘)导入临时表")
  89. parser.add_argument("--file", type=str, default="人员清单列表@外聘.xlsx", help="Excel 文件名或路径")
  90. parser.add_argument("--table", type=str, default="temp_external_staff", help="临时表名")
  91. parser.add_argument("--db-config", type=str, default="../学校历史数据迁移/db_config.py", help="db_config.py 路径")
  92. args = parser.parse_args()
  93. base_dir = os.path.dirname(os.path.abspath(__file__))
  94. excel_path = args.file
  95. if not os.path.isabs(excel_path):
  96. excel_path = os.path.join(base_dir, excel_path)
  97. db_config_path = args.db_config
  98. if not os.path.isabs(db_config_path):
  99. db_config_path = os.path.join(base_dir, db_config_path)
  100. if not os.path.exists(db_config_path):
  101. # try parent folder
  102. db_config_path = os.path.join(os.path.dirname(base_dir), "学校历史数据迁移", "db_config.py")
  103. if not os.path.exists(db_config_path):
  104. print(f"找不到 db_config 文件: {db_config_path}")
  105. sys.exit(1)
  106. db_config = load_db_config_from_file(db_config_path)
  107. engine = build_engine_from_config(db_config)
  108. print(f"开始导入文件: {excel_path} 到表 {args.table}")
  109. total = import_excel_to_table(engine, excel_path, args.table, chunk_size=1000)
  110. print(f"导入完成,共插入 {total} 条记录到 {args.table}")
  111. if __name__ == "__main__":
  112. main()