| 名称 | excel-to-word |
| 版本 | 2.3.3 |
| 描述 | “把 Excel 工作表/工作簿转换为 Word 原生表格,同时保留电子表格自身的字体(包括中文 eastAsia 字体)、字号、颜色、单元格底纹、边框、对齐、合并、行高、列宽和数字格式。单一 Python 入口(openpyxl + python-docx),不走 VBA/宏路线:带按工作表分节的 CLI、打印区域检测、纸张大小自动选择、按方向固定的页边距(横向 上2.5/下2.0/左2.0/右2.0cm,纵向 上2.0/下2.0/左2.5/右2.0cm)、表格根据窗口调整、列加宽使数字单元格永不换行,以及用于回读检查的 verify/diag 配对。包含初设概算(模板)参考文档的实测版式规范:A4 纵向 + A4 横向分节、宋体 9-10pt、每单元格 0.5pt 边框、atLeast 行高、重复表头行、全宽合并标题行。用于工程概预算/估算/概算/工程量清单 Excel 转 Word 交付件、保留原格式、固定页边距、根据窗口调整表格、Excel 表格转 Word 排版、多方案表拆分、按打印区域导出、逐格校验。区别于 excel-to-word-template,后者复现的是参考 Word 文档的版式,而不是电子表格自身的外观。” agent_created: true |
Excel → Word(保留 Excel 自身格式)v2.3.3
把 Excel 表转成 Word 原生表格,字体、字号、底纹、边框、对齐、合并、列宽行高、数字格式全部保留。
不去套 Word 模板——要套模板用 excel-to-word-template。
详细操作手册见 使用说明.md(给使用者);本文件是给 agent 的实现约定与决策依据。
唯一路线:Python CLI
本技能只走 Python 路线,不提供 VBA 宏。原因(都是实测得出的):
| VBA 宏的硬伤 | 造价表的真实情况 |
|---|---|
| 不认打印区域 | 作者常把「差额」「上一版」列设在打印区外,宏会一起导 |
| 不处理隐藏行列 | 概算表普遍藏几十到几百行辅助计算行 |
| 不能一表拆两段 | 方案一/方案二上下排在同一个工作表里是常态 |
| 不能控纸型与缩放 | 19 列表在 A4 横向要缩到 7.5pt,需要判 A3 |
| 不能重跑/批处理 | 改一版就要重新点一遍按钮 |
| 数字依赖剪贴板 | 无法逐格回读校验,交付前没法机器验收 |
Python 路线用同一份 openpyxl 数据既写 docx 又做校验,改完能重跑、交付前能自检。
| 干什么 | 命令 |
|---|---|
| 看结构(先跑这个) | python scripts/excel_to_word.py 概算.xlsx --diag-only |
| 转换 | python scripts/excel_to_word.py 概算.xlsx |
| 交付前校验 | python scripts/verify_docx.py 概算.xlsx 概算.docx |
依赖:pip install openpyxl python-docx。旧版 VBA 宏留在 _backup/ 仅作历史存档,不要再用。
Excel 侧前提(转之前先确认,否则一定多东西或少东西)
转换是「如实搬运」,不替用户做取舍。三个信号决定结果,命令行不会报错:
| 信号 | 决定的 | 没设好的后果 |
|---|---|---|
| 打印区域 | 取哪一块 | 打印区外的「差额」「上一版」「辅助列」不会导出;整表没设打印区域则退回整表使用区域,草稿列一起进来 |
| 工作表隐藏 | 转哪几张 | 明细、底稿、参考资料全部转进 Word(--include-hidden 才导隐藏表) |
| 隐藏行 / 隐藏列 | 辅助数据走不走 | 几百行辅助计算全部进版心 |
另外两件顺手事:Excel 里设「打印标题 → 顶端标题行」可控制跨页重复表头(不设则自动猜,易偏); 首行不是表名行时 Word 标题取工作表标签名,标签叫「控」标题就是「控」。
一个工作表竖着堆多张表是常态(「水工估」第 1 行是炸礁表、第 21 行又是水工建筑工程表)。 转换器会自动按表名行切开,每张表独立成节、独立表名、独立跨页表头—— 不切的话第二张表的表名行和列标题行会落在表格中间,看起来就是「标题行重复」。
所以拿到文件先跑 --diag-only,看 区A1:M24 可见 24行×13列 是否等于用户想导的范围,再动手转。
打印区域不方便改时,用 --sheets "名!A1:H86@4" 白名单绕开(但它不持久,改版重跑要重带)。
Python 路线
三条命令覆盖 90% 场景
# 1) 先诊断:列出每张表的取数区域、可见行列、宽度、建议纸型、缩放后字号
python scripts/excel_to_word.py 概算.xlsx --diag-only
# 2) 直接转(默认按打印区域取数、每表一节、纸型自动、页码自动)
python scripts/excel_to_word.py 概算.xlsx
# 3) 一表两段 + 指定表头行数(方案一 / 方案二在同一个工作表里上下排列时)
python scripts/excel_to_word.py 概算.xlsx -o 概算.docx \
--sheets "汇总,水工!A1:I86@4,水工!A88:I163@4,建!A1:G10@4,建!A12:G18@4"
参数
| 参数 | 默认 | 说明 |
|---|---|---|
-o/--out |
源文件同名 .docx |
输出路径 |
--sheets |
全部可见表 | 名1,名2!A1:H86@4;!区域 在打印区域内再收窄(不能扩大),@N 指定该段表头行数 |
--area |
auto |
auto=有打印区域就用+裁尾部空列空行;print=照打印区域不裁;used=整个使用区域(唯一能突破打印区域的开关) |
--orient |
auto |
auto 按宽度选纵向/横向;也可强制 portrait/landscape;excel=严格照 Excel 页面设置的方向,不按表宽自动改 |
--no-scale |
关 | 字号严格照 Excel,不做等比缩放;边距未固定时先压左右边距吸收,边距固定时改压列宽(见下) |
--margin-preset |
cost |
cost=横/纵各一套固定边距(横 2.5/2.0/2.0/2.0,纵 2.0/2.0/2.5/2.0 cm,顺序上/下/左/右,不随表宽变动);sym=用 --margin/--margin-v |
--margin-min |
0.8 |
仅 --margin-preset sym + --no-scale 时有效:左右边距可压到的最小值 cm |
--no-fit-window |
关 | 不设「根据窗口调整表格」。默认设:每表 tblW=100% + tblLayout=autofit,列宽等比归一到版心宽 |
--no-widen |
关 | 禁止「为放不下的数字加宽列」(默认会加宽,见下) |
--paper |
auto |
A3 = 放不下时允许升 A3 兜底(不是强制 A3) |
--min-font |
7.5 |
缩放后正文字号低于此值告警(不阻断) |
--repeat-header |
0(自动) |
>0 强制行数;0 时按 打印标题行 > 冻结窗格 > 「整行无数字」识别 |
--margin / --margin-v |
2.5 |
左右 / 上下页边距 cm,仅 --margin-preset sym 时生效 |
--row-height |
atleast |
atleast 按内容撑高不裁字;exact 复刻 Excel 固定行高(会裁字) |
--sheet-title |
auto |
首行已是合并表名行时不另加标题段 |
--page-number / --no-page-number |
开 | 页脚居中连续页码 |
--header "项目名" |
空 | 页眉文字 |
--include-hidden |
关 | 连隐藏工作表一起导(默认跳过;隐藏表即使被 --sheets 点名也不导) |
--cant-split |
关 | 禁止数据行跨页断开(样张不设) |
--diag-only |
— | 只打印结构诊断,不生成 docx |
--exact-row-height |
— | 等同 --row-height exact(兼容 v1 写法) |
输出怎么读
[ 4] 水工 A4L 行 163 列 9 宽 675pt 缩放100.0% 表头4行 缩字0格 区1-163
↑纸型 ↑行数 ↑列数 ↑原表宽 ↑等比缩放 ↑重复表头 ↑为保单行缩了字的格数
缩放 < 85%或提示「字号 < --min-font」→ 考虑--paper A3或拆表表头N行明显偏大/偏小 → 用@N指定(从表头首行起算)表头N行大到接近整表(如 71 行)→ v2.3.1 已修(冻结窗格会被当成表头提示), 若仍出现请回报该表名区a-b与预期不符 → 检查 Excel 打印区域,或加--area used
处理流程(v2)
诊断区域(打印区∩有值范围) → 按表名行切块(一表多表) → 定纸型/缩放系数 s
→ 逐格写内容+样式(noWrap 按 Excel 的 wrap_text)
→ 合并 → 清空段落 → 整表等比缩放(列宽/行高/字号 ×s)
→ 内边距自适应 → 不换行格缩字保单行
→ 表名行拆成无框表前标题块 → 表头竖合并展平 → 连续标记重复表头 → 分节+页码
整表等比缩放是关键决策:等价 Excel「调整为 1 页宽」,列宽/行高/字号同比例缩, 版面比例不变形。不再单独压列宽(v1 那样压列宽会把数字挤成两行)。
交付件要求「字号一分不让」时:--orient excel --no-scale
造价所审图常要求字号与 Excel 完全一致、纸张方向也照原表。此时用:
python scripts/excel_to_word.py 概算.xlsx --orient excel --no-scale
三件事会变,其余不变:
- 方向取 Excel 页面设置(
ws.page_setup.orientation),不再按表宽自动翻。 - 不缩放,字号/列宽/行高都是 Excel 原值。表比版心宽时:
- 边距未固定(
--margin-preset sym)→ 先压左右边距吸收(压到--margin-min); - 边距固定(默认
cost)→ 改压列宽:squeeze_columns按各列余量分摊挤掉的量, 数字/单元符号列守住单行最小宽度,只压项目名称、特征描述这类文字列; 连数字单行都保不住时才退化为等比缩并告警。
- 边距未固定(
- 数字不再被拆行:Excel 金额列常设「自动换行 + 行高锁死」,数字实际是截断显示的;
搬到 Word 会变成
490/5572/.14竖排。现在会把该列补宽到数字单行所需(余量从页面 空处腾),并打上noWrap——宁可列宽/行高变大,也不挤数字。 短单元符号(万m3)同理,否则一行变两行会把整表高度顶超一页。 - 单表差一两行就排满一页时,按比例压行高(不低于 0.7,atLeast 仍保证不裁字), 避免「工程费用合计」被甩到孤零零的第二页。压的是行高不是字号。
- 表格设为「根据窗口调整表格」:
tblW=100%+tblLayout=autofit, 列宽先等比归一到版心宽,Word/WPS 拿到的是「已经刚好铺满」的列宽,不会再二次分配 (否则刚加宽的金额列会被压回两行)。 例外:压过列宽的表(squeeze_columns生效)改用固定布局tblW=版心宽(dxa)+tblLayout=fixed。autofit 下 Word 会按内容重新分配宽度,把「序号」「计量单位」 这类窄列压成竖排、把030402009001和134613.4拆成两行(南沙项目电2 真实事故)。
--no-scale 下 shrink_nowrap 只放开换行、绝不缩字。想恢复旧行为就去掉这两个开关。
交付件要求固定页边距(横向/纵向各一套)
python scripts/excel_to_word.py 概算.xlsx --orient excel --no-scale # 默认即 --margin-preset cost
| 方向 | 上 | 下 | 左 | 右 | 版心(A4) |
|---|---|---|---|---|---|
| 横向 | 2.5 | 2.0 | 2.0 | 2.0 | 宽 25.7cm ≈ 728.5pt |
| 纵向 | 2.0 | 2.0 | 2.5 | 2.0 | 宽 16.5cm ≈ 467.7pt |
- 四个值分别写,不假设左右对称;页眉/页脚距自动压到
min(0.8, 边距-0.4), 否则页脚距 > 下边距时 Word 会把版心底边卡在页脚位置(下边距形同虚设)。 - 「固定」的确切含义:表比版心宽时绝不动边距,只压列宽或整表缩放。
--margin-preset cost下的--margin-min不生效(那是sym的口子)。 - 横纵版心差了 5.3cm,横向能铺下的表换到纵向就会超宽。用
--diag-only先看表宽 / 版心,超出的表会打「从列宽压掉 Npt」提示,属预期。
坑(WPS 渲染):tblW=100% 在 WPS 里按内容盒算宽,会把两侧单元格内边距算进去,
表格比版心宽 2×内边距,居中后左右各外溢一份。5.4pt 内边距 → 外溢 2.2mm;2pt → 0.8mm。
所以 --fit-window 打开时强制把内边距压到 2pt(set_cell_margins(40))。
Word 按边框盒算,本就是精确版心宽——两边的差异只在这 1mm 内。
版式判定
可见列宽合计 W,版心 U(p) = 纸宽 − 左 − 右(边距按 `page_margins(args, p)` 取,横纵各一套)
1) 列数 > --wide-col-limit(10) → A4 横向
2) W ≤ U(A4纵) × 1.04 → A4 纵向 ← 4% 容差,别为省几磅就翻页向
3) W ≤ U(A4横) → A4 横向
4) W ≤ U(A3横) 且 --paper A3 → A3 横向
5) 仍超 → A4 横向 + 等比缩到版心宽
可用宽速查:cost 预设下 A4 纵 481.9pt(17cm)/ A4 横 773.9pt(27.3cm);
cost(默认)版心:A4 横 728.5pt / A4 纵 467.7pt。
sym 2.5cm 对称边距下 A4 纵 453.5pt / A4 横 700.2pt / A3 横 1048.8pt。
每张表一个节(下一页分节符),所以同一份文档里 A4 纵的小表和 A4 横的宽表可以混排—— 参考样张就是这个结构。页码只在第 1 节设置,其余节链接到前一节。
硬规则(改代码前必读)
- 中文字体必须写
w:eastAsia,只设font.name中文不生效。 General格式按 10 位有效数字输出,绝不能int()取整(10.1→10、6.25→6 是真实事故)。- 金额用
Decimal ROUND_HALF_UP,round()银行家舍入会差 1 分。 - 行高规则属性是
w:hRule,写成w:h-rule会被 Word 忽略、exact失效。 w:tcPr / w:trPr / w:tblPr子元素必须按 schema 顺序插入,乱序会让 Word 报文档损坏。- 合并锚点判定用
(min_row,min_col)==(r,c);合并前先把锚点样式刷到区内所有格。 - 合并区必须裁剪到「窗口 ∩ 可见列」,否则
pos_c()抛 ValueError。 - 合并后清掉锚点里多余的空段落,否则行高虚高。
- 列宽无论
s是否为 1 都要写——python-docx 建表默认等宽。 w:tblHeader只认从表格首行起连续标记的行。想「只重复第 3、4 行」做不到—— Word 会连同前两行一起重复。做法是把表名行/工程信息行拆出表格(无框表 + 1pt 过渡段), 让列标题行成为新表的第 1 行。- 表头里的竖合并(「序号」跨两行这类)原样保留,不要拆。第 10 条已经把表名行移出
表格,表头只剩列标题行,实测 Word/WPS 跨页重复时
w:vMerge完整保留,续页表头仍是合并好 的两行。反过来把它们拆成「上格去下框、下格去上框」的无框拼接,在 Word 里看就是两个空格子 ——视觉上勉强连续,但用户能点中、能看出没合并(v2.3.3 真实返工)。 - 表前标题块要跟表格锁在同一页:
w:cantSplit只管行内不跨页,「与下段同页」必须在 块内所有单元格段落上加w:keepNext,否则表名会孤零零留在上页页底。 - 单元格值里的
\r要先归一成,否则 Word 里变双换行。 - 表间空段落与分节符寄生的空段落都要压到 0.1pt(
w:line="2"exact)。1pt 不够—— 表格排到版心底只剩 0.2pt 时仍会被挤出一张空白页(南沙五期真实事故)。 - 不要引入 Office COM / VBA 作为主路径——交付件必须能被
verify_docx.py逐格回读校验。
更多「症状 → 根因 → 修法」见 references/踩坑清单.md。
验收(交付前必过)
python scripts/verify_docx.py 概算.xlsx 概算.docx # 逐格回读 + 结构校验
检查项:表格数 = 期望表数;逐格内容差异 0(含 10.1、6.25 这类小数序号/数量);
表宽 ≤ 版心;每节纸型与预期一致;表头已连续重复;页脚有页码;无多余空白页。
需要肉眼确认时,用 WPS 表格把 xlsx 直接导成 PDF 做基准对比页数与列宽:
app = win32com.client.DispatchEx("KET.Application")
wb = app.Workbooks.Open(xlsx, ReadOnly=True)
wb.ExportAsFixedFormat(Type=0, Filename=pdf)
页数预期:Word 页数通常多于 Excel 打印稿(Excel 把行高锁死、超长文字被裁)。 珠海项目实测 96 页(Excel) → 179 页(Word 不裁字);南沙五期 145 页 → 199 页。 差额来自「文字显示完整」,不是 bug。
空白页自查(verify_docx.py 查不出来,必须过一遍 PDF):
import pymupdf
d = pymupdf.open(pdf)
for i, p in enumerate(d):
t = p.get_text().strip().replace(str(i + 1), "", 1).strip()
if len(t) < 6 and len(p.get_drawings()) < 3:
print("空白页", i + 1)
参考样张版式(实测值,见 references/参考样张-初设概算.md)
| 项 | 值 |
|---|---|
| 页边距 | 上下左右 2.5cm;页眉 1.5 / 页脚 1.75cm |
| 表宽 | 100% 可用宽,tblInd=0,表居中 |
| 表级边框 | 全 none;边框画在单元格上 |
| 单元格边框 | 单线 黑 0.5pt(sz=4) |
| 单元格边距 | 左右 5.4pt(108tw),上下 0;密集宽表压到 2pt |
| 垂直对齐 | 全部 center |
| 行高 | hRule=atLeast,255–480tw |
| cantSplit | 0 处(不禁止行跨页断开) |
| 跨页表头 | 2–4 行(12 列表 3 行,9 列表 4 行,8 列表 2 行) |
| 底纹 | 无彩色填充 |
| 表内第 1 行 | 跨全列合并的表名行,故不要另加标题段 |
| 字体 | ascii/hAnsi/eastAsia/cs 全「宋体」,表内 9–10pt,表名行 12–16pt |
格式映射表(逐项都踩过坑)
| Excel | openpyxl | Word OOXML | 坑 |
|---|---|---|---|
| 中文字体 | font.name |
w:rFonts w:eastAsia |
只设 font.name 中文不生效 |
| 字号 | font.sz |
w:sz |
半磅,10.5pt → 21 |
| 粗/斜/下划线/删除线 | b/i/u/strike |
w:b / w:i / w:u / w:strike |
|
| 字体色 | font.color.rgb |
w:color |
theme 色取不到 RGB → 兜底黑 |
| 底纹 | fill.fgColor |
w:shd w:fill |
渐变填充读不到,只拿纯色 |
| 边框 | border.X.style/color |
w:tcBorders/w:X |
没有的边必须写 val="nil" |
| 水平/垂直对齐 | alignment.h/v |
w:jc / w:vAlign |
general 按类型推断 |
| 自动换行 | alignment.wrap_text |
天然换行 | 关的时候要写 w:noWrap 并缩字保单行 |
| 合并 | merged_cells.ranges |
w:gridSpan / w:vMerge |
锚点判定、界外裁剪、空段落 |
| 列宽 | column_dimensions[X].width |
w:tblGrid + 每格 w:tcW |
1 字符 ≈ 5.25pt;gridSpan 要累加 |
| 行高 | row_dimensions[r].height |
w:trHeight + hRule |
属性名是 hRule |
| 数字格式 | cell.number_format |
渲染成字符串 | General 单独处理;Decimal ROUND_HALF_UP;¥/万元 前后缀 |
| 公式 | data_only=True |
取缓存值 | 未用 Excel 存过则缓存空 → 必须告警 |
| 隐藏行列 | hidden |
跳过 | 常藏辅助计算列;隐藏表的锚点要重锚 |
| 打印区域 | ws.print_area |
取数区域 | 可能是 str 或 list;常出现 A1:IV18 拉满整行 |
文件清单
SKILL.md 本文件(实现约定)
使用说明.md 给使用者的完整操作手册
CHANGELOG.md 版本记录
scripts/excel_to_word.py 主程序 v2.1.2(唯一路线)
scripts/verify_docx.py 逐格回读校验
references/参考样张-初设概算.md 版式基准实测数据
references/踩坑清单.md 症状→根因→修法
excel-to-word-离线版.html 浏览器离线版(单文件,双击即用,见下)
_backup/ 历史存档(v1.0 脚本、v1 VBA 宏),不要使用
离线网页版(excel-to-word-离线版.html)
把本技能完整搬进浏览器:单个 HTML 文件,约 113 KB,无任何外链、无网络请求, 双击用 Chrome / Edge 打开即用。适合把工具直接发给不用命令行、不装 Python 的同事。
- 交互:
上传 Excel 表按钮 → 自动解析并列出各表纸型/表宽/压缩率 →生成 Word→下载 Word - 实现:
zip.js(ZIP 读写,DecompressionStream('deflate-raw'))+xlsx.js(自研 xlsx 解析, 口径对齐 openpyxl)+ooxml.js(OOXML 元素树 + docx 打包)+convert.js(逐函数移植excel_to_word.pyv2.2.0 的排版决策),四段按序内联在同一个<script>里 - 与 CLI 等价性:同一份 xlsx,浏览器产物与 Python 产物逐字节一致(16 表 / 7050 格 /
48 页 /
verify_docx.py零差异),仅耗时不同 - 参数差异:网页默认「保留 Excel 字号 + 列宽口径=兼容(openpyxl)」,与 CLI 的
--orient excel --no-scale加同一colWidthMode等价;换口径时诊断数字会跟着变 - 构建:源码在项目
build/src/,node build/bundle.mjs把四个引擎文件内联进app.template.html的/*__ENGINE__*/占位,产出单文件