下载地址
https://www.zhengdianxue.com/wp-content/uploads/skills/org-vc3t81nj--moways-xlsx.zip将提示词发送给你的 AI 安装该 skills
请根据 https://skillhub.cn/install/skillhub.md,安装 @org-vc3t81nj/moways-xlsx。企业级 Excel 制表规范一键落地——字体、颜色、行高、边框、数字格式照规矩来。跨平台字体自动适配,支持自定义模板配置。
阅读全文 (展开查看完整说明)
概述
博维(MOWAYS)Excel 制表规范
本技能用于按照博维管理咨询公司的Excel行文格式标准来创建、修改和审阅Excel表格。博维的Excel规范追求的核心目标是:打印出来清晰美观、任何人拿到都能看懂、数据可回溯可验证。
前置依赖: 使用本技能前,需确保已安装 openpyxl:
pip install openpyxl在 WorkBuddy 中运行 Python 脚本时,直接使用
python命令即可(WorkBuddy 会自动选择已安装的 Python 环境):python script.py
本技能已包含 openpyxl 的完整用法和最佳实践,无需额外阅读其他技能文档。
如果需要了解更通用的 Excel 操作(如 pivot table、图表等),可自行查阅 openpyxl 官方文档。
零、前置检查与失败模式处理
生成表格之前,必须执行以下检查并处理所有可能失败场景。任何静默崩溃都会导致不合规的表格。
0.1 openpyxl 可用性检查
import subprocess
import sys
def ensure_openpyxl():
"""确保 openpyxl 已安装,未安装则自动安装。"""
try:
import openpyxl
return True
except ImportError:
print("⚠️ openpyxl 未安装,正在自动安装...")
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'openpyxl'])
import openpyxl
print("✅ openpyxl 安装成功")
return True
except Exception as e:
print(f"❌ openpyxl 安装失败: {e}")
print("请手动执行: pip install openpyxl")
return False⚠️ 如果安装失败:告知用户手动安装后重试,不要继续执行生成流程。
0.2 系统字体检测与跨平台映射
Windows/macOS/Linux 系统上,中文字体名可能不存在。必须在生成前检测可用字体并自动替换。
import platform
import subprocess
# ===== 跨平台字体映射表 =====
FONT_MAP = {
'黑体': {
'windows': ['黑体', 'SimHei'],
'darwin': ['Heiti SC', 'PingFang SC', 'STHeiti', 'Hiragino Sans GB'],
'linux': ['Noto Sans CJK SC', 'WenQuanYi Micro Hei', 'Source Han Sans SC'],
},
'仿宋': {
'windows': ['仿宋', 'FangSong', 'FangSong_GB2312'],
'darwin': ['STFangsong', 'Songti SC', 'PingFang SC'],
'linux': ['Noto Serif CJK SC', 'WenQuanYi Micro Hei Mono', 'FangSong'],
},
'宋体': {
'windows': ['宋体', 'SimSun'],
'darwin': ['Songti SC', 'STSong', 'PingFang SC'],
'linux': ['Noto Serif CJK SC', 'WenQuanYi Zen Hei'],
},
'Times New Roman': {
'windows': ['Times New Roman'],
'darwin': ['Times New Roman', 'Times'],
'linux': ['Times New Roman', 'Liberation Serif', 'FreeSerif'],
},
'Calibri': {
'windows': ['Calibri'],
'darwin': ['Calibri', 'Helvetica Neue'],
'linux': ['Carlito', 'Liberation Sans'],
},
}
def resolve_font_name(font_name, os_name=None):
"""
根据当前操作系统,将字体名映射为可用字体。
返回 (实际使用的字体名, 是否为替代字体)
示例:
Windows: resolve_font_name('黑体') → ('黑体', False)
macOS: resolve_font_name('黑体') → ('Heiti SC', True) # 替代
"""
if os_name is None:
os_name = platform.system().lower()
candidates = FONT_MAP.get(font_name, {}).get(os_name, [font_name])
if os_name == 'windows':
return candidates[0], False # Windows 直接信任字体名
# macOS/Linux: 尝试验证字体
for name in candidates:
try:
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws['A1'].font = Font(name=name, size=12)
ws['A1'] = 'test'
# 不实际保存,只验证是否能设置
return name, (name != font_name)
except Exception:
continue
# 全部验证失败,用第一个候选兜底
return candidates[0], True
# ===== 使用示例 =====
os_name = platform.system().lower()
title_font_name, _ = resolve_font_name('黑体', os_name)
body_font_cn_name, is_alt_cn = resolve_font_name('仿宋', os_name)
body_font_en_name, _ = resolve_font_name('Times New Roman', os_name)
# 如果使用了替代字体,在表格备注中标注
if is_alt_cn:
print(f"⚠️ 当前系统({os_name})无'仿宋'字体,已替代为: {body_font_cn_name}")
# 生成时使用解析后的字体名
title_font = Font(name=title_font_name, size=20, bold=True)
body_font_cn = Font(name=body_font_cn_name, size=12)
body_font_en = Font(name=body_font_en_name, size=12)跨平台字体效果说明:
| 标准字体 | Windows | macOS | Linux | 效果差异 |
|---|---|---|---|---|
| 黑体 | 黑体 | Heiti SC | Noto Sans CJK SC | 视觉高度一致 |
| 仿宋 | 仿宋 | STFangsong | Noto Serif CJK SC | 笔画风格略有差异,可接受 |
| Times New Roman | TNR | TNR | Liberation Serif | 几乎无差异 |
0.3 文件损坏检测
def safe_open_workbook(filepath):
"""安全打开 Excel 文件,处理常见错误场景。"""
from openpyxl import load_workbook
if not filepath.endswith(('.xlsx', '.xlsm', '.xltx', '.xltm')):
raise ValueError(
f"❌ 不支持的文件格式: '{filepath}'n"
f"仅支持 .xlsx / .xlsm / .xltx / .xltm 格式。"
)
try:
wb = load_workbook(filepath)
return wb
except FileNotFoundError:
raise FileNotFoundError(
f"❌ 文件不存在: '{filepath}'n"
f"请确认文件路径是否正确。"
)
except Exception as e:
# 文件可能损坏或格式异常
raise RuntimeError(
f"❌ 无法打开文件: '{filepath}'n"
f"文件可能已损坏或不是有效的 Excel 文件。n"
f"原始错误: {e}n"
f"建议:请尝试用 Excel 手动打开该文件确认是否正常。"
)0.4 列宽溢出检测与警告
def check_content_overflow(text, col_width_units, font_size=12):
"""
检测文本是否会因列宽太窄而严重溢出。
返回 (是否会溢出, 建议最小列宽, 预计换行行数)
"""
if not text:
return False, col_width_units, 1
total_width = 0
for ch in str(text):
cp = ord(ch)
if cp > 127:
total_width += 2.0
elif cp in (ord('('), ord(')'), ord('《'), ord('》')):
total_width += 0.8
else:
total_width += 0.55
effective_width = max(col_width_units * 0.92, 3)
lines = max(1, int(total_width / effective_width) + (1 if total_width % effective_width > 0 else 0))
# 换行超过5行 = 溢出警告
if lines > 5:
suggested_width = total_width / (4 * 0.92) # 目标不超过4行
return True, suggested_width, lines
return False, col_width_units, lines0.5 合并单元格冲突处理
def safe_merge_cells(ws, start_row, start_col, end_row, end_col):
"""
安全合并单元格,避免与已有合并区域冲突。
如果目标区域与已有合并区域重叠,先取消已有合并。
"""
target_range = f'{get_column_letter(start_col)}{start_row}:{get_column_letter(end_col)}{end_row}'
# 检查冲突
conflicts = []
for merged_range in list(ws.merged_cells.ranges):
if (start_row <= merged_range.max_row and end_row >= merged_range.min_row and
start_col <= merged_range.max_col and end_col >= merged_range.min_col):
conflicts.append(str(merged_range))
if conflicts:
print(f"⚠️ 合并区域冲突,先取消已有合并: {conflicts}")
for merged_range in ws.merged_cells.ranges:
if (start_row <= merged_range.max_row and end_row >= merged_range.min_row and
start_col <= merged_range.max_col and end_col >= merged_range.min_col):
ws.unmerge_cells(str(merged_range))
ws.merge_cells(target_range)0.6 生成前完整检查流程
每次生成博维标准表格时,必须按以下顺序执行检查:
┌─────────────────────────────────────┐
│ ① 检查 openpyxl 可用性 │ → 失败则安装重试,安装失败则中止
├─────────────────────────────────────┤
│ ② 检测操作系统 + 字体映射 │ → 生成解析后的字体名
├─────────────────────────────────────┤
│ ③ 如果是修改已有文件: │
│ 安全打开 + 检测文件损坏 │ → 损坏则报错中止
├─────────────────────────────────────┤
│ ④ 生成数据,设置样式 │ → 使用解析后的字体名
├─────────────────────────────────────┤
│ ⑤ 列宽溢出检测 │ → 如有溢出,增加列宽或告知用户
├─────────────────────────────────────┤
│ ⑥ 行高自适应计算 │ → 逐行估算
├─────────────────────────────────────┤
│ ⑦ 保存文件 │ → 输出到指定路径
└─────────────────────────────────────┘任何步骤失败都不能静默跳过,必须向用户报告具体错误和建议操作。
一、格式标准
1. 标题
- 字体:黑体(macOS → Heiti SC, Linux → Noto Sans CJK SC),16-24号(视具体情况,常用20号),加粗
- 对齐:跨列合并居中(合并范围覆盖数据区所有列),垂直居中
- 背景色:可用浅蓝色背景(theme 4, tint 0.8)来区分标题区域
- 行高:适当加大(如 17.5-25.5 pt)
openpyxl 实现要点:
from openpyxl.styles import Font, Alignment, PatternFill
title_font = Font(name='黑体', size=20, bold=True)
title_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
# 合并标题行覆盖所有数据列
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=last_col)
ws['A1'].font = title_font
ws['A1'].alignment = title_align2. 正文(数据区)
- 中文使用仿宋字体(macOS → STFangsong, Linux → Noto Serif CJK SC)(打印出来清晰美观)
- 英文和数字使用 Times New Roman 字体(跨平台通用,无需映射)
- 字号:9-12号(常用12号,10.5为五号字),视具体情况而定
- 行距、列宽视具体情况和打印页面布局而定
- 所有字体名必须通过
resolve_font_name()解析后使用(见 0.2 节),不可硬编码
⚠️ 字体一致性核心规则:先兜底,再覆盖
openpyxl 单个单元格只能设置一种字体,且不设置字体的单元格会回退到 Excel 默认字体(Calibri),这正是字体不一致的根因。ws.sheet_properties 设置默认字体在 openpyxl 中不生效,切勿依赖。
正确做法:先兜底,再覆盖(baseline + override)
- 第一步(兜底):数据区所有单元格先统一设置为
仿宋字体——确保没有任何单元格遗漏 - 第二步(覆盖):再对纯数字/英文/日期列覆盖为
Times New Roman——精准替换,不怕多设只怕漏设
# ===== 第一步:全表兜底 —— 数据区所有单元格统一设仿宋 =====
body_font_cn = Font(name='仿宋', size=12)
body_font_en = Font(name='Times New Roman', size=12)
for row in ws.iter_rows(min_row=header_row+1, max_row=last_row, min_col=1, max_col=last_col):
for cell in row:
cell.font = body_font_cn # 先全部设仿宋,兜底
# ===== 第二步:按列覆盖 —— 纯数字/日期列覆盖为 Times New Roman =====
# 提前定义哪些列是数字/日期列(按列索引,1-based)
numeric_cols = [1, 5, 6, 7, 8, 9] # 如:序号、入职日期、工龄、月薪、股份数、占比
for col_idx in numeric_cols:
for row_idx in range(header_row+1, last_row+1):
ws.cell(row=row_idx, column=col_idx).font = body_font_en判断列类型的方法:
- 纯数字列(序号、金额、数量、百分比)→ TNR
- 日期列 → TNR
- 纯中文列(姓名、部门、职位、备注)→ 仿宋(兜底已处理,无需额外操作)
- 混合内容列(如备注里可能中英文都有)→ 仿宋(兜底已处理)
⚠️ 禁止的做法:
- ❌ 逐个单元格判断内容再设字体——容易遗漏,导致部分单元格回退到 Calibri
- ❌ 依赖
ws.sheet_properties设默认字体——openpyxl 不支持 - ❌ 只设部分列的字体就不管其他列——未设的列全是 Calibri
3. 表头行
- 字体:黑体,12号,不加粗(除非特别需要)
- 对齐:水平居中,垂直居中
- 边框:四边细线(thin)
- 表头内容应清晰标注单位,如"月薪(元)""工龄(年)""拟分配股份数(万股)"
header_font = Font(name='黑体', size=12)
header_align = Alignment(horizontal='center', vertical='center')
header_border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin')
)4. 数据行
- 中文内容(姓名、部门、职位):
Font(name='仿宋', size=12) - 数字/英文/日期内容:
Font(name='Times New Roman', size=12)或Font(name='仿宋', size=12) - 对齐:
- 姓名列 → 左对齐(
horizontal='left') - 序号、数值、日期、百分比等 → 居中(
horizontal='center')
- 姓名列 → 左对齐(
- 垂直对齐:统一垂直居中(
vertical='center') - 边框:四边细线(thin),与表头一致
5. 字体颜色和背景色
用于两个场景:
- 突出重要内容和单元格(如合计行、关键数据)
- 行数较多时避免阅读串行
规则:
- 突出色方案:浅黄色背景(theme 7, tint 0.8) + 暗红色字体(
RGB: C00000) - 合计行:浅黄背景 + 暗红字体 + 加粗
from openpyxl.styles import PatternFill
# 突出色
highlight_font = Font(name='仿宋', size=12, bold=True, color='C00000')
highlight_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
# 注意:theme色在openpyxl中不易直接使用,用近似RGB替代
# theme 7 tint 0.8 ≈ FFF2CC(浅黄)
# 暗红 = C00000分类标题行(如"一、市场部""二、技术部"等数据区内的分组标题行):
- 字体:仿宋,12号,加粗
- 字体颜色:暗红色(
C00000) - 背景色:浅黄色(
FFF2CC) - 对齐:左对齐,垂直居中
- 可跨列合并(合并该行所有数据列)
- 这类行不是普通数据行,必须显式设置高亮样式,不能只靠兜底字体
# 分类标题行示例
category_font = Font(name='仿宋', size=12, bold=True, color='C00000')
category_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
category_align = Alignment(horizontal='left', vertical='center')
# 假设第5行是分类标题行
ws.merge_cells(start_row=5, start_column=1, end_row=5, end_column=last_col)
cell = ws.cell(row=5, column=1, value='一、市场部')
cell.font = category_font
cell.fill = category_fill
cell.alignment = category_align6. 内容布局
(1) 公司/单位名称与日期
- 公司名称放在表格左上方或居中位置
- 设计日期/填表日期放在表格右上方
- 这两个信息在标题行下方、表头行上方
- 公司名称用浅黄背景+暗红字体突出
(2) 表头布局
- 表头需要布局好各类小标题及其子项
- 可以根据打印要求分多行多列表示(合并单元格)
- 每列表头要标注清楚含义和单位
(3) 数字格式
- 金额、大额数字使用千分符(
number_format = '#,##0') - 数量保留到整数位(视行业而定)
- 销售金额、兑现金额等保留2位小数(
number_format = '#,##0.00') - 百分比使用百分比格式(
number_format = '0.00%'或'0.0%') - 日期使用标准格式(
number_format = 'YYYY-MM-DD')
(4) 消除错误值
- 表格中不能出现错误字符(
#DIV/0!,#N/A,#REF!等) - 数字为"0"时显示为
"-" - 编制公式时使用
IF/IFNA/IFERROR消除错误值 - 示例:
=IF(ISERROR(A1/B1),0,A1/B1)或=IFERROR(A1/B1,0)
在openpyxl中生成公式时,始终包裹IFERROR:
# 不要写:ws['F2'] = '=A2/B2'
# 要写:
ws['F2'] = '=IFERROR(A2/B2,"-")'(5) 公式标注列
- 列与列之间如有逻辑关系,需加一行标注计算公式
- 可用 "A, B, C…" 或 "1, 2, 3…" 标识列,然后在合适位置写出公式关系
- 例如在合计行下方加一行:
购股金额=A*3(万元) - 金额大写行:用合并单元格显示大写金额
7. 边框
- 数据区域统一使用四边细线(thin border)
- 标题区域可以不加边框或仅加下边框
- 合并的标题单元格通常只有外边框
from openpyxl.styles import Border, Side
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)8. 行高自适应
核心原则:行高必须保证内容完整显示,不能出现文字被截断的情况。
openpyxl 没有真正的"自动行高"功能(Excel 的自动行高仅在客户端打开时计算),因此在生成文件时必须手动估算并设置行高。
行高估算规则
| 内容类型 | 建议行高(pt) | 说明 |
|---|---|---|
| 标题行 | 25-35 | 字号大,需留白 |
| 副标题行(公司名/日期) | 18-22 | |
| 表头行(单行) | 18-22 | |
| 表头行(多行换行) | 25-35 | 视换行行数,每行约12-14pt |
| 普通数据行(不换行) | 16-20 | 12号字约需15pt,加留白 |
| 数据行(内容较长需换行) | 按公式计算 | 见下方公式 |
| 分类标题行 | 18-22 | |
| 合计行 | 18-22 | |
| 公式标注行 | 16-20 |
换行行高计算公式
当单元格设置了 wrap_text=True 且内容较长会换行时:
import math
def calc_row_height(text, col_width_units, font_size=12):
"""
计算单元格所需行高(pt)—— v3精确版
关键原理:
- openpyxl 的 column_width 单位是"默认字体(Calibri 11)的字符0宽度"
- 仿宋12号的中文字符实际占用约 2.0 个单位宽度
- 英文/数字约占用 0.55 个单位宽度,括号/书名号约 0.8
- 每行实际高度 ≈ 字号 × 1.4(含行间距)
- 安全系数 1.06(仅6%余量,避免行高过大)
"""
if not text:
return 20
total_width = 0
for ch in str(text):
cp = ord(ch)
if cp > 127: # 中文字符 / 全角符号 → 宽2.0单位
total_width += 2.0
elif cp in (ord('('), ord(')'), ord('《'), ord('》')):
total_width += 0.8
else: # 半角英文/数字/标点 → 宽0.55单位
total_width += 0.55
effective_width = max(col_width_units * 0.92, 3) # 留左右padding
lines = math.ceil(total_width / effective_width)
line_height = font_size * 1.4 # 每行高度含行距
height = lines * line_height + 2 # 上下安全边距
return max(18, int(height * 1.06)) # 安全系数1.06,最小18pt实际应用模板
# ===== 设置列宽(先设列宽,才能算行高) =====
col_widths = {'A': 6, 'B': 12, 'C': 15, 'D': 15, 'E': 14, 'F': 12,
'G': 14, 'H': 16, 'I': 12, 'J': 20}
for col, width in col_widths.items():
ws.column_dimensions[col].width = width
# ===== 设置行高 =====
# 固定行高(标题、表头等)
ws.row_dimensions[1].height = 30 # 标题行
ws.row_dimensions[2].height = 20 # 副标题行
ws.row_dimensions[3].height = 22 # 表头行
# 数据行:逐行估算,取该行所有单元格的最大所需高度
for row_idx in range(header_row + 1, last_row + 1):
max_height = 20 # 最小行高
for col_idx in range(1, last_col + 1):
cell = ws.cell(row=row_idx, column=col_idx)
if cell.value is not None:
col_letter = get_column_letter(col_idx)
col_width = col_widths.get(col_letter, 12)
h = calc_row_height(str(cell.value), col_width)
max_height = max(max_height, h)
ws.row_dimensions[row_idx].height = max_height⚠️ 关键注意事项:
- 先设列宽,再设行高——行高依赖列宽,顺序不能反
- wrap_text=True 的单元格必须设行高——否则 Excel 打开时可能不自动撑开
- 宁可高一点也不能矮——行高偏大有留白不影响美观,偏矮则截断内容
- 合并单元格的行高——合并后只有左上角单元格生效,需单独处理
- 如果内容确实很长(如备注列),考虑加宽列宽而非无限增加行高
二、工作底稿原则
博维制表不仅关注最终呈现,更强调Excel文件作为"工作底稿"的可追溯性和可读性。在为咨询项目制作测算表、分析模型时,遵循以下原则:
1. 数据分区:取数区、运算区、输出区
将数据处理过程分为三个区域,尽量用三张独立的工作表(Sheet)来组织:
- 取数区(Input):原始数据经过基本加工后的数据,基本保留数据原貌。原始数据需另外保存一份。
- 运算区(Working):公式运算区,复杂计算拆分成多步,让每个参与计算的要素一目了然。
- 输出区(Output):输出区的公式最好全部引用运算区的计算结果(
=Sheet_Working!B1),不再包含独立公式。
实际操作:
- 创建多Sheet的工作簿时,命名为
Input/取数区、Working/运算区、Output/输出区 - 如果在单Sheet中完成,用表头标注区分区域
- 输出区更注意标注和解释
2. 永远使用链接取数
- 尽量让数据来源存在于同一个工作簿中
- 永远用
=单元格引用进行取数,而不是复制粘贴和手打数值 - 从"最终加工形态的数据"处取数,而非从中间状态取数
- 同一计算口径的数字,从同一个加工场所取走
在openpyxl中:当需要在不同Sheet间引用数据时,在输出Sheet写公式引用运算Sheet的结果:
ws_output['B2'] = "=Working!B10"3. 不厌其烦地标注
- 辅助列的表头一定要有标注(说明该列用途)
- 标注方式用文字说明而非纯数字编码(
y/n优于1/0) - 在当前区块最右侧加入「备注」列,放入相关文字说明
- 不推荐在密集表格中使用Excel自带的「批注/附注」功能(容易挡住临近单元格)
4. 文件版本管理
- 每次修改先另存为新文件(在打开之前就复制)
- 文件命名格式:
日期+文件标题+版本号 - 可在文件名中体现重大变化(如 src-updated、commented、cleared)
- 甚至把计算口径也写在标题里
三、打印设置
生成的Excel文件应该考虑打印友好性:
# 设置打印区域
ws.print_area = f'A1:{get_column_letter(last_col)}{last_row}'
# 页面设置
ws.page_setup.orientation = 'landscape' # 或 'portrait',视表格宽度
ws.page_setup.paperSize = ws.PAPERSIZE_A4
ws.page_setup.fitToWidth = 1
ws.page_setup.fitToHeight = 0 # 不限制高度
ws.sheet_properties.pageSetUpPr.fitToPage = True
# 页边距(厘米转英寸)
ws.page_margins.left = 0.75
ws.page_margins.right = 0.75
ws.page_margins.top = 1.0
ws.page_margins.bottom = 1.0
# 冻结首行(表头)
ws.freeze_panes = 'A2' # 或表头下一行四、完整示例:创建一张博维标准表格的流程
import openpyxl
import math
import platform
import subprocess
import sys
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
# ===== 0. 前置检查 =====
# ① 检查 openpyxl 可用性
try:
import openpyxl
except ImportError:
print("⚠️ openpyxl 未安装,正在安装...")
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'openpyxl'])
import openpyxl
# ② 检测操作系统 + 字体映射
os_name = platform.system().lower()
FONT_MAP = {
'黑体': {'windows': ['黑体', 'SimHei'], 'darwin': ['Heiti SC', 'PingFang SC'], 'linux': ['Noto Sans CJK SC', 'WenQuanYi Micro Hei']},
'仿宋': {'windows': ['仿宋', 'FangSong'], 'darwin': ['STFangsong', 'Songti SC'], 'linux': ['Noto Serif CJK SC']},
'Times New Roman': {'windows': ['Times New Roman'], 'darwin': ['Times New Roman', 'Times'], 'linux': ['Times New Roman', 'Liberation Serif']},
}
def resolve_font_name(name):
"""将字体名映射为当前系统可用字体"""
candidates = FONT_MAP.get(name, {}).get(os_name, [name])
return candidates[0], (candidates[0] != name)
title_fn, _ = resolve_font_name('黑体')
body_cn_fn, is_alt = resolve_font_name('仿宋')
body_en_fn, _ = resolve_font_name('Times New Roman')
if is_alt:
print(f"⚠️ 字体替代: 仿宋 → {body_cn_fn}")
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "股权分配表"
# === 样式定义 ===
title_font = Font(name=title_fn, size=20, bold=True)
title_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
header_font = Font(name=title_fn, size=12)
header_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
body_font_cn = Font(name=body_cn_fn, size=12) # 中文兜底
body_font_en = Font(name=body_en_fn, size=12) # 英文/数字
body_align_center = Alignment(horizontal='center', vertical='center', wrap_text=True)
body_align_left = Alignment(horizontal='left', vertical='center', wrap_text=True)
highlight_font = Font(name=body_cn_fn, size=12, bold=True, color='C00000')
highlight_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
category_font = Font(name=body_cn_fn, size=12, bold=True, color='C00000')
category_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
category_align = Alignment(horizontal='left', vertical='center')
thin_border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin')
)
# === 行高估算函数(v3精确版) ===
def calc_row_height(text, col_width_units, font_size=12):
"""计算单元格所需行高(pt)。中文字符2.0单位,英文/数字0.55,安全系数1.06。"""
if not text:
return 20
total_width = 0
for ch in str(text):
cp = ord(ch)
if cp > 127:
total_width += 2.0
elif cp in (ord('('), ord(')'), ord('《'), ord('》')):
total_width += 0.8
else:
total_width += 0.55
effective_width = max(col_width_units * 0.92, 3)
lines = math.ceil(total_width / effective_width)
line_height = font_size * 1.4
height = lines * line_height + 2
return max(18, int(height * 1.06))
# === 布局参数 ===
last_col = 10 # J列
header_row = 3
last_row = 20 # 假设数据到第20行
# === 1. 先设列宽(行高依赖列宽,必须先设) ===
col_widths = {'A': 6, 'B': 12, 'C': 15, 'D': 15, 'E': 14, 'F': 10,
'G': 14, 'H': 18, 'I': 12, 'J': 25}
for col, width in col_widths.items():
ws.column_dimensions[col].width = width
# === 2. 标题 ===
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=last_col)
ws['A1'] = 'XX企业股权分配表'
ws['A1'].font = title_font
ws['A1'].alignment = title_align
ws.row_dimensions[1].height = 30
# === 3. 副标题行(公司名 + 日期) ===
ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=4)
ws['A2'] = '公司:XX有限公司'
ws['A2'].font = Font(name='仿宋', size=12, color='C00000')
ws['A2'].fill = highlight_fill
ws['A2'].alignment = body_align_left
ws.merge_cells(start_row=2, start_column=last_col-1, end_row=2, end_column=last_col)
ws[f'{get_column_letter(last_col-1)}2'] = '设计日期:2025年3月30日'
ws[f'{get_column_letter(last_col-1)}2'].font = Font(name='仿宋', size=12)
ws[f'{get_column_letter(last_col-1)}2'].alignment = body_align_left
ws.row_dimensions[2].height = 20
# === 4. 表头 ===
headers = ['序号', '姓名', '部门', '职位', '入职日期', '工龄(年)',
'月薪(元)', '拟分配股份数(万股)', '股份占比', '备注']
for col_idx, h in enumerate(headers, 1):
cell = ws.cell(row=header_row, column=col_idx, value=h)
cell.font = header_font
cell.alignment = header_align
cell.border = thin_border
ws.row_dimensions[header_row].height = 22
# === 5. 数据行 ===
# 示例数据
data = [
[1, '张三', '市场部', '市场总监', '2018-03-15', 7, 25000, 15.5, 0.0825, '核心骨干'],
[2, '李四', '市场部', '高级经理', '2019-07-01', 6, 18000, 8.0, 0.0425, ''],
# ... 更多数据
]
# ===== 第一步:全表兜底 —— 所有数据单元格先统一设仿宋 =====
for row_idx in range(header_row + 1, last_row + 1):
for col_idx in range(1, last_col + 1):
cell = ws.cell(row=row_idx, column=col_idx)
cell.font = body_font_cn # 兜底:先全部仿宋
cell.border = thin_border
cell.alignment = body_align_center
# 姓名列(B)和备注列(J)左对齐
ws.cell(row=row_idx, column=2).alignment = body_align_left
ws.cell(row=row_idx, column=10).alignment = body_align_left
# ===== 第二步:按列覆盖 —— 数字/日期列覆盖为 Times New Roman =====
numeric_cols = [1, 5, 6, 7, 8, 9] # 序号、入职日期、工龄、月薪、股份数、占比
for col_idx in numeric_cols:
for row_idx in range(header_row + 1, last_row + 1):
ws.cell(row=row_idx, column=col_idx).font = body_font_en
# ===== 填入数据 =====
for row_data in data:
row_idx = row_data[0] + header_row
for col_idx, value in enumerate(row_data, 1):
cell = ws.cell(row=row_idx, column=col_idx, value=value)
# === 6. 数字格式 ===
for row_idx in range(header_row + 1, last_row + 1):
ws.cell(row=row_idx, column=5).number_format = 'YYYY-MM-DD' # 入职日期
ws.cell(row=row_idx, column=7).number_format = '#,##0' # 月薪
ws.cell(row=row_idx, column=8).number_format = '#,##0.00' # 股份数
ws.cell(row=row_idx, column=9).number_format = '0.00%' # 占比
# === 7. 合计行(示例:第 last_row 行) ===
sum_row = last_row
ws.cell(row=sum_row, column=2, value='合计')
for col_idx in [7, 8]: # 月薪、股份数合计
col_letter = get_column_letter(col_idx)
ws.cell(row=sum_row, column=col_idx, value=f'=IFERROR(SUM({col_letter}{header_row+1}:{col_letter}{sum_row-1}),"-")')
ws.cell(row=sum_row, column=col_idx).number_format = '#,##0.00'
# 合计行样式:高亮
for col_idx in range(1, last_col + 1):
cell = ws.cell(row=sum_row, column=col_idx)
cell.font = highlight_font
cell.fill = highlight_fill
cell.border = thin_border
# === 8. 行高自适应 ===
for row_idx in range(header_row + 1, last_row + 1):
max_height = 18
for col_idx in range(1, last_col + 1):
cell = ws.cell(row=row_idx, column=col_idx)
if cell.value is not None:
col_letter = get_column_letter(col_idx)
col_width = col_widths.get(col_letter, 12)
h = calc_row_height(str(cell.value), col_width)
max_height = max(max_height, h)
ws.row_dimensions[row_idx].height = max_height
# === 9. 打印设置 ===
ws.print_area = f'A1:{get_column_letter(last_col)}{last_row}'
ws.page_setup.orientation = 'landscape'
ws.page_setup.paperSize = ws.PAPERSIZE_A4
ws.page_setup.fitToWidth = 1
ws.page_setup.fitToHeight = 0
ws.sheet_properties.pageSetUpPr.fitToPage = True
ws.freeze_panes = f'A{header_row + 1}'
wb.save('output.xlsx')五、审阅检查清单
当审阅已有Excel文件是否符合博维标准时,按以下清单逐项检查:
- 字体一致性:是否所有单元格都有显式字体?有无遗漏导致回退到 Calibri 默认字体?是否采用了"先兜底仿宋、再按列覆盖 TNR"的策略?
- 字体类型:中文是否为仿宋?英文/数字是否为Times New Roman?表头是否为黑体?标题是否为黑体加粗?分类标题行是否为仿宋加粗+暗红?
- 字号:标题16-24号?正文9-12号?
- 行高:所有行是否设置了显式行高?wrap_text 的单元格行高是否足以显示全部内容?有无文字被截断?列宽是否先于行高设置?
- 对齐:标题是否跨列居中?表头是否居中?姓名是否左对齐?数值是否居中?是否统一垂直居中?是否启用 wrap_text?
- 颜色:重要数据/合计行/分类标题行是否使用浅黄背景+暗红字体?是否加粗?
- 边框:数据区是否统一四边细线?
- 数字格式:金额是否有千分符?百分比格式是否正确?日期格式是否统一?
- 错误值:是否存在
#DIV/0!、#N/A、#REF!等错误?0值是否显示为"-"?公式是否包裹 IFERROR? - 公式标注:列间有逻辑关系的是否标注了公式?
- 表头标注:是否清楚标注了单位(元、万元、年、%等)?
- 布局:公司名称、日期位置是否正确?表头层级是否清晰?
- 打印:页面方向是否合理?是否能在A4纸上清晰打印?
- 工作底稿:数据是否分区?是否使用链接取数?辅助列是否有标注?
六、反例场景:不当使用与禁止做法
以下场景不应使用本技能,或必须做特殊处理:
6.1 系统无指定字体
场景:用户在 macOS/Linux 上生成表格,系统无"黑体""仿宋"字体。
错误做法 ❌:直接使用 Font(name='黑体') → 生成的 Excel 文件在 macOS 上显示为默认字体,排版混乱。
正确做法 ✅:
- 调用
resolve_font_name('黑体')→ 自动映射为 Heiti SC - 生成表格时使用映射后的字体名
- 在表格说明行标注:"⚠️ 当前系统使用 Heiti SC 替代黑体,如需原始字体效果,请安装对应字体"
6.2 输入不是表格数据
场景:用户提供了一段文本或 JSON 数据,要求"做成表格"但数据无明确的列结构。
错误做法 ❌:强行拆分成列,生成无意义的表格。
正确做法 ✅:
- 暂缓生成,先向用户确认列定义:
"请明确以下信息后我再生成:① 表头有哪些列?② 每列对应的数据是什么?③ 标题是什么?"
- 用户确认后再按规范生成。
6.3 大规模数据(>10000行)
场景:用户提供了超过 10000 行数据要求格式化为博维表格。
错误做法 ❌:逐行设置样式导致脚本运行超时或内存溢出。
正确做法 ✅:
- 警告用户数据量过大:"当前数据 {N} 行,逐行设置样式预计耗时 {T} 分钟,建议使用专业 ETL 工具处理。"
- 如用户坚持,采用批量样式(
ws.rows一次性设置)代替逐单元格设置 - 分批次保存,每 3000 行保存一次中间结果
6.4 冲突的格式要求
场景:用户同时要求"字体放大到 16 号"和"每页必须放下所有列"。
错误做法 ❌:静默选择一个,忽略另一个。
正确做法 ✅:
- 向用户指出冲突:"放大字号会导致列宽增加,当前 A4 横向打印可能放不下所有列。请选择:① 保持 16 号,列可能被压缩或跨页;② 降回 12 号,确保一页内显示所有列;③ 改为 A3 打印。"
- 等待用户选择后再继续。
6.5 修改他人制作的 Excel 文件
场景:用户上传了一个拼凑了多种字体、多种对齐方式的 Excel 要求"按博维标准改"。
错误做法 ❌:直接覆盖样式 → 可能破坏原始文件中的公式、数据验证或条件格式。
正确做法 ✅:
- 先另存副本,对副本操作
- 先读取并报告当前文件的问题:"检测到以下不合规项:① 5处使用 Calibri 字体;② 3行未设置垂直居中;③ 2个公式未包裹 IFERROR……"
- 让用户确认:"是否按清单逐项修正?"
- 修正后保存为新文件(不覆盖原文件)
七、操作暂停点
在以下节点,必须暂停并等待用户确认,不得自动继续:
| 暂停点 | 触发条件 | 等待用户确认的内容 |
|---|---|---|
| 🛑 P1 | 检测到 openpyxl 未安装且自动安装失败 | 是否手动安装后重试 |
| 🛑 P2 | 当前系统无指定字体(macOS/Linux) | 是否接受替代字体映射 |
| 🛑 P3 | 列宽溢出(某单元格换行超过 5 行) | 是否接受建议的列宽调整 |
| 🛑 P4 | 数据量超过 5000 行 | 是否继续或分批处理 |
| 🛑 P5 | 修改已有文件 | 是创建副本修改还是直接修改 |
| 🛑 P6 | 格式要求之间有冲突(如字号 vs 列数) | 选择优先满足哪个要求 |
| 🛑 P7 | 生成完成 | 是否需要调整列宽/行高/字体 |
暂停点执行规则:
- P1-P6:生成前确认,不确认不开始生成
- P7:生成后确认,不确认不视为完成
- 每次暂停用清晰的一二三四列出选项,让用户一句话选择
八、测试 prompt 用例
以下 prompt 可用于验证本技能是否正常工作:
测试 1:基础表格生成
输入:
帮我做一个员工信息表,包含序号、姓名、部门、职位、入职日期、月薪6列,数据随便填5行
期望输出:
- 标题:黑体20号加粗居中
- 副标题:公司名(仿宋暗红浅黄)+ 日期(仿宋右对齐)
- 表头:黑体12号居中四边细线
- 数据区:姓名/部门/职位 → 仿宋;序号/入职日期/月薪 → TNR
- 数字格式:月薪有千分符,日期 YYYY-MM-DD
- 行高自适应
测试 2:字体跨平台检测
输入:
(在 macOS 或 Linux 上)帮我做一个简单的表格,标题"项目进度表",3列:序号、任务、完成日期
期望输出:
- 使用 Heiti SC / Noto Sans CJK SC 替代黑体
- 使用 STFangsong / Noto Serif CJK SC 替代仿宋
- 在表格中有字体替代说明
测试 3:已有文件审阅
输入:
检查这个 Excel 文件是否符合企业制表规范,帮我列出所有不合规的地方
期望输出:
- 先另存副本
- 逐项检查 14 项清单
- 列出每条不合规项的具体位置(哪个Sheet、哪行哪列)
- 询问是否逐项修正