""" Excel 格式转换程序 - 带日期时间后缀版本 输出文件名自动添加日期时间避免重复 """
import os
import sys
from datetime import datetime
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext, messagebox
import threading
import openpyxl
import xlwt
from openpyxl import load_workbook
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, NamedStyle
from openpyxl.styles.numbers import FORMAT_NUMBER_COMMA_SEPARATED1
class ExcelConverterPure:
"""完全不使用 pandas 的 Excel 转换器"""
def __init__(self):
self.input_columns = ['交易时间', '交易对手户名', '交易对手账号', '交易对手行名', '借贷标志', '交易金额', '交易后余额', '币种', '交易类型', '用途', '摘要']
self.output_columns = ['交易时间', '贷方发生额/元 (收入)', '对方户名', '对方账号', '备注']
self.column_mapping = {
'交易时间': '交易时间',
'交易金额': '贷方发生额/元 (收入)',
'交易对手户名': '对方户名',
'交易对手账号': '对方账号',
'用途': '备注'
}
def convert_time_format(self, time_str):
"""转换时间格式:2026-02-02 14:58:01 → 20260202 14:58:01"""
if not time_str:
return ''
time_str = str(time_str).strip()
if not time_str:
return ''
try:
date_formats = ['%Y-%m-%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d', '%Y/%m/%d']
for fmt in date_formats:
try:
dt = datetime.strptime(time_str, fmt)
if ':' in time_str:
return dt.strftime('%Y%m%d %H:%M:%S')
else:
return dt.strftime('%Y%m%d')
except ValueError:
continue
return time_str
except Exception:
return time_str
def generate_timestamp_filename(self, original_path, file_format):
"""生成带时间戳的文件名
参数:
original_path: 原始文件路径
file_format: 文件格式 ('xls' 或 'xlsx')
返回:
str: 带时间戳的新文件路径
"""
now = datetime.now()
timestamp = now.strftime("%Y%m%d_%H%M%S")
dir_path = os.path.dirname(original_path)
base_name = os.path.basename(original_path)
if '.' in base_name:
name_without_ext, old_ext = os.path.splitext(base_name)
if name_without_ext.endswith('_converted'):
name_without_ext = name_without_ext[:-10]
else:
name_without_ext = base_name
new_filename = f"{name_without_ext}_converted_{timestamp}.{file_format}"
new_path = os.path.join(dir_path, new_filename)
return new_path
def get_unique_filename(self, file_path):
"""获取唯一的文件名,如果文件已存在则添加序号
参数:
file_path: 文件路径
返回:
str: 唯一的文件路径
"""
if not os.path.exists(file_path):
return file_path
dir_path = os.path.dirname(file_path)
base_name = os.path.basename(file_path)
name_without_ext, ext = os.path.splitext(base_name)
counter = 1
while True:
new_filename = f"{name_without_ext}_{counter}{ext}"
new_path = os.path.join(dir_path, new_filename)
if not os.path.exists(new_path):
return new_path
counter += 1
def parse_numeric_value(self, value):
"""解析数值,转换为浮点数"""
if value is None:
return 0.0
value_str = str(value).strip()
value_str = value_str.replace('¥', '').replace('¥', '').replace('$', '').replace('€', '')
value_str = value_str.replace(',', '').replace(',', '')
if not value_str:
return 0.0
try:
return float(value_str)
except ValueError:
return 0.0
def read_xlsx_file(self, filepath):
"""读取.xlsx 文件,不使用 pandas"""
try:
wb = openpyxl.load_workbook(filepath, data_only=True)
ws = wb.active
data = []
headers = []
for row in ws.iter_rows(min_row=9, max_row=9, values_only=True):
headers = [str(cell).strip() if cell else '' for cell in row]
break
missing_cols = []
for col in self.input_columns:
if col not in headers:
missing_cols.append(col)
if missing_cols:
return False, f"缺少必需的列:{', '.join(missing_cols)}", None
for row in ws.iter_rows(min_row=10, values_only=True):
row_dict = {}
for i, header in enumerate(headers):
if i < len(row):
row_dict[header] = row[i]
else:
row_dict[header] = ''
data.append(row_dict)
wb.close()
return True, f"读取成功,共{len(data)}行", data
except Exception as e:
return False, f"读取文件失败:{str(e)}", None
def save_to_xls(self, data, output_path):
"""保存数据到.xls 文件,使用 xlwt,确保数值类型正确"""
try:
workbook = xlwt.Workbook(encoding='utf-8')
worksheet = workbook.add_sheet('Sheet1')
header_style = xlwt.XFStyle()
header_font = xlwt.Font()
header_font.bold = True
header_style.font = header_font
number_style = xlwt.XFStyle()
number_format = '#,##0.00'
number_style.num_format_str = number_format
text_style = xlwt.XFStyle()
for col_idx, col_name in enumerate(self.output_columns):
worksheet.write(0, col_idx, col_name, header_style)
if col_idx == 0:
worksheet.col(col_idx).width = 256 * 20
elif col_idx == 1:
worksheet.col(col_idx).width = 256 * 18
elif col_idx == 2:
worksheet.col(col_idx).width = 256 * 25
elif col_idx == 3:
worksheet.col(col_idx).width = 256 * 20
elif col_idx == 4:
worksheet.col(col_idx).width = 256 * 30
for row_idx, row_data in enumerate(data, start=1):
time_val = row_data.get('交易时间', '')
worksheet.write(row_idx, 0, self.convert_time_format(time_val), text_style)
amount_val = row_data.get('交易金额', '')
numeric_amount = self.parse_numeric_value(amount_val)
worksheet.write(row_idx, 1, numeric_amount, number_style)
name_val = row_data.get('交易对手户名', '')
worksheet.write(row_idx, 2, str(name_val), text_style)
account_val = row_data.get('交易对手账号', '')
worksheet.write(row_idx, 3, str(account_val), text_style)
remark_val = row_data.get('用途', '')
worksheet.write(row_idx, 4, str(remark_val), text_style)
workbook.save(output_path)
return True, f"保存成功:{output_path}"
except Exception as e:
return False, f"保存文件失败:{str(e)}"
def save_to_xlsx(self, data, output_path):
"""保存数据到.xlsx 文件,使用 openpyxl,确保数值类型正确"""
try:
wb = Workbook()
ws = wb.active
ws.title = "Sheet1"
number_style = NamedStyle(name="currency_style")
number_style.number_format = FORMAT_NUMBER_COMMA_SEPARATED1
for col_idx, col_name in enumerate(self.output_columns, start=1):
cell = ws.cell(row=1, column=col_idx, value=col_name)
cell.font = Font(bold=True)
if col_idx == 1:
ws.column_dimensions['A'].width = 20
elif col_idx == 2:
ws.column_dimensions['B'].width = 18
elif col_idx == 3:
ws.column_dimensions['C'].width = 25
elif col_idx == 4:
ws.column_dimensions['D'].width = 20
elif col_idx == 5:
ws.column_dimensions['E'].width = 30
for row_idx, row_data in enumerate(data, start=2):
time_val = row_data.get('交易时间', '')
cell_time = ws.cell(row=row_idx, column=1, value=self.convert_time_format(time_val))
amount_val = row_data.get('交易金额', '')
numeric_amount = self.parse_numeric_value(amount_val)
cell_amount = ws.cell(row=row_idx, column=2, value=numeric_amount)
if numeric_amount != 0:
cell_amount.style = number_style
cell_amount.number_format = '#,##0.00'
name_val = row_data.get('交易对手户名', '')
ws.cell(row=row_idx, column=3, value=str(name_val))
account_val = row_data.get('交易对手账号', '')
ws.cell(row=row_idx, column=4, value=str(account_val))
remark_val = row_data.get('用途', '')
ws.cell(row=row_idx, column=5, value=str(remark_val))
wb.save(output_path)
return True, f"保存成功:{output_path}"
except Exception as e:
return False, f"保存文件失败:{str(e)}"
def filter_and_convert_data(self, data):
"""筛选借贷标志为'贷'的数据,并预处理数值"""
filtered_data = []
for row in data:
debit_credit = str(row.get('借贷标志', '')).strip()
if debit_credit == '贷':
if '交易金额' in row:
row['交易金额'] = self.parse_numeric_value(row['交易金额'])
filtered_data.append(row)
return filtered_data
def convert_file(self, input_path, output_path, use_timestamp, log_callback=None):
"""转换文件主函数"""
def log(msg):
if log_callback:
log_callback(msg)
try:
if not os.path.exists(input_path):
return False, f"输入文件不存在:{input_path}", 0, 0, 0
if not input_path.lower().endswith('.xlsx'):
return False, "输入文件必须是.xlsx 格式", 0, 0, 0
log("正在读取输入文件...")
success, message, data = self.read_xlsx_file(input_path)
if not success:
return False, message, 0, 0, 0
log(f"读取成功,共 {len(data)} 行数据")
log("正在筛选数据(借贷标志='贷')...")
filtered_data = self.filter_and_convert_data(data)
log(f"筛选出 {len(filtered_data)} 行符合条件的记录")
if len(filtered_data) == 0:
return False, "没有找到借贷标志为'贷'的记录", 0, 0, 0
if not output_path:
input_dir = os.path.dirname(input_path)
input_name = os.path.splitext(os.path.basename(input_path))[0]
if 'format_choice' in self.__dict__ and self.format_choice == 'xls':
ext = 'xls'
else:
ext = 'xls'
base_filename = f"{input_name}_converted.{ext}"
output_path = os.path.join(input_dir, base_filename)
if use_timestamp:
if output_path.lower().endswith('.xls'):
file_format = 'xls'
elif output_path.lower().endswith('.xlsx'):
file_format = 'xlsx'
else:
file_format = 'xls'
output_path = self.generate_timestamp_filename(output_path, file_format)
log(f"使用带时间戳的文件名:{os.path.basename(output_path)}")
output_path = self.get_unique_filename(output_path)
log(f"正在保存文件:{output_path}")
if output_path.lower().endswith('.xls'):
success, message = self.save_to_xls(filtered_data, output_path)
else:
if not output_path.lower().endswith('.xlsx'):
output_path += '.xlsx'
success, message = self.save_to_xlsx(filtered_data, output_path)
if success:
log("转换完成!")
total_amount = 0
for row in filtered_data:
if '交易金额' in row:
try:
total_amount += float(row['交易金额'])
except:
pass
log(f"总金额:{total_amount:,.2f}")
return True, output_path, len(data), len(filtered_data), total_amount
else:
return False, message, len(data), len(filtered_data), 0
except Exception as e:
return False, f"转换过程中出现错误:{str(e)}", 0, 0, 0
class ExcelConverterGUI:
"""GUI 界面"""
def __init__(self):
self.root = tk.Tk()
self.converter = ExcelConverterPure()
self.setup_ui()
def setup_ui(self):
"""设置用户界面"""
self.root.title("Excel 格式转换程序 - 带时间戳版本")
self.root.geometry("950x750")
self.root.resizable(True, True)
default_font = ('微软雅黑', 10)
self.root.option_add('*Font', default_font)
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
title_label = ttk.Label(main_frame, text="Excel 格式转换程序(带时间戳版本)", font=('微软雅黑', 16, 'bold'))
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
ttk.Label(main_frame, text="输入文件 (.xlsx):").grid(row=1, column=0, sticky=tk.W, pady=5)
self.input_path = tk.StringVar()
input_entry = ttk.Entry(main_frame, textvariable=self.input_path, width=60)
input_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5, pady=5)
input_btn = ttk.Button(main_frame, text="浏览...", command=self.browse_input)
input_btn.grid(row=1, column=2, padx=(5, 0), pady=5)
ttk.Label(main_frame, text="输出文件:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.output_path = tk.StringVar()
output_entry = ttk.Entry(main_frame, textvariable=self.output_path, width=60)
output_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), padx=5, pady=5)
output_btn = ttk.Button(main_frame, text="浏览...", command=self.browse_output)
output_btn.grid(row=2, column=2, padx=(5, 0), pady=5)
self.format_var = tk.StringVar(value="xls")
format_frame = ttk.LabelFrame(main_frame, text="输出格式设置", padding="10")
format_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
ttk.Radiobutton(format_frame, text="Excel 97-2003 (.xls)", variable=self.format_var, value="xls").pack(side=tk.LEFT, padx=10)
ttk.Radiobutton(format_frame, text="Excel 2007+ (.xlsx)", variable=self.format_var, value="xlsx").pack(side=tk.LEFT, padx=10)
self.number_format_var = tk.StringVar(value="plain")
ttk.Label(format_frame, text="数值格式:").pack(side=tk.LEFT, padx=(20, 5))
ttk.Radiobutton(format_frame, text="普通数字", variable=self.number_format_var, value="plain").pack(side=tk.LEFT, padx=5)
ttk.Radiobutton(format_frame, text="千位分隔符", variable=self.number_format_var, value="comma").pack(side=tk.LEFT, padx=5)
ttk.Radiobutton(format_frame, text="货币格式", variable=self.number_format_var, value="currency").pack(side=tk.LEFT, padx=5)
options_frame = ttk.LabelFrame(main_frame, text="文件名选项", padding="10")
options_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
self.timestamp_var = tk.BooleanVar(value=True)
timestamp_check = ttk.Checkbutton(options_frame, text="添加日期时间后缀 (避免重复)", variable=self.timestamp_var)
timestamp_check.pack(anchor=tk.W, padx=5, pady=2)
example_label = ttk.Label(options_frame, text="示例:原文件 → 输出文件_converted_20240215_143025.xls", font=('微软雅黑', 8))
example_label.pack(anchor=tk.W, padx=5, pady=2)
ttk.Label(main_frame, text="转换日志:").grid(row=5, column=0, sticky=tk.W, pady=(10, 5))
log_frame = ttk.Frame(main_frame)
log_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
self.log_text = scrolledtext.ScrolledText(log_frame, width=85, height=18, font=('Consolas', 9))
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
stats_frame = ttk.LabelFrame(main_frame, text="转换统计", padding="10")
stats_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
self.stats_text = tk.Text(stats_frame, width=85, height=4, font=('Consolas', 9))
self.stats_text.pack()
self.stats_text.insert(tk.END, "等待转换...")
self.stats_text.config(state='disabled')
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=8, column=0, columnspan=3, pady=10)
self.convert_btn = ttk.Button(button_frame, text="开始转换", command=self.start_conversion)
self.convert_btn.pack(side=tk.LEFT, padx=5)
self.clear_btn = ttk.Button(button_frame, text="清空日志", command=self.clear_log)
self.clear_btn.pack(side=tk.LEFT, padx=5)
self.preview_btn = ttk.Button(button_frame, text="预览文件名", command=self.preview_filename)
self.preview_btn.pack(side=tk.LEFT, padx=5)
self.exit_btn = ttk.Button(button_frame, text="退出", command=self.root.quit)
self.exit_btn.pack(side=tk.LEFT, padx=5)
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.grid(row=10, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0))
main_frame.rowconfigure(6, weight=1)
def log(self, message):
"""添加日志消息"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_message = f"[{timestamp}] {message}\n"
self.log_text.insert(tk.END, log_message)
self.log_text.see(tk.END)
self.root.update_idletasks()
def update_stats(self, message):
"""更新统计信息"""
self.stats_text.config(state='normal')
self.stats_text.delete(1.0, tk.END)
self.stats_text.insert(tk.END, message)
self.stats_text.config(state='disabled')
def clear_log(self):
"""清空日志"""
self.log_text.delete(1.0, tk.END)
self.log("日志已清空")
self.update_stats("等待转换...")
def browse_input(self):
"""浏览输入文件"""
filepath = filedialog.askopenfilename(
title="选择输入文件",
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")])
if filepath:
self.input_path.set(filepath)
self.log(f"选择输入文件:{filepath}")
if not self.output_path.get():
self.generate_output_filename()
def browse_output(self):
"""浏览输出文件"""
ext = self.format_var.get()
filetypes = [("Excel 文件", f"*.{ext}"), ("所有文件", "*.*")]
filepath = filedialog.asksaveasfilename(
title="选择输出文件",
defaultextension=f".{ext}",
filetypes=filetypes)
if filepath:
self.output_path.set(filepath)
self.log(f"选择输出文件:{filepath}")
def generate_output_filename(self):
"""生成输出文件名"""
input_path = self.input_path.get()
if not input_path:
return
input_dir = os.path.dirname(input_path)
input_name = os.path.splitext(os.path.basename(input_path))[0]
ext = self.format_var.get()
if self.timestamp_var.get():
now = datetime.now()
timestamp = now.strftime("%Y%m%d_%H%M%S")
base_filename = f"{input_name}_converted_{timestamp}.{ext}"
else:
base_filename = f"{input_name}_converted.{ext}"
output_file = os.path.join(input_dir, base_filename)
self.output_path.set(output_file)
self.log(f"自动生成输出文件名:{base_filename}")
def preview_filename(self):
"""预览文件名生成效果"""
input_path = self.input_path.get()
if not input_path:
messagebox.showinfo("预览", "请先选择输入文件")
return
input_name = os.path.splitext(os.path.basename(input_path))[0]
ext = self.format_var.get()
if self.timestamp_var.get():
now = datetime.now()
timestamp = now.strftime("%Y%m%d_%H%M%S")
example_filename = f"{input_name}_converted_{timestamp}.{ext}"
message = f"启用时间戳后的文件名示例:\n\n{example_filename}\n\n格式说明:\n• 输入文件名:{input_name}\n• 时间戳:{timestamp}\n• 格式:{ext}"
else:
example_filename = f"{input_name}_converted.{ext}"
message = f"禁用时间戳后的文件名示例:\n\n{example_filename}\n\n注意:如果文件已存在会被覆盖!"
messagebox.showinfo("文件名预览", message)
def start_conversion(self):
"""开始转换"""
input_path = self.input_path.get().strip()
output_path = self.output_path.get().strip()
if not input_path:
messagebox.showerror("错误", "请选择输入文件!")
return
if not os.path.exists(input_path):
messagebox.showerror("错误", "输入文件不存在!")
return
if not output_path:
messagebox.showerror("错误", "请选择或输入输出文件路径!")
return
self.convert_btn.config(state='disabled')
self.clear_btn.config(state='disabled')
self.preview_btn.config(state='disabled')
self.progress.start()
self.status_var.set("正在转换...")
self.clear_log()
self.converter.format_choice = self.format_var.get()
thread = threading.Thread(
target=self.run_conversion,
args=(input_path, output_path),
daemon=True)
thread.start()
def run_conversion(self, input_path, output_path):
"""在后台线程中运行转换"""
try:
success, message, total_rows, filtered_rows, total_amount = self.converter.convert_file(
input_path, output_path, self.timestamp_var.get(),
self.log)
self.root.after(0, self.conversion_finished, success, message, total_rows, filtered_rows, total_amount)
except Exception as e:
self.root.after(0, self.conversion_failed, str(e))
def conversion_finished(self, success, message, total_rows, filtered_rows, total_amount):
"""转换完成"""
self.progress.stop()
self.convert_btn.config(state='normal')
self.clear_btn.config(state='normal')
self.preview_btn.config(state='normal')
if success:
self.status_var.set("转换完成")
stats_text = f"转换统计:\n"
stats_text += f" 总行数:{total_rows}\n"
stats_text += f" 转换行数:{filtered_rows}\n"
if total_rows > 0:
stats_text += f" 转换率:{filtered_rows / total_rows * 100:.1f}%\n"
stats_text += f" 总金额:{total_amount:,.2f}\n"
stats_text += f" 输出文件:{os.path.basename(message)}"
self.update_stats(stats_text)
self.log("=" * 50)
self.log("转换成功!")
self.log(f"输出文件:{message}")
self.log(f"处理统计:{filtered_rows}/{total_rows} 行")
self.log(f"总金额:{total_amount:,.2f}")
self.log("=" * 50)
if messagebox.askyesno("转换完成", f"转换成功!\n\n" f"输出文件:{os.path.basename(message)}\n" f"总行数:{total_rows}\n" f"转换行数:{filtered_rows}\n" f"总金额:{total_amount:,.2f}\n\n" f"是否打开输出文件?"):
try:
if sys.platform == 'win32':
os.startfile(message)
elif sys.platform == 'darwin':
import subprocess
subprocess.call(['open', message])
else:
import subprocess
subprocess.call(['xdg-open', message])
except Exception as e:
self.log(f"无法打开文件:{e}")
else:
self.status_var.set("转换失败")
self.log("=" * 50)
self.log(f"转换失败:{message}")
self.log("=" * 50)
messagebox.showerror("转换失败", f"转换过程中出现错误:\n\n{message}")
def conversion_failed(self, error_msg):
"""转换失败"""
self.progress.stop()
self.convert_btn.config(state='normal')
self.clear_btn.config(state='normal')
self.preview_btn.config(state='normal')
self.status_var.set("转换失败")
self.log("=" * 50)
self.log(f"转换过程异常:{error_msg}")
self.log("=" * 50)
messagebox.showerror("转换异常", f"转换过程中出现异常:\n\n{error_msg}")
def run(self):
"""运行 GUI"""
self.root.mainloop()
''' def check_dependencies():
"""检查依赖库"""
missing_deps = []
# 检查 openpyxl
try:
import openpyxl
print("✓ openpyxl 已安装")
except ImportError:
missing_deps.append("openpyxl")
print("✗ openpyxl 未安装")
# 检查 xlwt
try:
import xlwt
print("✓ xlwt 已安装")
except ImportError:
missing_deps.append("xlwt")
print("✗ xlwt 未安装")
if missing_deps:
print(f"\n缺少依赖库:{', '.join(missing_deps)}")
print("请运行以下命令安装:")
print(f"pip install {' '.join(missing_deps)}")
# 询问是否自动安装
response = input("\n是否自动安装缺失的库?(y/n): ").lower().strip()
if response == 'y':
import subprocess
for dep in missing_deps:
print(f"正在安装 {dep}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", dep])
print("安装完成!")
else:
print("请手动安装依赖库后重新运行程序。")
return False
return True
'''
def main():
"""主函数"""
print("=" * 50)
print("Excel 格式转换程序 - 带时间戳版本")
print("=" * 50)
print("功能:")
print(" • 读取.xlsx 文件(第 9 行标题,第 10 行开始数据)")
print(" • 筛选借贷标志为'贷'的记录")
print(" • 转换时间格式:2026-02-02 14:58:01 → 20260202 14:58:01")
print(" • 确保数值型数据正确保存(非文本型)")
print(" • 文件名自动添加日期时间后缀避免重复")
print(" • 格式:原文件名_converted_YYYYMMDD_HHMMSS.xls")
print("=" * 50)
''' # 检查依赖
if not check_dependencies():
return
'''
print("\n启动 GUI 界面...")
app = ExcelConverterGUI()
app.run()
if __name__ == "__main__":
main()