fix:修复一些BUG
This commit is contained in:
@@ -2,14 +2,16 @@ import shutil
|
||||
import os
|
||||
import time
|
||||
from loguru import logger
|
||||
import zipfile
|
||||
|
||||
|
||||
def export_data_folder(source_folder="data", output_folder="backup"):
|
||||
def export_templates_folder(output_folder="backup"):
|
||||
"""
|
||||
将指定文件夹压缩为 zip 包
|
||||
:param source_folder: 要压缩的文件夹路径 (默认 'data')
|
||||
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
|
||||
"""
|
||||
source_folder = "data"
|
||||
try:
|
||||
# 1. 检查源文件夹是否存在
|
||||
if not os.path.exists(source_folder):
|
||||
@@ -53,6 +55,76 @@ def export_data_folder(source_folder="data", output_folder="backup"):
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def export_data(save_dir, root_dir="."):
|
||||
"""
|
||||
导出 data 和 output 两个文件夹到同一个 zip 包中
|
||||
:param save_dir: 用户在 GUI 弹窗中选择的保存目录 (例如: D:/Backup)
|
||||
:param root_dir: 项目根目录 (用于找到 data 和 output)
|
||||
"""
|
||||
|
||||
# 1. 定义要打包的目标文件夹
|
||||
targets = ["data", "output"]
|
||||
|
||||
# 2. 检查保存目录
|
||||
if not os.path.exists(save_dir):
|
||||
logger.error(f"保存目录不存在: {save_dir}")
|
||||
return
|
||||
|
||||
# 3. 生成压缩包路径
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
zip_filename = f"完整备份_{timestamp}.zip"
|
||||
zip_path = os.path.join(save_dir, zip_filename)
|
||||
|
||||
logger.info(f"开始备份,目标文件: {zip_path}")
|
||||
|
||||
try:
|
||||
# 4. 创建压缩包 (使用 'w' 写入模式,ZIP_DEFLATED 表示压缩)
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
|
||||
has_files = False # 标记是否真的压缩了文件
|
||||
|
||||
for target in targets:
|
||||
target_abs_path = os.path.join(root_dir, target)
|
||||
|
||||
# 检查 data 或 output 是否存在
|
||||
if not os.path.exists(target_abs_path):
|
||||
logger.warning(f"⚠️ 跳过: 找不到文件夹 '{target}'")
|
||||
continue
|
||||
|
||||
logger.info(f"正在压缩: {target} ...")
|
||||
|
||||
# 5. 遍历文件夹写入 ZIP
|
||||
# os.walk 会递归遍历子文件夹
|
||||
for root, dirs, files in os.walk(target_abs_path):
|
||||
for file in files:
|
||||
# 获取文件的绝对路径
|
||||
file_abs_path = os.path.join(root, file)
|
||||
|
||||
# 【关键】计算在压缩包里的相对路径
|
||||
# 例如: D:/Project/data/images/1.jpg -> data/images/1.jpg
|
||||
arcname = os.path.relpath(file_abs_path, root_dir)
|
||||
|
||||
# 写入压缩包
|
||||
zf.write(file_abs_path, arcname)
|
||||
has_files = True
|
||||
|
||||
if has_files:
|
||||
logger.success(f"✅ 备份成功! 文件已保存至:\n{zip_path}")
|
||||
return zip_path
|
||||
else:
|
||||
logger.error("❌ 备份失败: data 和 output 文件夹均为空或不存在。")
|
||||
# 如果生成了空文件,建议删除
|
||||
if os.path.exists(zip_path):
|
||||
os.remove(zip_path)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"导出过程出错: {str(e)}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def initialize_project(root_dir="."):
|
||||
"""
|
||||
初始化项目:清空 data,重建目录,复制模板
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import time
|
||||
import pythoncom
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
@@ -12,7 +13,13 @@ from utils.agent_utils import generate_comment
|
||||
from utils.font_utils import install_fonts_from_directory
|
||||
from utils.image_utils import find_image_path
|
||||
from utils.zodiac_utils import calculate_zodiac
|
||||
from utils.growt_utils import replace_one_page, replace_two_page, replace_three_page, replace_four_page, replace_five_page
|
||||
from utils.growt_utils import (
|
||||
replace_one_page,
|
||||
replace_two_page,
|
||||
replace_three_page,
|
||||
replace_four_page,
|
||||
replace_five_page,
|
||||
)
|
||||
|
||||
|
||||
# 如果你之前没有全局定义 console,这里定义一个
|
||||
@@ -23,6 +30,7 @@ console = Console()
|
||||
# ==========================================
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
|
||||
# ==========================================
|
||||
@@ -58,8 +66,10 @@ def generate_template():
|
||||
logger.error(f"程序运行出错: {str(e)}")
|
||||
# 打印详细报错位置,方便调试
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 2. 生成评语(根据names.xlsx文件生成评价)
|
||||
# ==========================================
|
||||
@@ -83,20 +93,24 @@ def generate_comment_all():
|
||||
# 这样我们可以通过索引 i 精准地把数据写回某一行
|
||||
for i in df.index:
|
||||
name = df.at[i, "姓名"] # 获取当前行的姓名
|
||||
sex = df.at[i,"性别"]
|
||||
sex = df.at[i, "性别"]
|
||||
if pd.isna(sex):
|
||||
sex = "男"
|
||||
else:
|
||||
sex = str(sex).strip()
|
||||
|
||||
|
||||
# 健壮性处理
|
||||
if pd.isna(name): continue # 跳过空行
|
||||
if pd.isna(name):
|
||||
continue # 跳过空行
|
||||
name = str(name).strip()
|
||||
|
||||
# 获取当前行的特征(如果Excel里有“特征”这一列就读,没有就用默认值)
|
||||
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
|
||||
traits = df.at[i, "表现特征"] if "表现特征" in df.columns and not pd.isna(
|
||||
df.at[i, "表现特征"]) else "有礼貌、守纪律"
|
||||
traits = (
|
||||
df.at[i, "表现特征"]
|
||||
if "表现特征" in df.columns and not pd.isna(df.at[i, "表现特征"])
|
||||
else "有礼貌、守纪律"
|
||||
)
|
||||
|
||||
# 优化:如果“评价”列已经有内容了,跳过不生成(节省API费用)
|
||||
current_comment = df.at[i, "评价"]
|
||||
@@ -109,7 +123,9 @@ def generate_comment_all():
|
||||
try:
|
||||
# 调用你的生成函数,并【接收返回值】
|
||||
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
|
||||
generated_text = generate_comment(name, config["age_group"], traits,sex)
|
||||
generated_text = generate_comment(
|
||||
name, config["age_group"], traits, sex
|
||||
)
|
||||
|
||||
# --- 将结果写入 DataFrame ---
|
||||
df.at[i, "评价"] = generated_text
|
||||
@@ -136,8 +152,10 @@ def generate_comment_all():
|
||||
except Exception as e:
|
||||
logger.error(f"程序运行出错: {str(e)}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. 生成成长报告(根据names.xlsx文件生成)
|
||||
# ==========================================
|
||||
@@ -148,10 +166,11 @@ def generate_report():
|
||||
time.sleep(2)
|
||||
|
||||
os.makedirs(config["output_folder"], exist_ok=True)
|
||||
|
||||
# 检查模版文件是否存在
|
||||
if not os.path.exists(config["source_file"]):
|
||||
logger.info(f"错误: 找不到模版文件 {config['source_file']}")
|
||||
logger.info(f"错误: 找不到模版文件 {config["source_file"]}")
|
||||
return
|
||||
# 检查数据文件是否存在
|
||||
if not os.path.exists(config["excel_file"]):
|
||||
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
|
||||
return
|
||||
@@ -160,8 +179,18 @@ def generate_report():
|
||||
# 2. 读取数据
|
||||
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
|
||||
# 确保列名对应
|
||||
columns = ["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
|
||||
"评价"]
|
||||
columns = [
|
||||
"姓名",
|
||||
"英文名",
|
||||
"性别",
|
||||
"生日",
|
||||
"属相",
|
||||
"我的好朋友",
|
||||
"我的爱好",
|
||||
"喜欢的游戏",
|
||||
"喜欢吃的食物",
|
||||
"评价",
|
||||
]
|
||||
datas = df[columns].values.tolist()
|
||||
|
||||
teacher_names_str = " ".join(config["teachers"])
|
||||
@@ -171,13 +200,23 @@ def generate_report():
|
||||
# 3. 循环处理
|
||||
for i, row_data in enumerate(datas):
|
||||
# 解包数据
|
||||
(name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) = row_data
|
||||
(
|
||||
name,
|
||||
english_name,
|
||||
sex,
|
||||
birthday,
|
||||
zodiac,
|
||||
friend,
|
||||
hobby,
|
||||
game,
|
||||
food,
|
||||
comments,
|
||||
) = row_data
|
||||
|
||||
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
|
||||
|
||||
# 每次循环重新加载模版
|
||||
template_source_file = os.path.join("templates", config["source_file"])
|
||||
prs = Presentation(template_source_file)
|
||||
prs = Presentation(config["source_file"])
|
||||
|
||||
# --- 页面 1 ---
|
||||
replace_one_page(prs, name, config["class_name"])
|
||||
@@ -187,32 +226,47 @@ def generate_report():
|
||||
|
||||
# --- 页面 3 ---
|
||||
student_image_folder = os.path.join(config["image_folder"], name)
|
||||
logger.info(f"学生图片文件夹: {student_image_folder}")
|
||||
logger.info(f"学生:{name},图片文件夹: {student_image_folder}")
|
||||
if os.path.exists(student_image_folder):
|
||||
me_image_path = find_image_path(student_image_folder, "me_image")
|
||||
|
||||
me_image_path = find_image_path(student_image_folder, "me")
|
||||
|
||||
# 构造信息字典供 helper 使用
|
||||
info_dict = {
|
||||
"name": name, "english_name": english_name, "sex": sex,
|
||||
"birthday": birthday, "zodiac": zodiac, "friend": friend,
|
||||
"hobby": hobby, "game": game, "food": food
|
||||
"name": name,
|
||||
"english_name": english_name,
|
||||
"sex": sex,
|
||||
"birthday": birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else "",
|
||||
"zodiac": zodiac,
|
||||
"friend": friend,
|
||||
"hobby": hobby,
|
||||
"game": game,
|
||||
"food": food,
|
||||
}
|
||||
# 逻辑:必须同时满足 "不是None" 且 "是字符串" 且 "文件存在" 才能执行
|
||||
if me_image_path and isinstance(me_image_path, str) and os.path.exists(me_image_path):
|
||||
logger.info(f"学生:{name},图片路径有效: {me_image_path},正在替换...")
|
||||
if (
|
||||
me_image_path
|
||||
and isinstance(me_image_path, str)
|
||||
and os.path.exists(me_image_path)
|
||||
):
|
||||
replace_three_page(prs, info_dict, me_image_path)
|
||||
else:
|
||||
# 只有在这里打印日志,告诉用户跳过了,但不中断程序
|
||||
replace_three_page(prs, info_dict, None)
|
||||
replace_three_page(prs, info_dict, None)
|
||||
else:
|
||||
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||||
logger.warning(f"⚠️ 警告: 学生:{name},学生图片文件夹不存在 {student_image_folder}")
|
||||
|
||||
# --- 页面 4 ---
|
||||
class_image_path = find_image_path(config["image_folder"], config["class_name"])
|
||||
if os.path.exists(class_image_path):
|
||||
class_image_path = find_image_path(
|
||||
config["image_folder"], config["class_name"]
|
||||
)
|
||||
if (
|
||||
class_image_path
|
||||
and isinstance(class_image_path, str)
|
||||
and os.path.exists(class_image_path)
|
||||
):
|
||||
replace_four_page(prs, class_image_path)
|
||||
else:
|
||||
logger.warning(f"错误: 班级图片文件不存在 {class_image_path}")
|
||||
logger.warning(f"⚠️ 警告: 班级图片文件不存在 {class_image_path}")
|
||||
|
||||
# --- 页面 5 ---
|
||||
if os.path.exists(student_image_folder):
|
||||
@@ -228,7 +282,9 @@ def generate_report():
|
||||
replace_five_page(prs, img1_path, img1_path)
|
||||
# 情况C: 一张都没找到
|
||||
else:
|
||||
logger.warning(f"⚠️ 警告: {name} 缺少作品照片 (1.jpg/png 或 2.jpg/png)[/]")
|
||||
logger.warning(
|
||||
f"⚠️ 警告: {name} 缺少作品照片 (1.jpg/png 或 2.jpg/png)[/]"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||||
|
||||
@@ -242,7 +298,9 @@ def generate_report():
|
||||
prs.save(output_path)
|
||||
logger.success(f"学生:{name},保存成功: {new_filename}")
|
||||
except PermissionError:
|
||||
logger.error(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
|
||||
logger.error(
|
||||
f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。"
|
||||
)
|
||||
|
||||
logger.success("所有报告生成完毕!")
|
||||
|
||||
@@ -250,63 +308,82 @@ def generate_report():
|
||||
logger.error(f"程序运行出错: {str(e)}")
|
||||
# 打印详细报错位置,方便调试
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
||||
# ==========================================
|
||||
def batch_convert_folder(folder_path):
|
||||
"""
|
||||
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint,速度快)
|
||||
已修复多线程 CoInitialize 报错,并适配 GUI 日志
|
||||
"""
|
||||
folder_path = os.path.abspath(folder_path)
|
||||
if not os.path.exists(folder_path):
|
||||
print("文件夹不存在")
|
||||
return
|
||||
# 【核心修复 1】子线程初始化 COM 组件
|
||||
pythoncom.CoInitialize()
|
||||
|
||||
# 获取所有 ppt/pptx 文件
|
||||
files = [f for f in os.listdir(folder_path) if f.lower().endswith(('.ppt', '.pptx'))]
|
||||
|
||||
if not files:
|
||||
print("没有找到 PPT 文件")
|
||||
return
|
||||
|
||||
print(f"发现 {len(files)} 个文件,准备开始转换...")
|
||||
|
||||
powerpoint = None
|
||||
try:
|
||||
# 1. 启动应用 (只启动一次)
|
||||
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
|
||||
# 某些环境下需要设为可见,否则无法运行
|
||||
# powerpoint.Visible = 1
|
||||
folder_path = os.path.abspath(folder_path)
|
||||
if not os.path.exists(folder_path):
|
||||
logger.error(f"文件夹不存在: {folder_path}")
|
||||
return
|
||||
|
||||
for filename in files:
|
||||
ppt_path = os.path.join(folder_path, filename)
|
||||
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
|
||||
# 获取所有 ppt/pptx 文件
|
||||
files = [
|
||||
f for f in os.listdir(folder_path) if f.lower().endswith((".ppt", ".pptx"))
|
||||
]
|
||||
|
||||
# 如果 PDF 已存在,可以选择跳过
|
||||
if os.path.exists(pdf_path):
|
||||
print(f"[跳过] 已存在: {filename}")
|
||||
continue
|
||||
if not files:
|
||||
logger.warning("没有找到 PPT 文件")
|
||||
return
|
||||
|
||||
print(f"正在转换: {filename} ...")
|
||||
logger.info(f"发现 {len(files)} 个文件,准备开始转换...")
|
||||
|
||||
try:
|
||||
# 打开 -> 另存为 -> 关闭
|
||||
deck = powerpoint.Presentations.Open(ppt_path)
|
||||
deck.SaveAs(pdf_path, 32)
|
||||
deck.Close()
|
||||
except Exception as e:
|
||||
print(f"文件 {filename} 转换出错: {e}")
|
||||
powerpoint = None
|
||||
try:
|
||||
# 1. 启动应用 (只启动一次)
|
||||
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
|
||||
|
||||
# 【建议】在后台线程运行时,有时设置为不可见更稳定,
|
||||
# 但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
|
||||
# powerpoint.Visible = 1
|
||||
|
||||
for filename in files:
|
||||
ppt_path = os.path.join(folder_path, filename)
|
||||
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
|
||||
|
||||
# 如果 PDF 已存在,可以选择跳过
|
||||
if os.path.exists(pdf_path):
|
||||
logger.info(f"[跳过] 已存在: {filename}")
|
||||
continue
|
||||
|
||||
logger.info(f"正在转换: {filename} ...")
|
||||
|
||||
try:
|
||||
# 打开 -> 另存为 -> 关闭
|
||||
deck = powerpoint.Presentations.Open(ppt_path)
|
||||
deck.SaveAs(pdf_path, 32) # 32 代表 PDF 格式
|
||||
deck.Close()
|
||||
except Exception as e:
|
||||
logger.error(f"文件 {filename} 转换出错: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PowerPoint 进程启动出错: {e}")
|
||||
finally:
|
||||
# 2. 退出应用
|
||||
if powerpoint:
|
||||
try:
|
||||
powerpoint.Quit()
|
||||
except:
|
||||
pass
|
||||
logger.success("PowerPoint 已关闭,批量转换完成。")
|
||||
|
||||
except Exception as e:
|
||||
print(f"PowerPoint 进程出错: {e}")
|
||||
logger.error(f"未知错误: {e}")
|
||||
finally:
|
||||
# 2. 退出应用
|
||||
if powerpoint:
|
||||
powerpoint.Quit()
|
||||
print("PowerPoint 已关闭,批量转换完成。")
|
||||
|
||||
# 【核心修复 2】释放资源
|
||||
pythoncom.CoUninitialize()
|
||||
|
||||
# ==========================================
|
||||
# 5. 生成属相(根据names.xlsx文件生成属相)
|
||||
@@ -319,8 +396,8 @@ def generate_zodiac():
|
||||
df = pd.read_excel(excel_path, sheet_name="Sheet1")
|
||||
|
||||
# 2. 检查必要的列
|
||||
date_column = '生日'
|
||||
target_column = '属相'
|
||||
date_column = "生日"
|
||||
target_column = "属相"
|
||||
|
||||
if date_column not in df.columns:
|
||||
logger.error(f"Excel中找不到列名:【{date_column}】,请检查表头。")
|
||||
@@ -335,12 +412,12 @@ def generate_zodiac():
|
||||
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
|
||||
|
||||
# 3. 数据清洗与计算
|
||||
temp_dates = pd.to_datetime(df[date_column], errors='coerce')
|
||||
temp_dates = pd.to_datetime(df[date_column], errors="coerce")
|
||||
df[target_column] = temp_dates.apply(calculate_zodiac)
|
||||
|
||||
|
||||
# 5. 保存结果
|
||||
save_path = excel_path
|
||||
|
||||
save_path = excel_path
|
||||
|
||||
try:
|
||||
df.to_excel(save_path, index=False)
|
||||
logger.success(f"所有属相已更新并写入文件:{save_path}")
|
||||
@@ -353,6 +430,5 @@ def generate_zodiac():
|
||||
except Exception as e:
|
||||
logger.error(f"程序运行出错: {str(e)}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user