car_zu_lin.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. """车辆租赁合同数据处理
  2. """
  3. import re
  4. import subprocess
  5. from datetime import datetime
  6. from dateutil.relativedelta import relativedelta
  7. from loguru import logger
  8. import pandas as pd
  9. import paramiko
  10. # 添加日志记录,将日志输出到文件 a.log
  11. logger.add(sink='a.log')
  12. ssh_hostname = '172.16.107.4' # 定义远程主机地址
  13. ssh_port = 22 # 定义SSH服务的端口号
  14. ssh_username = 'app' # 定义登录远程主机的用户名
  15. ssh_password = '(l4w0ST_' # 定义登录远程主机的密码
  16. # 服务器文件夹路径
  17. remote_dir_path = '/data/history/car/zu-lin/'
  18. # 数据库连接信息
  19. db_host = "172.16.107.5" # 数据库主机地址
  20. db_port = 5432 # 数据库端口号
  21. db_username = "finance" # 数据库用户名
  22. db_password = "Finance@unicom23" # 数据库密码
  23. dbname = "financialdb" # 数据库名称
  24. # 数据文件路径
  25. input_path = 'data.xlsx'
  26. # 输出文件路径
  27. output_path = 'output.csv'
  28. def data_process():
  29. # 正则表达式匹配车牌省份简称(如京、津、晋等)
  30. has_che_pai_province_pattern = re.compile(
  31. "[" + re.escape("京津晋冀蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁国防") + "]")
  32. # 正则表达式匹配非车牌字符,排除车牌可能包含的字符(如字母、数字、特殊标志等)
  33. not_che_pai_pattern = re.compile(
  34. "[^京津晋冀蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新港澳学挂领试超练警国防A-Z\\d]")
  35. # 正则表达式匹配完整的车牌号格式
  36. che_pai_pattern = re.compile(
  37. r"([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z][A-Z]"
  38. r"(([DF]((?![IO])[A-Z0-9](?![IO]))\d{4})|(\d{5}[DF]))|"
  39. r"[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z][A-Z][A-Z0-9]{4}[A-Z0-9挂学警港澳])"
  40. )
  41. # 定义二级行政区划映射表(地级市及其下属区县)
  42. er_ji_map = {
  43. "石家庄": ["鹿泉", "藁城", "栾城", "井陉矿区", "井陉", "无极", "正定", "元氏", "新乐", "晋州", "平山", "灵寿",
  44. "赞皇", "赵县", "行唐", "高邑", "辛集", "深泽"],
  45. "唐山": ["唐山高开区", "迁西", "海港", "开平", "丰南", "滦县", "乐亭", "丰润", "玉田", "古冶", "曹妃甸", "遵化",
  46. "滦南", "迁安"],
  47. "秦皇岛": ["北戴河新区", "北戴河", "山海关", "昌黎", "卢龙", "青龙", "抚宁"],
  48. "邯郸": ["曲周", "魏县", "馆陶", "磁县", "大名", "鸡泽", "成安", "涉县", "永年", "武安", "峰峰", "广平", "临漳",
  49. "邱县", "肥乡"],
  50. "邢台": ["新河", "南宫", "隆尧", "内邱", "平乡", "宁晋", "广宗", "清河", "临西", "任县", "巨鹿", "沙河", "威县",
  51. "临城", "柏乡", "南和"],
  52. "保定": ["涞水", "蠡县", "顺平", "博野", "安国", "涞源", "唐县", "定州", "高阳", "曲阳", "阜平", "清苑",
  53. "高碑店",
  54. "满城", "涿州", "易县", "望都", "徐水", "定兴", "白沟"],
  55. "张家口": ["张北", "崇礼", "康保", "赤城", "阳原", "万全", "下花园", "尚义", "怀安", "怀来", "蔚县", "涿鹿",
  56. "沽源",
  57. "宣化"],
  58. "承德": ["承德县", "兴隆", "宽城", "平泉", "营子", "隆化", "滦平", "围场", "丰宁", "双滦"],
  59. "廊坊": ["文安", "霸州", "大城", "廊坊开发区", "三河", "香河", "永清", "胜芳", "燕郊", "固安", "大厂"],
  60. "沧州": ["东光", "吴桥", "黄骅", "盐山", "孟村", "泊头", "献县", "南皮", "渤海新区", "海兴", "沧县", "河间",
  61. "青县",
  62. "任丘", "肃宁"],
  63. "衡水": ["景县", "阜城", "枣强", "深州", "饶阳", "故城", "武强", "武邑", "冀州", "安平"],
  64. "雄安": ["容城", "雄县", "安新"]
  65. }
  66. # 读取 Excel 文件中的数据
  67. df = pd.read_excel(io=input_path)
  68. # 对需要清理的列进行字符串清理,移除多余的空白字符
  69. df = df.map(lambda x: re.sub(r'\s+', '', x) if type(x) is str else x)
  70. df.columns = ["year_month", "raw_che_pai_hao_he_tong", "che_xing", "che_liang_suo_shu_dan_wei",
  71. "he_tong_ming_cheng", "he_tong_bian_hao", "jia_shui_he_ji_jin_e", "bu_han_shui_jin_e", "shui_e",
  72. "zu_qi", "raw_che_pai_hao_ti_huan", "ti_huan_nian_yue", "bei_zhu"]
  73. # 定义函数,用于提取并标准化车牌号
  74. def get_che_pai(che_pai):
  75. # 如果车牌号为空或无效,则返回空字符串
  76. if pd.isna(che_pai) or not che_pai or not che_pai.strip():
  77. return ""
  78. # 将车牌号转换为大写
  79. upper_case = che_pai.upper()
  80. # 移除车牌号中不符合规则的字符
  81. s = not_che_pai_pattern.sub("", upper_case)
  82. # 使用正则表达式匹配合法的车牌号
  83. m = che_pai_pattern.search(s)
  84. if m:
  85. return m.group(0)
  86. # 如果车牌号包含省份简称但未匹配成功,记录警告日志
  87. if has_che_pai_province_pattern.search(che_pai):
  88. logger.warning(f"车牌匹配失败: {che_pai} -> {s}")
  89. return s
  90. # 如果完全无法匹配,记录警告日志并返回原车牌号
  91. logger.warning(f"车牌匹配失败: {che_pai} -> {upper_case}")
  92. return upper_case
  93. # 定义函数,用于标记车牌号是否匹配失败
  94. def che_pai_fail(che_pai):
  95. # 如果车牌号为空或无效,则标记为失败
  96. if pd.isna(che_pai) or not che_pai or not che_pai.strip():
  97. return "1"
  98. # 移除车牌号中不符合规则的字符
  99. s = not_che_pai_pattern.sub("", che_pai.upper())
  100. # 使用正则表达式匹配合法的车牌号
  101. m = che_pai_pattern.search(s)
  102. if m:
  103. return "0" # 匹配成功
  104. return "1" # 匹配失败
  105. # 定义函数,用于提取一级单位
  106. def get_first_unit(unit):
  107. # 如果单位为空或无效,则返回空字符串
  108. if pd.isna(unit) or not unit or not unit.strip():
  109. return ""
  110. # 根据单位名称中的关键词返回对应的一级单位
  111. if "机动通信局" in unit or "机动局" in unit or "传输局" in unit or "线路维护中心" in unit:
  112. return "机动局"
  113. if "雄安基地建设部" in unit:
  114. return "雄安基地建设部"
  115. if "华北基地建设部" in unit:
  116. return "华北基地建设部"
  117. # 遍历 er_ji_map 的键,寻找匹配的一级单位
  118. for yj in er_ji_map.keys():
  119. if yj in unit:
  120. return yj
  121. return "省公司本部" # 默认返回省公司本部
  122. # 结果列表
  123. result_list = []
  124. # 遍历每一行
  125. for _, row in df.iterrows():
  126. # 提取 year_month 并转换为整数
  127. year_month = row['year_month']
  128. year_month_integer = int(year_month)
  129. # 解析日期
  130. local_date = datetime.strptime(str(year_month_integer), "%Y%m")
  131. # 获取原始车牌号并处理
  132. raw_che_pai_hao_he_tong = row['raw_che_pai_hao_he_tong']
  133. che_pai_hao_he_tong = get_che_pai(raw_che_pai_hao_he_tong)
  134. che_pai_he_tong_fail = che_pai_fail(raw_che_pai_hao_he_tong)
  135. # 获取替换车牌号并处理
  136. raw_che_pai_hao_ti_huan = row['raw_che_pai_hao_ti_huan']
  137. che_pai_hao_ti_huan = get_che_pai(raw_che_pai_hao_ti_huan)
  138. che_pai_ti_huan_fail = che_pai_fail(raw_che_pai_hao_ti_huan)
  139. # 获取车辆所属单位并提取第一个单位
  140. che_liang_suo_shu_dan_wei = row['che_liang_suo_shu_dan_wei']
  141. first_unit = get_first_unit(che_liang_suo_shu_dan_wei)
  142. # 获取租期并转换为整数
  143. zu_qi = row['zu_qi']
  144. zu_qi_int = int(zu_qi)
  145. # 获取替换年月并处理
  146. ti_huan_nian_yue = row['ti_huan_nian_yue']
  147. ti_huan_nian_yue_integer = int(ti_huan_nian_yue) if pd.notna(ti_huan_nian_yue) else None
  148. # 确定当前车牌号
  149. che_pai_hao = (
  150. che_pai_hao_ti_huan
  151. if ti_huan_nian_yue_integer and year_month_integer >= ti_huan_nian_yue_integer
  152. else che_pai_hao_he_tong
  153. )
  154. # 日志记录(Python 中使用 print 模拟)
  155. if pd.notna(che_pai_hao_ti_huan) and che_pai_hao_ti_huan:
  156. logger.info("{} -> {} -> {} -> {}", che_pai_hao_he_tong, year_month_integer, ti_huan_nian_yue_integer,
  157. ti_huan_nian_yue_integer and year_month_integer >= ti_huan_nian_yue_integer)
  158. # 构造结果字典
  159. result_dict = {
  160. **row.to_dict(),
  161. "che_pai_hao_he_tong": che_pai_hao_he_tong,
  162. "che_pai_he_tong_fail": che_pai_he_tong_fail,
  163. "che_pai_hao_ti_huan": che_pai_hao_ti_huan,
  164. "che_pai_ti_huan_fail": che_pai_ti_huan_fail,
  165. "first_unit": first_unit,
  166. "che_pai_hao": che_pai_hao,
  167. "year_no": local_date.strftime("%Y"),
  168. "month_no": local_date.strftime("%m"),
  169. }
  170. # 添加到结果列表
  171. result_list.append(result_dict)
  172. next_local_date = local_date
  173. # 处理后续月份
  174. for _ in range(1, zu_qi_int):
  175. next_local_date = next_local_date + relativedelta(months=1, day=1)
  176. next_month_integer = int(next_local_date.strftime("%Y%m"))
  177. # 确定当前车牌号
  178. next_che_pai_hao = (
  179. che_pai_hao_ti_huan
  180. if ti_huan_nian_yue_integer is not None and next_month_integer >= ti_huan_nian_yue_integer
  181. else che_pai_hao_he_tong
  182. )
  183. # 构造后续月份的结果字典
  184. next_result_dict = {
  185. **result_dict,
  186. "year_month": str(next_month_integer),
  187. "year_no": next_local_date.strftime("%Y"),
  188. "month_no": next_local_date.strftime("%m"),
  189. "che_pai_hao": next_che_pai_hao,
  190. }
  191. # 添加到结果列表
  192. result_list.append(next_result_dict)
  193. # 转换为 DataFrame
  194. result_df = pd.DataFrame(result_list)
  195. result_df['ti_huan_nian_yue'] = result_df['ti_huan_nian_yue'].astype('Int64')
  196. # 打印DataFrame的信息
  197. print(result_df.info())
  198. # 将处理后的数据保存到CSV文件中
  199. result_df.to_csv(path_or_buf=output_path,
  200. index=False,
  201. encoding='utf-8-sig')
  202. def data_import():
  203. # 定义 PowerShell 脚本的路径
  204. script_path = r"../../copy.ps1"
  205. # 目标表和文件信息
  206. table = "car.car_zu_lin" # 数据库目标表名
  207. # 表字段列名,用于指定导入数据的列顺序
  208. columns = "year_month,raw_che_pai_hao_he_tong,che_xing,che_liang_suo_shu_dan_wei,he_tong_ming_cheng,he_tong_bian_hao,jia_shui_he_ji_jin_e,bu_han_shui_jin_e,shui_e,zu_qi,raw_che_pai_hao_ti_huan,ti_huan_nian_yue,bei_zhu,che_pai_hao_he_tong,che_pai_he_tong_fail,che_pai_hao_ti_huan,che_pai_ti_huan_fail,first_unit,che_pai_hao,year_no,month_no"
  209. # 构造执行 PowerShell 脚本的命令
  210. command = f"powershell -File {script_path} -db_host {db_host} -db_port {db_port} -db_username {db_username} -db_password {db_password} -dbname {dbname} -table {table} -filename {output_path} -columns {columns}"
  211. # 打印生成的命令,方便调试和日志记录
  212. logger.info("command: {}", command)
  213. # 使用 subprocess 模块运行 PowerShell 命令,并捕获输出
  214. completed_process = subprocess.run(
  215. command, # 执行的命令
  216. check=False, # 如果命令执行失败,不抛出异常
  217. text=True, # 将输出作为字符串处理
  218. capture_output=True, # 捕获标准输出和标准错误
  219. )
  220. # 打印命令执行的结果,包括返回码、标准输出和标准错误
  221. logger.info("导入结果:\n{}\n{}\n{}", completed_process.returncode, completed_process.stdout,
  222. completed_process.stderr)
  223. # 定义正则表达式,用于匹配标准输出中的 COPY 结果
  224. p = re.compile(r"^(COPY) (\d+)$")
  225. count = None # 初始化计数变量
  226. matcher = p.match(completed_process.stdout) # 匹配标准输出中的 COPY 结果
  227. if matcher:
  228. count = int(matcher.group(2)) # 提取导入的数据行数
  229. # 如果没有成功提取到导入数据的行数,抛出运行时异常
  230. if count is None:
  231. raise RuntimeError("导入数据失败")
  232. def upload_file():
  233. # 获取当前日期,并计算上个月的第一天
  234. today = datetime.today()
  235. start_date = today - relativedelta(months=1, day=1)
  236. remote_path = f'{remote_dir_path}{start_date.strftime('%Y%m')}.xlsx' # 定义远程主机的目标文件路径
  237. # 使用paramiko.SSHClient创建一个SSH客户端对象,并通过with语句管理其上下文
  238. with paramiko.SSHClient() as ssh:
  239. # 设置自动添加主机密钥策略,避免因未知主机密钥导致连接失败
  240. ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  241. # 连接到远程主机,传入主机地址、端口、用户名和密码
  242. ssh.connect(ssh_hostname, port=ssh_port, username=ssh_username, password=ssh_password)
  243. # 执行远程命令,创建远程目录(如果不存在)
  244. ssh.exec_command(f'mkdir -p {remote_dir_path}')
  245. # 打开SFTP会话,用于文件传输,并通过with语句管理其上下文
  246. with ssh.open_sftp() as sftp:
  247. # 记录日志,提示即将上传的本地文件和远程目标路径
  248. logger.info("upload {} to {}", input_path, remote_path)
  249. # 使用SFTP的put方法将本地文件上传到远程主机
  250. sftp.put(input_path, remote_path)
  251. # 记录日志,提示文件已成功上传
  252. logger.info("uploaded {}", input_path)
  253. data_process()
  254. data_import()
  255. upload_file()