學術論文 docx 格式重整
何時用
- 需要把 Markdown 分析報告/表格轉成符合投稿規範的 docx
- 需要批量設定中西文混排、三線表、多級標題字號間距
- 逐段插入類 API 無法控制三線表邊框、run 級 rFonts、斜體拆分時,用本 skill
中文字號 → 磅值
| 字號 | 磅 |
|---|---|
| 三號 | 16 |
| 小三 | 15 |
| 四號 | 14 |
| 小四 | 12 |
| 五號 | 10.5 |
默認規範(可按目標期刊/學校要求覆蓋):一級標題小三黑體、二級四號黑體、 正文小四宋體 1.5 倍行距、西文一律 Times New Roman、表格五號宋體。
關鍵實現要點(踩過的坑)
1. 三線表邊框
w:tblBorders 的 w:sz 單位是 1/8 磅:
- 頂/底 1.5 磅 →
sz=12 - 中間線(表頭下方)1.0 磅 →
sz=8 - 豎線必須顯式
val="none",否則 Word 會畫默認豎線
表頭下方那條線要靠 row.cells[i] 逐格設 w:tcBorders/w:bottom,只設
tblBorders 的 insideH 在某些渲染器下不可靠。
2. cell.text = '' 會留下無格式空 run
清空單元格要用:
p = cell.paragraphs[0]
for r in list(p.runs):
r._element.getparent().remove(r._element)
否則 runs[0] 是一個無 rPr 的空 run,後續校驗字體時會誤判。
3. 中西文混排必須同時設 4 個字體槽位
rFonts.set(qn('w:ascii'), 'Times New Roman')
rFonts.set(qn('w:hAnsi'), 'Times New Roman')
rFonts.set(qn('w:eastAsia'), '宋體') # 中文字體
rFonts.set(qn('w:cs'), 'Times New Roman')
漏設 eastAsia → 中文回落到默認字體。
4. 斜體要拆 run
統計量(P<0.001、OR=2.17、CI、β、P 值)必須拆成獨立 run 才能單獨設 italic。
正則順序有講究:P值 要用 \bP(\s*值)\b 整體匹配;
先匹配 P\s*[<>=]\s*[\d.]+ 再匹配孤立的 P,否則 P=0.06 會被拆成 P + =0.06。
5. 「空一行」= 段落間距,不是空段落
絕對不要用回車(空段落)實現"空一行"。 空段落會在 Word 裡造成:
- 後續編輯時多出空行、無法乾淨刪除
- 列印/轉 PDF 時出現不確定的空白
- 與段前段後間距疊加後間距失控
正確做法——用 space_before / space_after(單位磅):
| 場景 | 實現 |
|---|---|
| 一級標題「上下各空一行」 | space_before = Pt(小四字號×1.5)、space_after = 同值 |
| 二級標題「上面空一行」 | space_before = Pt(小四字號×1.5)、space_after = Pt(0) |
| 三級及以下標題 | 不設間距 |
| 正文 | space_before = Pt(0)、space_after = Pt(0) |
| 圖/表與正文之間空一行 | 在圖/表所在段落上設 space_before/after,不另插空段 |
參考值:小四(12pt) 空一行 ≈ 18 磅;五號(10.5pt) 空一行 ≈ 15.75 磅。
校驗方式:sum(1 for p in doc.paragraphs if not p.text.strip()) == 0
——正常文檔不應存在任何空段落。
6. 禁止自動加粗
任何時候都不要給文字加粗,除非用戶明確要求。
這條極易踩坑:Markdown 裡的 **文字** 是源文件的強調標記,不是排版指令。
把它渲染成 Word 加粗,等於擅自給原文加重點。
正確做法:
**文字**→ 只取文字內容,不加粗- 需要分小標題時,用獨立段落 + 目標字體/字號,不要靠加粗實現層級
- 層級感只能來自:字體(黑體/楷體)、字號、段前段後間距、編號
同樣禁止的還有:斜體除指定的統計量/符號外不要用;底線、著重號、 彩色字、字元縮放/字間距——用戶沒要求的一律不動。
轉換完成後應自檢:
# 不應出現任何非用戶要求的 bold
bold = [r.text for p in doc.paragraphs for r in p.runs if r.font.bold]
表頭是否加粗看用戶要求;本 skill 默認表頭不加粗(靠字號+三線表區分)。
7. 三級及以下標題不空行
只有 1、2 級標題設段前段後間距;3 級及以下不加間距、不加粗。
腳本
| 腳本 | 用途 |
|---|---|
scripts/tri_line_table.py | 三線表邊框(含 verify_table_borders() 自檢) |
scripts/fmt_lib.py | 字體/間距/加粗段解析/單元格樣式 工具函式庫 |
scripts/fix_italic.py | 統計量斜體拆分(原地改,不重建 run) |
scripts/fix_spacing2.py | 中英文混排與符號間距修正(原地改,不重建 run) |
依賴:pip install python-docx
fmt_lib.parse_bold_segments(text, allow_bold=False) 默認丟棄但不渲染 ** 標記;
只有用戶明確要求加粗時才傳 allow_bold=True。
對已有 docx 做格式修正時的執行順序(不可反序):
python3 scripts/fix_italic.py 目標.docx # 先拆斜體
python3 scripts/fix_spacing2.py 目標.docx # 後修間距
反序會被間距規則把已拆開的符號重新拼錯。
校驗清單(生成後必查)
- 空段落數 == 0 —— 最高優先級,
sum(1 for p in doc.paragraphs if not p.text.strip()) - 加粗 run == 0(除非用戶要求)——
[r.text for p in doc.paragraphs for r in p.runs if r.font.bold]必須為空;表格內同樣檢查 - 一級標題 sz==15 且 eastAsia==黑體,space_before==space_after==18pt
- 二級標題 sz==14,space_before==18pt
- 正文 sz==12、eastAsia==宋體、line_spacing==1.5
- 所有 run 的 ascii/hAnsi==Times New Roman
- 斜體 run 數量 >0 且內容是統計量(這是唯一允許的特殊格式)
- 三線表:用
verify_table_borders(table)逐表校驗,應全部返回(True, []) - 表題:五號黑體置中、句末無標點
- 表格內所有 run: 宋體 + 10.5pt
- 正文段落 alignment == JUSTIFY,標題與表題保持原樣
- 百分號前有空格:
33 %而非33% - 符號與漢字間無空格:
30餘次;符號與數字間有空格:≥ 10 小時、P < 0.05
一次性自檢腳本:
import sys; sys.path.insert(0, '<skill>/scripts')
from tri_line_table import verify_table_borders
from docx import Document
d = Document('out.docx'); ps = d.paragraphs
assert sum(1 for x in ps if not x.text.strip()) == 0, '存在空段落'
assert not [r for x in ps for r in x.runs if r.font.bold], '存在加粗'
assert not [r for t in d.tables for row in t.rows for c in row.cells
for r in c.paragraphs[0].runs if r.font.bold], '表格存在加粗'
for i, t in enumerate(d.tables, 1):
good, prob = verify_table_borders(t)
assert good, f'表{i} 三線表不合規: {prob}'
print('全部通過')
無可視化驗證環境時
沒有 LibreOffice / Word 可轉 PDF 目視驗證三線表時, 改用 XML 層校驗(清單第 1–9 項),能可靠覆蓋字號、字體、邊框、間距。
補充:三線表的精確規範
只有 3 條線:
| 位置 | 磅值 | w:sz |
|---|---|---|
| 表頭行(上框線) | 1.5 | 12 |
| 表頭行(下框線) | 1.0 | 8 |
| 末行(下框線) | 1.5 | 12 |
| 其餘所有框線(含所有中間行、豎線) | 空 | val="none" |
關鍵:不能只在 tblBorders 設 insideH。表級邊框會被單元格級 tcBorders 覆蓋,
一旦某儲存格寫了 bottom,就以儲存格級為準。必須逐儲存格顯式設定:
- 表級
tblBorders六項全設none - 所有行
tcBorders四邊先全設none - 表頭行儲存格:
top=('single',12)+bottom=('single',8) - 末行儲存格:
bottom=('single',12)
clear_row_borders() 要對每一行呼叫,不能只清表頭和末行——否則中間行殘留線條。
補充:中文引號替換
Markdown 裡的 " 是直引號,轉 docx 後必須成對替換為 「 / 」:
dq_index = 0 # 必須跨 run 連續計數,不能每個 run 從 0 開始
for r in p.runs:
for ch in r.text:
if ch == '"':
out.append('「' if dq_index % 2 == 0 else '」')
dq_index += 1
只改 r.text,不動 rPr → 字體/字號/斜體全部保留。
替換後必須驗證:全文 " 計數 == 0、「 計數 == 」 計數、逐段左右配對
(整篇平衡不代表逐段平衡)。
補充:Word 編輯會產生碎片 run
用戶在 Word 裡手工編輯後,段落會被拆成多個 run,其中部分(被拆出的 -、單字)
不帶 rFonts,會回落到默認字體。修完引號/邊框後應掃一遍:
rf = r._element.rPr.rFonts if r._element.rPr is not None else None
if rf is None or rf.get(qn('w:eastAsia')) is None: # → 需補齊
補齊方式:取段內第一個已設定字體的 run 作參照,繼承其 rFonts/sz/b。
重要工作流程提醒
用戶在 Word 中手工修改過文件後,絕不能重新生成整份文件——會覆蓋用戶改動。
正確做法:讀取現有 docx → 原地定點修 XML → 儲存。
修前先備份:cp xxx.docx /tmp/backup.docx。
斜體規範
唯一判定標準:該字元在數學/統計學中是否是變數或符號
| 類別 | 是否斜體 | 示例 |
|---|---|---|
| 統計學顯著性 P | 斜體 | P = 0.04、P < 0.001 |
| 檢驗水準 α | 斜體 | α = 0.05 |
| 檢驗統計量 F、Z、H、χ²、t、r | 斜體 | F = 4.52、χ² = 8.31 |
| 特殊字母(基因符號、甲基化 β 值等) | 斜體 | β |
| OR、CI、RR、HR、SD、SE、AUC | 不斜體 | OR = 3.14、95% CI |
| 多字母縮寫(BMI、OGTT、FPG…) | 不斜體 | BMI、OGTT |
| 中文文字 | 不斜體 | 值、為 |
⚠️ 最易錯:「P值」只有 P 斜體
✗ 斜體 'P值' ← "值" 跟著斜了
✓ 斜體 'P' + 正體 '值'
✓ 斜體 'P' + 正體 '值為' + ...
P 值、P值、P 值為 0.07 ——一律只有 P 斜體,緊跟的漢字全部正體。
斜體拆分的三條鐵律(踩過血)
- 絕不重建 run。用"字元流重組 run"修格式會導致 rFonts/sz/b 全丟、
三線表邊框被清空、斜體標記錯位成碎片。只能改
run.text,絕不刪/建 run 元素。 - 拆分必須用
copy.deepcopy(run._element)保留全部格式屬性; 插入位置parent.insert(index+1, el)(緊鄰原 run)。用append會跑到段末、 順序錯亂出現PP。 副本文本 =lead + rest + trail(不含 stat),原 run 文本 =lead + stat。 run.text = x不會清空已有<w:t>節點,在 deepcopy 副本上會追加。 必須先el.remove(child)刪掉所有w:t/w:br/w:tab/w:cr; 且要先移除w:i/w:iCs再設文本,順序反了也會錯。
參考實現見 scripts/fix_italic.py。
中英文混排與符號間距
基本規則
| 場景 | 規則 | 示例 |
|---|---|---|
| 中文 ↔ 數字/字母/標點 | 不加空格 | 在30餘次未校正檢驗的背景下、但Table 1顯示 |
| 漢字 ↔ 漢字 | 不加空格 | 研究目的 |
| 英文單詞之間 | 加空格 | Table 1 |
| 數學運算式內部 | 字母/數字 與 運算子 之間加空格 | P = 0.04、BMI < 18.5 |
| 數學運算式 ↔ 中文 | 加空格 | P < 0.05 為閾值 |
符號與數字/字母/漢字的間距(易錯)
| 組合 | 規則 | 示例 |
|---|---|---|
| 符號 ↔ 數字 | 加空格 | 33 %、87.3 %、P = 0.04 |
| 符號 ↔ 英文單詞/字母 | 加空格 | Table 1;α = 0.05 |
| 符號 ↔ 漢字 | 不加空格 | 30餘次、≥10小時(符號緊貼漢字) |
易錯點:30 餘次(錯,符號與漢字間多空格) vs 30餘次(對)。
運算子類符號(≥ ≤ = < >)與數字之間要空格,但符號與緊隨的漢字之間不補空格。
百分號
33 % 數字與 % 之間加空格。這是科技論文規範的要求,不要寫成 33%。
參考實現見 scripts/fix_spacing2.py(含 fix_spacing_in_run)。
正文段落對齊
正文段落用兩端對齊——即 Word 的 JUSTIFY(Cmd+J / Ctrl+J),
注意不是 DISTRIBUTE。
from docx.enum.text import WD_ALIGN_PARAGRAPH
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
只應用到正文段落,以下保持各自原有對齊,不要改動:
- 各級標題——通常左對齊
- 表題——置中
- 表格內儲存格——按需(左對齊或置中)
⚠️ 三條鐵律(執行順序與校驗)
執行順序
python3 scripts/fix_italic.py 目標.docx # 先拆斜體
python3 scripts/fix_spacing2.py 目標.docx # 後修間距
校驗(必做,缺一不可)
import re
norm = lambda s: re.sub(r'\s', '', s)
# ① 文字零遺失——防吞字/重複字
assert all(norm(a.text) == norm(b.text)
for a, b in zip(原文件.paragraphs, 新文件.paragraphs))
# ② 斜體 run 只能是統計符號本身(P/α/β/F/Z/H/χ/t/r),不得含:
# - 運算子或數字:[=<>≤≥0-9]
# - 漢字('P值' 的 '值' 必須是正體)
# - OR/CI/RR 等縮寫
for p in 新文件.paragraphs:
for r in p.runs:
if r.font.italic and r.text.strip():
t = r.text.strip()
assert not re.search(r'[=<>≤≥0-9\u4e00-\u9fff]', t), f'斜體含運算子/數字/漢字: {t!r}'
assert t.split(' ')[0] not in ('OR','CI','RR','HR','SD','SE','AUC'), f'縮寫不應斜體: {t!r}'
# ③ 正文段落必須兩端對齊
from docx.enum.text import WD_ALIGN_PARAGRAPH
assert any(p.alignment == WD_ALIGN_PARAGRAPH.JUSTIFY for p in 新文件.paragraphs)
# ④ 中文引號成對
assert sum(p.text.count('「') for p in 新文件.paragraphs) == \
sum(p.text.count('」') for p in 新文件.paragraphs)
⚠️ 最高優先級鐵律(違反即返工)
- 「空一行」只能用段落間距(
space_before/space_after),絕不能用回車/空段落。 - 任何時候都不要給文字加粗,除非用戶在需求裡明確說了要加粗。
Markdown 的
**文字**是源文件的強調記號,不是排版指令——只去標記、不加粗。 - 用戶沒要求的格式(底線、著重號、彩色字、字間距、字元縮放)一律不動。
- 用戶手工改過文件後,禁止重新生成整份文件,只能讀現有檔案原地改 XML。
層級感只能來自字體(黑體/楷體)、字號、段前段後間距、編號——不是加粗。
精簡正文/表格文字時的注意事項
在用戶已修改的 docx 上精簡文字內容時:
鐵律:合併寫入第一個 run,其餘置空
def set_paragraph_text(p, text):
runs = p.runs
if not runs: return False
runs[0].text = text # 格式保留在第一個 run
for r in runs[1:]:
r.text = '' # ⚠️ 必須置空,否則殘留舊文字造成重複
return True
同樣遵守「不重建 run」原則——只改 run.text,絕不刪/建 <w:r>。
定位表格:依文件流順序,不能用 doc.paragraphs 索引
段落裡嵌著表格時,doc.paragraphs[i] 的序號與表格不對應。必須走 body 流:
from docx.table import Table
from docx.text.paragraph import Paragraph
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
seq = []
for c in doc.element.body.iterchildren():
if c.tag == W+'p': seq.append(('p', Paragraph(c, doc)))
elif c.tag == W+'tbl': seq.append(('t', Table(c, doc)))
增刪行的兩個坑
table.rows每次存取都重新解析 XML —— 刪行後再用rows[ri]會越界。 先rows = list(table.rows)快照,從後往前刪。- 克隆出來的行會丟末行下框線 —— 克隆末行補齊後,三線表的底線會變成
nil。 收尾必須重新補末行bottom邊框(sz=12)。
⚠️ 偵測邊框時不要只認 none
Word 自己的三線表,中間行寫的是 w:val="nil" 而非 "none"。
判定"無線"要同時接受兩者:
assert tv in ('nil','none') and bv in ('nil','none')
只認 none 會誤報"邊框損壞",實際是偵測邏輯錯。