
背景需求
马上开学了,小班升中班,中班升大班,都要换园区了,所以要重新制作被子卡的园区班级。而新来的托班、小班孩子也需要新的被子卡、接送卡、床贴。
但是电脑坏了,编程资料导不出来,幸好在CSDN上备份了代码部分。

这次我需要复制代码,并重新制作可编辑圆形挂牌的Word模版。
一、代码部分复制
代码部分参考最新的“有效期”的一份
D:\test\办公类里面设置代码



取名"01圆形挂牌_有效期_1份和2份.py"
把“有效期”那份代码贴进去。

修改日期


修改读取和导出路径

修改后

二、Word模版部分
之前用的 Word插入背景图片的方法,生成了PDF(打印可以看见后面的圆形图片),但是如果教师自己修改信息后按打印,看见的就是没有圆边框,只有文字的效果

虽然我在文件名上标注了打印方法,但是教师一年就打印1、2次,根本不会记得复杂的打印设置流程

这次正好要重做模版,我就想试试能不能不要用“Word背景”,其他添加圆框的方法。
(一)制作基础样式












作品效果

(二)添加圆形框



方法1:浮于文字上方或下方



无论是浮于文字上方还是下方,运行后置只有文字,没有看到圆形

方法2:表格-底纹-填充效果


没有看到填充效果

换成WPS








搞不出来,先调试代码
三、代码调试
运行上次CSDN 保存的有效期的程序,文字部分都对的,就是单元格框变窄了。




修改后,确保单元格都是9.4*9.4CM 方形

四、继续制作Word
方案3:制作文本框,覆盖在单元格上层



摆放到格子里

设置透明色,黑色边框















测试看看效果



不死心,把所有格子都布满






但是看看所有表格,也都是最后一页没有圆圈


我尝试把班级人数增加,看看是否能确保托1班第4页有圆形框。


改成24人,还是没有框


改成25人,第5页,还是没有框(因为锁定托班最多20人)


五、代码调试
我把模版上传,提交代码,再问为什么最后一个文本框不见了









我看看零时文件夹里,检查单独生成的表单

这四份docx,都有圆形边框

说明不是人数的问题,还是代码的问题。








结果一样,还是最后一页没有圆形边框

因为单元格里面能设置的都设置了,现在我再打开“文本框”,看看有没有可以修改的——看到“图片布局”-后面有一个“查看更多”

“查看更多”-布局-位置,有一个允许重复。我试着取消勾选。再测试


一页一个名字,


一页两个名字



对比中5班,因为写全了30条园区和班级,所以圆卡里也有信息


六、最终效果
(1)把模版里面的单元格框线取消。





# -*- coding:utf-8 -*-
'''
制作被子圆牌(word合并)多个班级-凑满整夜,如托班20人,凑满6*4页、中班30人,凑满5页,不存在的名字都是"张三"占位用,以后有插班生就可以自己修改名字了
预先再word里面页面背景-纹理,插入A4大小的图片背景(有6个园),
一个人一个牌子(被子圆牌、接送牌)
增加有效日期(2026.9-2027.6)
deepseek,豆包 阿夏
20260901
修改:支持同时生成COPIES_PER_NAME=1和2两套文件,统一保存到可编辑目录
'''
import os
import pandas as pd
from docx import Document
from docx.shared import Cm
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.shared import Pt # 用于设置字体大小
from docx.oxml.ns import qn # 用于设置中文字体
from pypinyin import pinyin, Style
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT
from docx.shared import RGBColor # 用于设置字体颜色
import shutil
import time
import re
# ========== 1. 核心配置(仅修改此部分路径) ==========
date='2026.9-2027.6'
school='XXX幼'
# 同时生成1份和2份两套文件
COPIES_PER_NAME_LIST = [1, 2]
base_dir = r'D:\Python最终内容\20260901圆形挂牌'
excel_file = os.path.join(base_dir, "01 20260805班级信息表.xlsx") # Excel数据路径
# excel_file = os.path.join(base_dir, "班级信息表(测试).xlsx") # 测试用Excel
# template_word = os.path.join(base_dir, "02 圆形挂牌模版.docx") # 现有3行2列表格模板
template_word = os.path.join(base_dir, "02 圆形挂牌模版_无框线.docx") # 现有3行2列表格模板
root_output_dir = os.path.join(base_dir, "零时文件夹") # 临时根目录
editable_dir = os.path.join(base_dir, f"03 {date[:4]}{int(date[5:6]):02}圆形挂牌(word编辑)" ) # 统一可编辑文件夹
os.makedirs(editable_dir, exist_ok=True) # 创建可编辑文件夹
# 定义各年级的基础固定人数(不含份数,份数动态乘)
GRADE_BASE_SIZES = {
"托": 24, # 托班基础24人(凑满4张)
"小": 30, # 小班基础30人(5张)
"中": 30, # 中班基础30人(5张)
"大": 36 # 大班基础36人(6张)
}
# ========== 2. Excel数据读取与拼音生成(保留原有核心逻辑,未修改) ==========
def read_excel_and_generate_pinyin(file_path):
"""读取Excel数据,生成带声调的姓名拼音,返回{班级名: 学生数据列表}"""
# 特殊姓氏读音校正(确保拼音准确性)
SPECIAL_SURNAMES = {
"乐": "Yuè", "单": "Shàn", "解": "Xiè", "查": "Zhā",
"盖": "Gě", "仇": "Qiú", "种": "Chóng", "朴": "Piáo",
"翟": "Zhái", "区": "Ōu", "繁": "Pó", "覃": "Qín",
"召": "Shào", "华": "Huà", "纪": "Jǐ", "曾": "Zēng",
"缪": "Miào", "员": "Yùn", "车": "Chē", "过": "Guō",
"尉": "Yù", "万": "Wàn"
}
COMPOUND_SURNAMES = {
"欧阳": "Ōu yáng", "上官": "Shàng guān", "皇甫": "Huáng fǔ",
"尉迟": "Yù chí", "万俟": "Mò qí", "长孙": "Zhǎng sūn",
"司徒": "Sī tú", "司空": "Sī kōng", "司徒": "Sī tú",
"诸葛": "Zhū gě", "东方": "Dōng fāng", "独孤": "Dú gū",
"慕容": "Mù róng", "宇文": "Yǔ wén"
}
def correct_name_pinyin(name):
"""生成带声调的姓名拼音(姓氏校正)"""
if not name or not isinstance(name, str):
return ""
name = name.strip()
# 优先处理复姓
for compound_surn in COMPOUND_SURNAMES:
if name.startswith(compound_surn):
surn_pinyin = COMPOUND_SURNAMES[compound_surn]
given_name = name[len(compound_surn):]
given_pinyin = ' '.join([p[0] for p in pinyin(given_name, style=Style.TONE)])
return f"{surn_pinyin} {given_pinyin}"
# 处理单姓多音字
first_char = name[0]
if first_char in SPECIAL_SURNAMES:
surn_pinyin = SPECIAL_SURNAMES[first_char]
given_name = name[1:]
given_pinyin = ' '.join([p[0] for p in pinyin(given_name, style=Style.TONE)])
return f"{surn_pinyin} {given_pinyin}"
# 普通姓名拼音
return ' '.join([p[0] for p in pinyin(name, style=Style.TONE)])
try:
# 读取所有工作表
excel = pd.ExcelFile(file_path)
sheet_names = excel.sheet_names
class_data = {}
for sheet_name in sheet_names:
# 跳过非班级工作表(如果有的话)
if not any(keyword in sheet_name for keyword in ["班", "class", "Class"]):
continue
df = pd.read_excel(file_path, sheet_name=sheet_name, usecols=range(5), header=None, dtype={0: str})
df = df.iloc[1:] # 跳过第1行标题
student_list = []
for idx, row in df.iterrows():
# 过滤无效数据(学号为空、姓名为空)
raw_id = str(row[0]).strip() if pd.notna(row[0]) else ""
name = str(row[3]).strip() if pd.notna(row[3]) else ""
if not raw_id.isdigit() or not name: # 如果没有学号,没有名字
name='张三'
elif not name: # 如果有学号,没有名字
name='张三'
# 提取核心信息
student_info = {
"campus": f"{school}-" + (str(row[1]).strip() if pd.notna(row[1]) else ""), # 园区
"class_name": str(row[2]).strip() if pd.notna(row[2]) else "", # 班级
"student_id": f"{(raw_id)}号", # 学号(如:1号)
"name": name, # 姓名
"name_pinyin": correct_name_pinyin(name), # 带声调拼音
"date": date # 添加日期字段
}
student_list.append(student_info)
if student_list:
print(f"读取[{sheet_name}]:{len(student_list)}个有效学生")
class_data[sheet_name] = student_list
else:
print(f"工作表[{sheet_name}]无有效学生数据")
return class_data
except Exception as e:
print(f"Excel读取失败:{str(e)}")
return {}
def get_grade_size(class_name, copies):
"""根据班级名称+份数获取对应的目标总挂牌数"""
if "托" in class_name:
return GRADE_BASE_SIZES["托"] * copies
elif "小" in class_name:
return GRADE_BASE_SIZES["小"] * copies
elif "中" in class_name:
return GRADE_BASE_SIZES["中"] * copies
elif "大" in class_name:
return GRADE_BASE_SIZES["大"] * copies
else:
# 默认返回小班人数
return GRADE_BASE_SIZES["小"] * copies
def expand_student_list(student_list, target_size, class_name, copies):
"""扩展学生列表到目标大小,用空学生填充(适配动态份数)"""
# 实际需要的名字数量 = 目标总挂牌数 / 每份数
name_count_needed = target_size // copies
if len(student_list) >= name_count_needed:
return student_list[:name_count_needed] # 如果实际学生多于目标,截取前name_count_needed个
# 扩展学生列表
expanded_list = student_list.copy()
# 获取园区和班级名称(从第一个有效学生获取)
campus = student_list[0]["campus"] if student_list else f"{school}"
class_name_full = student_list[0]["class_name"] if student_list else class_name
# 添加空学生(张三占位)
for i in range(len(student_list), name_count_needed):
empty_student = {
"campus": campus,
"class_name": class_name_full,
"student_id": f"{i+1}号",
"name": "张三", # 空姓名用张三占位
"name_pinyin": "Zhang San", # 对应拼音
"date": date # 有效日期
}
expanded_list.append(empty_student)
return expanded_list
def duplicate_students_for_copies(student_list, copies):
"""为每个学生复制指定份数(动态传1/2)"""
duplicated_list = []
for student in student_list:
for i in range(copies):
duplicated_list.append(student.copy())
return duplicated_list
# ========== 3. 写入Word表格(适配动态份数,保留原有字体/行间距/样式) ==========
def write_student_to_word(student_list, template_path, output_path, class_name, copies):
"""
将学生数据写入Word模板的3行2列表格
适配动态份数copies,保留原有:字体样式、行间距、颜色、单元格垂直居中
单个单元格内容:园区\n班级\n学号\n拼音\n姓名\n有效期
"""
# 根据班级类型+份数确定目标总挂牌数
target_size = get_grade_size(class_name, copies)
print(f"班级 {class_name}({copies}份版)目标挂牌数: {target_size}, 实际学生数: {len(student_list)}")
# 扩展学生列表到目标名字数(适配份数)
expanded_student_list = expand_student_list(student_list, target_size, class_name, copies)
# 为每个名字复制指定份数
final_student_list = duplicate_students_for_copies(expanded_student_list, copies)
print(f"班级 {class_name}({copies}份版)复制后总挂牌数: {len(final_student_list)}")
# 按6个学生一组分割(3行2列表格可容纳6个学生)
student_groups = [final_student_list[i:i+6] for i in range(0, len(final_student_list), 6)]
generated_files = [] # 存储生成的文件路径
for group_idx, group in enumerate(student_groups, 1):
# 复制模板文件(避免修改原模板,保留背景图)
temp_template = os.path.join(output_path, f"temp_{class_name}_{copies}份_{group_idx}.docx")
shutil.copy2(template_path, temp_template)
# 打开复制后的模板,操作表格
doc = Document(temp_template)
# 获取模板中的第一个表格(需确保"挂牌.docx"的第一个表格是3行2列")
if not doc.tables:
print(f"模板{template_path}中未找到表格,跳过此组")
os.remove(temp_template)
continue
table = doc.tables[0]
# 清除表格原有内容(保留表格结构和模板背景)
for row in table.rows:
for cell in row.cells:
for para in cell.paragraphs:
p = para._element
p.getparent().remove(p)
cell._element.clear() # 完全清空单元格
# 定义字体样式(完全保留原有逻辑)
def set_paragraph_style(para, text, font_size, is_bold=False, is_gray=True, underline=False):
"""设置段落的字体、大小、对齐(居中)和颜色"""
if not text: # 跳过空文本
return
run = para.add_run(text)
# 设置中文字体为微软雅黑
run.font.name = '微软雅黑'
run.element.rPr.rFonts.set(qn('w:eastAsia'), '微软雅黑')
run.font.size = Pt(font_size)
run.font.bold = is_bold
run.font.underline = underline # 设置下划线
# 设置颜色:灰色或黑色
if is_gray:
run.font.color.rgb = RGBColor(169, 169, 169) # DarkGray
else:
run.font.color.rgb = RGBColor(0, 0, 0) # 黑色
para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # 文字居中
# 逐个单元格写入学生信息(3行2列,共6个单元格)
cell_index = 0 # 单元格索引(0-5对应3行2列的6个单元格)
for row_idx, row in enumerate(table.rows):
for cell_idx, cell in enumerate(row.cells):
if cell_index >= len(group):
break # 学生不足6个时,空单元格跳过
# 单元格垂直居中(保留原有逻辑)
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
# 当前学生的6种信息(按原有顺序)
student = group[cell_index]
info_lines = [
student["campus"], # 第1行:园区
student["class_name"], # 第2行:班级
student["student_id"], # 第3行:学号
student["name_pinyin"], # 第4行:拼音(10pt)
student["name"], # 第5行:姓名(30pt,加粗)
student["date"] # 第6行:有效期
]
# 向单元格写入内容(完全保留原有行间距、字体大小)
for line_idx, line in enumerate(info_lines):
if line: # 只写入非空内容
para = cell.add_paragraph()
if line_idx == 0: # 园区:16pt
set_paragraph_style(para, line, font_size=16, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(45)
elif line_idx == 1: # 班级:30pt
set_paragraph_style(para, line, font_size=30, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(35)
elif line_idx == 2: # 学号:50pt 黑色下划线
set_paragraph_style(para, line, font_size=50, is_bold=True, is_gray=False, underline=True)
para.paragraph_format.line_spacing = Pt(65)
elif line_idx == 3: # 拼音行:10pt
set_paragraph_style(para, line, font_size=10, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(20)
elif line_idx == 4: # 姓名行:30pt,加粗
set_paragraph_style(para, line, font_size=30, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(35)
elif line_idx == 5: # 有效期:8pt
set_paragraph_style(para, line, font_size=8, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(15)
cell_index += 1
# =====新增下面这段代码=====
# 强制固定表格宽度9.4cm,关闭自动适配
table.autofit = False
table.width = Cm(9.4)
# ==========================
# 保存当前组的Word文件(带份数标识)
final_word_path = os.path.join(output_path, f"{class_name}_{copies}份_挂牌_{group_idx:02d}.docx")
doc.save(final_word_path)
generated_files.append(final_word_path) # 添加到生成文件列表
os.remove(temp_template) # 删除临时模板
print(f"生成{copies}份版第{group_idx}组Word:{os.path.basename(final_word_path)}")
return generated_files
# ========== 4. Word合并函数(保留原有win32+备用双方案,适配份数) ==========
def merge_docx_files_with_win32(template_file, files_list, output_filename):
"""使用win32com合并Word文档,去掉换页符(保留原有逻辑)"""
if not files_list:
print("没有文件可合并")
return
word = None
doc = None
try:
# 启动Word应用程序
import win32com.client as win32
word = win32.Dispatch("Word.Application")
word.Visible = False # 不显示Word界面
word.DisplayAlerts = False # 不显示警告
# 复制模板文件到输出文件
shutil.copy2(template_file, output_filename)
# 打开输出文件
doc = word.Documents.Open(output_filename)
# 将光标移动到文档末尾
selection = word.Selection
selection.EndKey(6) # wdStory = 6, 移动到文档末尾
# 逐个插入其他文件的内容(跳过第一个文件,因为已经是模板)
for i, file_path in enumerate(files_list):
if i == 0: # 跳过模板文件本身
continue
print(f"正在插入文件: {os.path.basename(file_path)}")
# 插入文件内容
selection.InsertFile(file_path)
# 移动到文档末尾准备插入下一个文件
selection.EndKey(6)
time.sleep(1) # 添加延时,确保Word有足够时间处理
# 保存合并后的文档
doc.Save()
print(f"已合并所有文档到:{output_filename}")
except Exception as e:
print(f"合并文档时出错:{e}")
# 尝试使用备选方法
try:
print("尝试使用备选合并方法...")
merge_docx_files_simple(template_file, files_list, output_filename)
except Exception as e2:
print(f"备选方法也失败:{e2}")
finally:
# 确保正确关闭文档和Word应用程序
try:
if doc:
doc.Close(False)
time.sleep(0.5)
except:
pass
try:
if word:
word.Quit()
time.sleep(0.5)
except:
pass
def merge_docx_files_simple(template_file, files_list, output_filename):
"""简单的合并方法:复制模板,然后逐个追加内容(保留原有逻辑)"""
# 复制模板文件
shutil.copy2(template_file, output_filename)
# 打开模板文件
main_doc = Document(output_filename)
# 逐个添加其他文件的内容
for i, file_path in enumerate(files_list):
if i == 0: # 跳过模板文件本身
continue
# 添加分页符
if i > 1: # 从第二个文件开始添加分页符
main_doc.add_page_break()
# 读取要添加的文件
sub_doc = Document(file_path)
# 复制表格内容
for table in sub_doc.tables:
# 创建新表格
new_table = main_doc.add_table(rows=len(table.rows), cols=len(table.columns))
# 复制内容和格式
for row_idx, row in enumerate(table.rows):
for col_idx, cell in enumerate(row.cells):
new_cell = new_table.cell(row_idx, col_idx)
# 清空新单元格
for paragraph in new_cell.paragraphs:
p = paragraph._element
p.getparent().remove(p)
new_cell._element.clear()
# 复制内容
for paragraph in cell.paragraphs:
new_para = new_cell.add_paragraph()
for run in paragraph.runs:
new_run = new_para.add_run(run.text)
# 复制字体样式
new_run.font.name = run.font.name
new_run.font.size = run.font.size
new_run.font.bold = run.font.bold
new_run.font.underline = run.font.underline
if hasattr(run.font.color, 'rgb'):
new_run.font.color.rgb = run.font.color.rgb
# 保存合并后的文档
main_doc.save(output_filename)
print(f"使用简单方法合并完成:{output_filename}")
def cleanup_directory(directory_path):
"""清理目录(保留原有逻辑)"""
try:
if os.path.exists(directory_path):
shutil.rmtree(directory_path)
print(f"已删除临时目录: {directory_path}")
except Exception as e:
print(f"删除目录时出错: {e}")
# ========== 5. 主程序(核心修改:循环处理1/2份,统一保存) ==========
if __name__ == "__main__":
# 步骤1:读取Excel数据(含拼音生成)
print("=== 开始读取Excel数据 ===")
class_data = read_excel_and_generate_pinyin(excel_file)
if not class_data:
print("无有效学生数据,程序退出")
exit()
# 步骤2:循环处理1份和2份版(核心修改)
all_merged_files = []
for copies in COPIES_PER_NAME_LIST:
print(f"\n" + "="*50)
print(f"开始处理【{copies}份版】所有班级")
print("="*50)
# 为当前份数创建独立临时目录,避免文件冲突
output_dir = os.path.join(root_output_dir, f"{copies}份版")
os.makedirs(output_dir, exist_ok=True)
# 为每个班级创建单独的临时子目录
class_output_dirs = {}
for class_name in class_data.keys():
class_dir = os.path.join(output_dir, class_name)
os.makedirs(class_dir, exist_ok=True)
class_output_dirs[class_name] = class_dir
# 遍历每个班级处理
for sheet_name, student_list in class_data.items():
# 获取班级名称(取第一个学生的班级信息)
class_name = student_list[0]["class_name"] if student_list else sheet_name
print(f"\n=== 处理{class_name}({copies}份版)===")
# 步骤3:写入Word(适配当前份数,保留所有原有样式)
generated_files = write_student_to_word(
student_list=student_list,
template_path=template_word,
output_path=class_output_dirs[sheet_name],
class_name=class_name,
copies=copies # 传入当前份数
)
# 步骤4:为每个班级合并所有生成的docx文件
if generated_files:
# 使用第一个文件作为模板
template_file = generated_files[0]
# 合并后的文件保存到统一可编辑文件夹,文件名带【1块/2块】标识
merged_filename = os.path.join(
editable_dir,
f"04_{date[:4]}{int(date[5:6]):02}_{class_name}_{copies}块圆形牌(接送&被子&床卡)_文件 选项 打印 打印文档的附加信息 打印背景色和图像.docx"
)
print(f"开始合并{class_name}({copies}份版)的文档...")
try:
merge_docx_files_with_win32(template_file, generated_files, merged_filename)
all_merged_files.append(merged_filename)
print(f"{class_name}({copies}份版)合并完成")
except Exception as e:
print(f"{class_name}({copies}份版)的win32合并失败,尝试简单方法: {e}")
try:
merge_docx_files_simple(template_file, generated_files, merged_filename)
all_merged_files.append(merged_filename)
print(f"{class_name}({copies}份版)使用简单方法合并完成")
except Exception as e2:
print(f"{class_name}({copies}份版)的所有合并方法都失败: {e2}")
# 处理完一个班级后稍作延时,释放资源
time.sleep(2)
# 步骤5:清理所有临时文件根目录
print("\n=== 清理所有临时文件 ===")
cleanup_directory(root_output_dir)
# 步骤6:输出最终结果
print(f"\n=== 所有处理完成 ===")
print(f"所有1/2份版文件统一保存到:{editable_dir}")
print(f"生成的合并文件列表:")
for file in all_merged_files:
print(f" - {os.path.basename(file)}")








最后生成1份和2份两款Word,由于这个可以编辑,而且是Word,打印比较方便,所以不做PDF。




都是可以编辑的


如果小2班用一个学期,到明年这个时候,可以直接批量把小2替换成中2,




把“二分园”替换为“总园”




把有效日期起止日期也替换


后续改良
1.在线的“班级信息表”的名单列拉长





2、只要生成1份就行了,如果有插班生,另外做“长方形+圆牌”合并一页的单独版本。
3、长方形挂牌生成时间只要10秒,但是圆牌时间很长(有图片,质量大)所以想计算生成19个班的圆牌Word需要多少时间。
生成1份圆牌,用时3份30秒左右

4、打印时,不再需要“文件-选项-打印-打印文档的附加信息-打印背景色和图像”

修改文件名



以下是最终代码:
# -*- coding:utf-8 -*-
'''
制作被子圆牌(word合并)多个班级-凑满整夜,如托班20人,凑满6*4页、中班30人,凑满5页,不存在的名字都是"张三"占位用,以后有插班生就可以自己修改名字了
预先再word里面页面背景-纹理,插入A4大小的图片背景(有6个园),
一个人一个牌子(被子圆牌、接送牌)
增加有效日期(2026.9-2027.6)
deepseek,豆包 阿夏
20260901
修改:支持同时生成COPIES_PER_NAME=1和2两套文件,统一保存到可编辑目录
'''
import os
import pandas as pd
from docx import Document
from docx.shared import Cm
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.shared import Pt # 用于设置字体大小
from docx.oxml.ns import qn # 用于设置中文字体
from pypinyin import pinyin, Style
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT
from docx.shared import RGBColor # 用于设置字体颜色
import shutil
import time
import re
# ========== 记录程序开始时间 ==========
import time
start_time = time.time()
# ========== 1. 核心配置(仅修改此部分路径) ==========
date='2026.9-2027.6'
school='XXX幼'
# 同时生成1份和2份两套文件
COPIES_PER_NAME_LIST = [1, 2]
COPIES_PER_NAME_LIST = [1]
base_dir = r'D:\Python最终内容\20260901圆形挂牌'
excel_file = os.path.join(base_dir, "01 20260805班级信息表.xlsx") # Excel数据路径
# excel_file = os.path.join(base_dir, "班级信息表(测试).xlsx") # 测试用Excel
# template_word = os.path.join(base_dir, "02 圆形挂牌模版.docx") # 现有3行2列表格模板
template_word = os.path.join(base_dir, "02 圆形挂牌模版_无框线.docx") # 现有3行2列表格模板
root_output_dir = os.path.join(base_dir, "零时文件夹") # 临时根目录
editable_dir = os.path.join(base_dir, f"03 {date[:4]}{int(date[5:6]):02}圆形挂牌(word编辑)" ) # 统一可编辑文件夹
os.makedirs(editable_dir, exist_ok=True) # 创建可编辑文件夹
# 定义各年级的基础固定人数(不含份数,份数动态乘)
GRADE_BASE_SIZES = {
"托": 24, # 托班基础24人(凑满4张)
"小": 30, # 小班基础30人(5张)
"中": 30, # 中班基础30人(5张)
"大": 36 # 大班基础36人(6张)
}
# ========== 2. Excel数据读取与拼音生成(保留原有核心逻辑,未修改) ==========
def read_excel_and_generate_pinyin(file_path):
"""读取Excel数据,生成带声调的姓名拼音,返回{班级名: 学生数据列表}"""
# 特殊姓氏读音校正(确保拼音准确性)
SPECIAL_SURNAMES = {
"乐": "Yuè", "单": "Shàn", "解": "Xiè", "查": "Zhā",
"盖": "Gě", "仇": "Qiú", "种": "Chóng", "朴": "Piáo",
"翟": "Zhái", "区": "Ōu", "繁": "Pó", "覃": "Qín",
"召": "Shào", "华": "Huà", "纪": "Jǐ", "曾": "Zēng",
"缪": "Miào", "员": "Yùn", "车": "Chē", "过": "Guō",
"尉": "Yù", "万": "Wàn"
}
COMPOUND_SURNAMES = {
"欧阳": "Ōu yáng", "上官": "Shàng guān", "皇甫": "Huáng fǔ",
"尉迟": "Yù chí", "万俟": "Mò qí", "长孙": "Zhǎng sūn",
"司徒": "Sī tú", "司空": "Sī kōng", "司徒": "Sī tú",
"诸葛": "Zhū gě", "东方": "Dōng fāng", "独孤": "Dú gū",
"慕容": "Mù róng", "宇文": "Yǔ wén"
}
def correct_name_pinyin(name):
"""生成带声调的姓名拼音(姓氏校正)"""
if not name or not isinstance(name, str):
return ""
name = name.strip()
# 优先处理复姓
for compound_surn in COMPOUND_SURNAMES:
if name.startswith(compound_surn):
surn_pinyin = COMPOUND_SURNAMES[compound_surn]
given_name = name[len(compound_surn):]
given_pinyin = ' '.join([p[0] for p in pinyin(given_name, style=Style.TONE)])
return f"{surn_pinyin} {given_pinyin}"
# 处理单姓多音字
first_char = name[0]
if first_char in SPECIAL_SURNAMES:
surn_pinyin = SPECIAL_SURNAMES[first_char]
given_name = name[1:]
given_pinyin = ' '.join([p[0] for p in pinyin(given_name, style=Style.TONE)])
return f"{surn_pinyin} {given_pinyin}"
# 普通姓名拼音
return ' '.join([p[0] for p in pinyin(name, style=Style.TONE)])
try:
# 读取所有工作表
excel = pd.ExcelFile(file_path)
sheet_names = excel.sheet_names
class_data = {}
for sheet_name in sheet_names:
# 跳过非班级工作表(如果有的话)
if not any(keyword in sheet_name for keyword in ["班", "class", "Class"]):
continue
df = pd.read_excel(file_path, sheet_name=sheet_name, usecols=range(5), header=None, dtype={0: str})
df = df.iloc[1:] # 跳过第1行标题
student_list = []
for idx, row in df.iterrows():
# 过滤无效数据(学号为空、姓名为空)
raw_id = str(row[0]).strip() if pd.notna(row[0]) else ""
name = str(row[3]).strip() if pd.notna(row[3]) else ""
if not raw_id.isdigit() or not name: # 如果没有学号,没有名字
name='张三'
elif not name: # 如果有学号,没有名字
name='张三'
# 提取核心信息
student_info = {
"campus": f"{school}-" + (str(row[1]).strip() if pd.notna(row[1]) else ""), # 园区
"class_name": str(row[2]).strip() if pd.notna(row[2]) else "", # 班级
"student_id": f"{(raw_id)}号", # 学号(如:1号)
"name": name, # 姓名
"name_pinyin": correct_name_pinyin(name), # 带声调拼音
"date": date # 添加日期字段
}
student_list.append(student_info)
if student_list:
print(f"读取[{sheet_name}]:{len(student_list)}个有效学生")
class_data[sheet_name] = student_list
else:
print(f"工作表[{sheet_name}]无有效学生数据")
return class_data
except Exception as e:
print(f"Excel读取失败:{str(e)}")
return {}
def get_grade_size(class_name, copies):
"""根据班级名称+份数获取对应的目标总挂牌数"""
if "托" in class_name:
return GRADE_BASE_SIZES["托"] * copies
elif "小" in class_name:
return GRADE_BASE_SIZES["小"] * copies
elif "中" in class_name:
return GRADE_BASE_SIZES["中"] * copies
elif "大" in class_name:
return GRADE_BASE_SIZES["大"] * copies
else:
# 默认返回小班人数
return GRADE_BASE_SIZES["小"] * copies
def expand_student_list(student_list, target_size, class_name, copies):
"""扩展学生列表到目标大小,用空学生填充(适配动态份数)"""
# 实际需要的名字数量 = 目标总挂牌数 / 每份数
name_count_needed = target_size // copies
if len(student_list) >= name_count_needed:
return student_list[:name_count_needed] # 如果实际学生多于目标,截取前name_count_needed个
# 扩展学生列表
expanded_list = student_list.copy()
# 获取园区和班级名称(从第一个有效学生获取)
campus = student_list[0]["campus"] if student_list else f"{school}"
class_name_full = student_list[0]["class_name"] if student_list else class_name
# 添加空学生(张三占位)
for i in range(len(student_list), name_count_needed):
empty_student = {
"campus": campus,
"class_name": class_name_full,
"student_id": f"{i+1}号",
"name": "张三", # 空姓名用张三占位
"name_pinyin": "Zhang San", # 对应拼音
"date": date # 有效日期
}
expanded_list.append(empty_student)
return expanded_list
def duplicate_students_for_copies(student_list, copies):
"""为每个学生复制指定份数(动态传1/2)"""
duplicated_list = []
for student in student_list:
for i in range(copies):
duplicated_list.append(student.copy())
return duplicated_list
# ========== 3. 写入Word表格(适配动态份数,保留原有字体/行间距/样式) ==========
def write_student_to_word(student_list, template_path, output_path, class_name, copies):
"""
将学生数据写入Word模板的3行2列表格
适配动态份数copies,保留原有:字体样式、行间距、颜色、单元格垂直居中
单个单元格内容:园区\n班级\n学号\n拼音\n姓名\n有效期
"""
# 根据班级类型+份数确定目标总挂牌数
target_size = get_grade_size(class_name, copies)
print(f"班级 {class_name}({copies}份版)目标挂牌数: {target_size}, 实际学生数: {len(student_list)}")
# 扩展学生列表到目标名字数(适配份数)
expanded_student_list = expand_student_list(student_list, target_size, class_name, copies)
# 为每个名字复制指定份数
final_student_list = duplicate_students_for_copies(expanded_student_list, copies)
print(f"班级 {class_name}({copies}份版)复制后总挂牌数: {len(final_student_list)}")
# 按6个学生一组分割(3行2列表格可容纳6个学生)
student_groups = [final_student_list[i:i+6] for i in range(0, len(final_student_list), 6)]
generated_files = [] # 存储生成的文件路径
for group_idx, group in enumerate(student_groups, 1):
# 复制模板文件(避免修改原模板,保留背景图)
temp_template = os.path.join(output_path, f"temp_{class_name}_{copies}份_{group_idx}.docx")
shutil.copy2(template_path, temp_template)
# 打开复制后的模板,操作表格
doc = Document(temp_template)
# 获取模板中的第一个表格(需确保"挂牌.docx"的第一个表格是3行2列")
if not doc.tables:
print(f"模板{template_path}中未找到表格,跳过此组")
os.remove(temp_template)
continue
table = doc.tables[0]
# 清除表格原有内容(保留表格结构和模板背景)
for row in table.rows:
for cell in row.cells:
for para in cell.paragraphs:
p = para._element
p.getparent().remove(p)
cell._element.clear() # 完全清空单元格
# 定义字体样式(完全保留原有逻辑)
def set_paragraph_style(para, text, font_size, is_bold=False, is_gray=True, underline=False):
"""设置段落的字体、大小、对齐(居中)和颜色"""
if not text: # 跳过空文本
return
run = para.add_run(text)
# 设置中文字体为微软雅黑
run.font.name = '微软雅黑'
run.element.rPr.rFonts.set(qn('w:eastAsia'), '微软雅黑')
run.font.size = Pt(font_size)
run.font.bold = is_bold
run.font.underline = underline # 设置下划线
# 设置颜色:灰色或黑色
if is_gray:
run.font.color.rgb = RGBColor(169, 169, 169) # DarkGray
else:
run.font.color.rgb = RGBColor(0, 0, 0) # 黑色
para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # 文字居中
# 逐个单元格写入学生信息(3行2列,共6个单元格)
cell_index = 0 # 单元格索引(0-5对应3行2列的6个单元格)
for row_idx, row in enumerate(table.rows):
for cell_idx, cell in enumerate(row.cells):
if cell_index >= len(group):
break # 学生不足6个时,空单元格跳过
# 单元格垂直居中(保留原有逻辑)
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
# 当前学生的6种信息(按原有顺序)
student = group[cell_index]
info_lines = [
student["campus"], # 第1行:园区
student["class_name"], # 第2行:班级
student["student_id"], # 第3行:学号
student["name_pinyin"], # 第4行:拼音(10pt)
student["name"], # 第5行:姓名(30pt,加粗)
student["date"] # 第6行:有效期
]
# 向单元格写入内容(完全保留原有行间距、字体大小)
for line_idx, line in enumerate(info_lines):
if line: # 只写入非空内容
para = cell.add_paragraph()
if line_idx == 0: # 园区:16pt
set_paragraph_style(para, line, font_size=16, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(45)
elif line_idx == 1: # 班级:30pt
set_paragraph_style(para, line, font_size=30, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(35)
elif line_idx == 2: # 学号:50pt 黑色下划线
set_paragraph_style(para, line, font_size=50, is_bold=True, is_gray=False, underline=True)
para.paragraph_format.line_spacing = Pt(65)
elif line_idx == 3: # 拼音行:10pt
set_paragraph_style(para, line, font_size=10, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(20)
elif line_idx == 4: # 姓名行:30pt,加粗
set_paragraph_style(para, line, font_size=30, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(35)
elif line_idx == 5: # 有效期:8pt
set_paragraph_style(para, line, font_size=8, is_bold=True, is_gray=True)
para.paragraph_format.line_spacing = Pt(15)
cell_index += 1
# =====新增下面这段代码=====
# 强制固定表格宽度9.4cm,关闭自动适配
table.autofit = False
table.width = Cm(9.4)
# ==========================
# 保存当前组的Word文件(带份数标识)
final_word_path = os.path.join(output_path, f"{class_name}_{copies}份_挂牌_{group_idx:02d}.docx")
doc.save(final_word_path)
generated_files.append(final_word_path) # 添加到生成文件列表
os.remove(temp_template) # 删除临时模板
print(f"生成{copies}份版第{group_idx}组Word:{os.path.basename(final_word_path)}")
return generated_files
# ========== 4. Word合并函数(保留原有win32+备用双方案,适配份数) ==========
def merge_docx_files_with_win32(template_file, files_list, output_filename):
"""使用win32com合并Word文档,去掉换页符(保留原有逻辑)"""
if not files_list:
print("没有文件可合并")
return
word = None
doc = None
try:
# 启动Word应用程序
import win32com.client as win32
word = win32.Dispatch("Word.Application")
word.Visible = False # 不显示Word界面
word.DisplayAlerts = False # 不显示警告
# 复制模板文件到输出文件
shutil.copy2(template_file, output_filename)
# 打开输出文件
doc = word.Documents.Open(output_filename)
# 将光标移动到文档末尾
selection = word.Selection
selection.EndKey(6) # wdStory = 6, 移动到文档末尾
# 逐个插入其他文件的内容(跳过第一个文件,因为已经是模板)
for i, file_path in enumerate(files_list):
if i == 0: # 跳过模板文件本身
continue
print(f"正在插入文件: {os.path.basename(file_path)}")
# 插入文件内容
selection.InsertFile(file_path)
# 移动到文档末尾准备插入下一个文件
selection.EndKey(6)
time.sleep(1) # 添加延时,确保Word有足够时间处理
# 保存合并后的文档
doc.Save()
print(f"已合并所有文档到:{output_filename}")
except Exception as e:
print(f"合并文档时出错:{e}")
# 尝试使用备选方法
try:
print("尝试使用备选合并方法...")
merge_docx_files_simple(template_file, files_list, output_filename)
except Exception as e2:
print(f"备选方法也失败:{e2}")
finally:
# 确保正确关闭文档和Word应用程序
try:
if doc:
doc.Close(False)
time.sleep(0.5)
except:
pass
try:
if word:
word.Quit()
time.sleep(0.5)
except:
pass
def merge_docx_files_simple(template_file, files_list, output_filename):
"""简单的合并方法:复制模板,然后逐个追加内容(保留原有逻辑)"""
# 复制模板文件
shutil.copy2(template_file, output_filename)
# 打开模板文件
main_doc = Document(output_filename)
# 逐个添加其他文件的内容
for i, file_path in enumerate(files_list):
if i == 0: # 跳过模板文件本身
continue
# 添加分页符
if i > 1: # 从第二个文件开始添加分页符
main_doc.add_page_break()
# 读取要添加的文件
sub_doc = Document(file_path)
# 复制表格内容
for table in sub_doc.tables:
# 创建新表格
new_table = main_doc.add_table(rows=len(table.rows), cols=len(table.columns))
# 复制内容和格式
for row_idx, row in enumerate(table.rows):
for col_idx, cell in enumerate(row.cells):
new_cell = new_table.cell(row_idx, col_idx)
# 清空新单元格
for paragraph in new_cell.paragraphs:
p = paragraph._element
p.getparent().remove(p)
new_cell._element.clear()
# 复制内容
for paragraph in cell.paragraphs:
new_para = new_cell.add_paragraph()
for run in paragraph.runs:
new_run = new_para.add_run(run.text)
# 复制字体样式
new_run.font.name = run.font.name
new_run.font.size = run.font.size
new_run.font.bold = run.font.bold
new_run.font.underline = run.font.underline
if hasattr(run.font.color, 'rgb'):
new_run.font.color.rgb = run.font.color.rgb
# 保存合并后的文档
main_doc.save(output_filename)
print(f"使用简单方法合并完成:{output_filename}")
def cleanup_directory(directory_path):
"""清理目录(保留原有逻辑)"""
try:
if os.path.exists(directory_path):
shutil.rmtree(directory_path)
print(f"已删除临时目录: {directory_path}")
except Exception as e:
print(f"删除目录时出错: {e}")
# ========== 5. 主程序(核心修改:循环处理1/2份,统一保存) ==========
if __name__ == "__main__":
# 步骤1:读取Excel数据(含拼音生成)
print("=== 开始读取Excel数据 ===")
class_data = read_excel_and_generate_pinyin(excel_file)
if not class_data:
print("无有效学生数据,程序退出")
exit()
# 步骤2:循环处理1份和2份版(核心修改)
all_merged_files = []
for copies in COPIES_PER_NAME_LIST:
print(f"\n" + "="*50)
print(f"开始处理【{copies}份版】所有班级")
print("="*50)
# 为当前份数创建独立临时目录,避免文件冲突
output_dir = os.path.join(root_output_dir, f"{copies}份版")
os.makedirs(output_dir, exist_ok=True)
# 为每个班级创建单独的临时子目录
class_output_dirs = {}
for class_name in class_data.keys():
class_dir = os.path.join(output_dir, class_name)
os.makedirs(class_dir, exist_ok=True)
class_output_dirs[class_name] = class_dir
# 遍历每个班级处理
for sheet_name, student_list in class_data.items():
# 获取班级名称(取第一个学生的班级信息)
class_name = student_list[0]["class_name"] if student_list else sheet_name
print(f"\n=== 处理{class_name}({copies}份版)===")
# 步骤3:写入Word(适配当前份数,保留所有原有样式)
generated_files = write_student_to_word(
student_list=student_list,
template_path=template_word,
output_path=class_output_dirs[sheet_name],
class_name=class_name,
copies=copies # 传入当前份数
)
# 步骤4:为每个班级合并所有生成的docx文件
if generated_files:
# 使用第一个文件作为模板
template_file = generated_files[0]
# 合并后的文件保存到统一可编辑文件夹,文件名带【1块/2块】标识
merged_filename = os.path.join(
editable_dir,
f"04_{date[:4]}{int(date[5:6]):02}_{class_name}_{copies}块_圆形牌_接送&被子&床卡_(开学直接Word打印).docx"
# 文件 选项 打印 打印文档的附加信息 打印背景色和图像.docx
)
print(f"开始合并{class_name}({copies}份版)的文档...")
try:
merge_docx_files_with_win32(template_file, generated_files, merged_filename)
all_merged_files.append(merged_filename)
print(f"{class_name}({copies}份版)合并完成")
except Exception as e:
print(f"{class_name}({copies}份版)的win32合并失败,尝试简单方法: {e}")
try:
merge_docx_files_simple(template_file, generated_files, merged_filename)
all_merged_files.append(merged_filename)
print(f"{class_name}({copies}份版)使用简单方法合并完成")
except Exception as e2:
print(f"{class_name}({copies}份版)的所有合并方法都失败: {e2}")
# 处理完一个班级后稍作延时,释放资源
time.sleep(2)
# 步骤5:清理所有临时文件根目录
print("\n=== 清理所有临时文件 ===")
cleanup_directory(root_output_dir)
# 步骤6:输出最终结果
print(f"\n=== 所有处理完成 ===")
print(f"所有1/2份版文件统一保存到:{editable_dir}")
print(f"生成的合并文件列表:")
for file in all_merged_files:
print(f" - {os.path.basename(file)}")
# 计算并显示运行时间
end_time = time.time()
total_seconds = end_time - start_time
minutes = int(total_seconds // 60)
seconds = int(total_seconds % 60)
print(f"\n程序运行时间: {minutes}分{seconds}秒 (共{total_seconds:.2f}秒)")
最终最关键的就是把“圆形挂牌_文本框圆形”的模版上传资源库



1556

被折叠的 条评论
为什么被折叠?



