田字格听写纸生成
用 python-docx 生成「拼音在上、正方形田字格在下」的 printable 听写卷。
完整可运行实现见 scripts/generate_worksheet.py(含中文生词数据,改 LESSONS 即可复用)。
何时使用
- 生成小学语文生词听写纸、写字练习卷、词语默写卷
- 要求每字一个田字格,且格宽 = 格高(正方形,不能是长方形)
- 要求控制页数(「控制在 2 页内」「做满 2 页」「不必强制占满」)
- 需要拼音与格子强绑定,绝不跨页拆开
- 排查 Word 页数溢出、空白页、单页留白过多、格子大小不统一
核心实现:田字格 = 3 行表格
每行听写格用一个 3 行表格:
| 行 | 内容 | 高度 |
|---|---|---|
| 行0 | 拼音(每字 merge 2 列 → gridSpan=2) |
PINYIN_H |
| 行1 | 格子上半 | box/2 |
| 行2 | 格子下半 | box/2 |
正方形关键:列宽 = box/2,行高 = box/2,2 列 × 2 行 = box × box。宽高由同一 box 推导,从数学上保证不偏。
边框图案(外框实线 + 内部十字虚线):每字占 2 列,half=0/1 表示左半/右半:
- half=0:左 single、右 dashed
- half=1:左 dashed、右 single
- 行1(上半):上 single、下 dashed;行2(下半):反之
⚠️ 必须用半格索引 half 判断虚实,不能用绝对列号 ci % 2——词间隔列会打乱奇偶,导致其后的格子边框整体错位。
五个必踩的坑(全部踩过)
坑 1:空段落高度由「段落标记」决定,不是 run
表格之间的空段落做行间距时,只给空 run 设字号无效:
# 无效:高度仍继承 Normal(10.5pt)
sp = doc.add_paragraph(); sp.add_run(""); sp.runs[0].font.size = Pt(3)
空段落高度由段落标记 w:pPr/w:rPr/w:sz 决定。21 个空段落按 10.5pt 算会多占约 7.3cm,直接多出一页。
坑 2:w:rPr 必须按 schema 顺序插在 spacing/jc 之后
用 pPr.insert(0, rPr) 插到最前,Word 会静默忽略该元素(不报错、不提示)。
CT_PPr 顺序为 ... spacing, ind, jc, ..., rPr, sectPr。必须按序插入:
PPR_ORDER = ['pStyle','keepNext','keepLines','pageBreakBefore','framePr',
'widowControl','numPr','suppressLineNumbers','pBdr','shd','tabs',
'suppressAutoHyphens','kinsoku','wordWrap','overflowPunct','topLinePunct',
'autoSpaceDE','autoSpaceDN','bidi','adjustRightInd','snapToGrid','spacing',
'ind','contextualSpacing','mirrorIndents','suppressOverlap','jc',
'textDirection','textAlignment','textboxTightWrap','outlineLvl','divId',
'cnfStyle','rPr','sectPr','pPrChange']
def insert_in_order(pPr, el):
idx = PPR_ORDER.index(el.tag.split('}')[1])
for child in pPr:
t = child.tag.split('}')[1]
if t in PPR_ORDER and PPR_ORDER.index(t) > idx:
child.addprevious(el); return
pPr.append(el)
坑 3:表格防拆页要从首行加到倒数第二行
keepNext 语义是「与下一段同页」。只给行0 加只能保证 行0→行1 不拆,行1→行2 仍会断开 → 表现为「拼音在上一页、格子在下一页」。
必须给行0、行1 的所有单元格段落(含词间隔列)都加 keepNext,行2 是末行不加。
另:w:cantSplit(防单行内拆)要排在 w:trHeight 之前(trPr schema 顺序)。
坑 4:改词数或分页方案后必须重算格子尺寸
约束页是哪一页会随行数变化转移。补几个词可能让约束从第 1 页转到第 2 页,沿用旧 BOX 必然溢出。每次都要重算。
坑 5:「铺满整行」与「格子统一」本质冲突
若按「铺满 18.4cm 版心」反推每行格宽,词数少的行格子会明显偏大。
优先满足视觉一致性:把 box 做成 add_line() 的可选参数,需要时只调全局 BOX 一个值全局生效,不要按行自动放大。
无渲染环境时的页高核算
若无可用的 Word / LibreOffice,无法渲染看页数,必须从 XML 反推:
- 表格行高:
w:trHeight值 ÷ 567 = cm - 段落高:
字号 × 1.32 × 2.54/72 + space_before + space_after - 段落字号取「段落内 run 最大字号」与「段落标记字号」的较大者;都没有则按 10.5pt
- 逐元素累计,遇
w:br type=page切页
反推 BOX:
BOX ≤ (usable - 非格子高度) / 该页行数 - PINYIN_H
非格子高度 实测约 3.53cm(含标题区的页)/ 1.30cm(后续页)。再留 0.03~0.05cm 安全余量(Word 实际渲染常略高于估算)。
配套 gap = (usable - 2×PER_ROW×BOX) / (PER_ROW - 1) 保证整行铺满。
⚠️ 两页内容量不均时,填不满是物理上限,不要拉伸填满——放大格子会让内容多的那页溢出。
应如实告知占用率,并给出「若想更饱满可把 BOX 提到多少」的参考值。
排版布局模式
- 短词(2字):每行
PER_ROW=5个,10 格 + 4 间隔 = 铺满版心 - 长词(3-4字):
LONG_PER_ROW=2个并排一行。两个并排比每词独占一行省一半行数,是压缩页数的有效手段 - 分页:
PAGE_BREAK_BEFORE是 LESSONS 的下标,不是课次数字。例:在第6 课(下标 2)前分页 → 第1页=4+5课,第2页=6+7课。⚠️ 用错会导致溢出或留白异常
交付前必查(7 项)
# 1) 正方形:格宽 == 格高(逐行)
w = (gc[0].w + gc[1].w) / 567
h = (row1.trHeight + row2.trHeight) / 567
assert abs(w - h) < 0.01
# 2) 格子尺寸全卷统一:所有行 box 的集合应只有 1 个元素
# 3) keepNext 覆盖:行0、行1 全部单元格有,行2 为 0
# 4) pPr 顺序:'spacing' 必须在 'rPr' 之前
# 5) trPr 顺序:'cantSplit' 必须在 'trHeight' 之前
# 6) 音节总数 == 汉字总数(发现漏词/多词最直接的指标)
# 7) 每页高度 <= 可用高度,且总页数符合预期
⚠️ 第 6 项特别重要:本项目曾因漏读课本词语表边角的 3 个生词而出错。
「音节数 == 汉字数」能立刻发现漏词。核对课本图片时用:逆时针旋转 90°(顺时针会得到镜像假象)
+ 逐行裁剪 + LANCZOS 放大 4倍 + 对比度增强,必要时用像素投影定位文本行边界。
使用步骤
- 复制
scripts/generate_worksheet.py到工作目录 - 替换
LESSONS为目标词表(格式:(带声调拼音, 汉字)) - 若要改页数/格子:只调
BOX、PER_ROW、LONG_PER_ROW、PAGE_BREAK_BEFORE四个常量 - 运行后必须执行上面 7 项校验
- 提醒用户打印时选 A4、缩放 100%、「实际大小」(非「适应页面」)
- 输出文件名带版本号;生成新版前提醒用户清理旧版(被 Word 占用时脚本删不掉)
变体:纯黑白 + 每页独立文件
当用户要求「纯黑白」(省墨/单色打印)或「每页单独一个 Word」(双面打印)时,
参考 scripts/generate_bw_pages.py。两个关键改动:
1. 纯黑化
把所有彩色常量统一为纯黑,避免灰度偏色:
BLACK = RGBColor(0x00, 0x00, 0x00)
DARK = RGBColor(0x00, 0x00, 0x00) # 原 0x333333
GRAY = RGBColor(0x00, 0x00, 0x00) # 原 0x666666
田字格边框色本就是 '000000',无需改。
校验:遍历 w:color 属性,颜色集合应 ⊆ {'000000'}。
2. 每页新建 Document,替代「分页符」
PAGE_GROUPS = [(0, 2), (2, 4)] # [(起始下标, 结束下标)] × 页
for i, (s, e) in enumerate(PAGE_GROUPS, 1):
doc = Document() # 每页独立新建,不复用
... # 插入该页的课次
doc.save("..._第%d页.docx" % i)
单页文件里不再插 w:br type=page——分页符只用于单文档多页场景。
3. 仅首页保留标题栏
第2 页只放课次标题,不重复「姓名/日期/得分」栏。这样双面打印时背面是纯练习区,
且两页页面设置一致(A4、边距相同)可整齐叠放对齐。
校验脚本
scripts/verify_output.py 是可直接复用的交付前把关工具,覆盖 7 项检查:
页高是否溢出、颜色是否纯黑、正方形是否达标、格子边长是否统一、
keepNext 强绑定、边框虚实是否正确、音节数是否等于汉字数。
用法一:校验任意文件(推荐,不改代码)
python verify_output.py 听写纸A.docx 听写纸B.docx
命令行传入文件后会自动跳过「汉字数比对」(因为无法预知总字数),
其余 6 项检查照常执行,空页面也不会报错。
用法二:校验固定配置
修改脚本顶部 FILES 与 EXPECT_CN 后直接 python verify_output.py,
即可启用音节数 == 汉字数的比对。
⚠️ 校验脚本中不要在模块顶层写 return/break 逃出循环(会 SyntaxError),
应把逻辑放进函数或显式循环块内;空列表要先判空再取 [0]。