Install
openclaw skills install @trisia/mddocopenclaw skills install @trisia/mddoc# 安装依赖(仅首次)
pip install python-docx Pillow requests mistune
# 转换 Markdown 文件 → 输出到同目录
python <skill-path>/scripts/md2docx.py paper.md
# 指定输出路径
python <skill-path>/scripts/md2docx.py paper.md -o /path/to/output.docx
# 直接转换粘贴的文本
python <skill-path>/scripts/md2docx.py --text "# 标题\n\n正文内容" -o out.docx
其中 <skill-path> = /home/kkk/.claude/skills/mddoc
@ 引用)则读取该文件pip install python-docx Pillow requests mistunescripts/md2docx.py;若 Markdown 结构特殊则参照下方格式规范编写自定义脚本# 标题 提取每个元素配可直接使用的 python-docx 代码。公共导入和工具函数:
from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
def set_cn_font(run, cn_name, en_name='Times New Roman', size_pt=10.5, bold=False):
"""设置 run 的中英文字体、字号、加粗、颜色(黑)"""
run.font.size = Pt(size_pt)
run.font.bold = bold
run.font.name = en_name
run.font.color.rgb = None
rPr = run._element.get_or_add_rPr()
rFonts = rPr.find(qn('w:rFonts'))
if rFonts is None:
rFonts = OxmlElement('w:rFonts'); rPr.insert(0, rFonts)
rFonts.set(qn('w:eastAsia'), cn_name)
rFonts.set(qn('w:ascii'), en_name)
rFonts.set(qn('w:hAnsi'), en_name)
def add_empty(doc):
"""五号空行"""
p = doc.add_paragraph()
run = p.add_run('')
set_cn_font(run, '宋体', size_pt=10.5)
return p
def set_outline(para, level):
"""设置 outline level(XML方式,兼容所有python-docx版本)"""
pPr = para._element.get_or_add_pPr()
ol = OxmlElement('w:outlineLvl')
ol.set(qn('w:val'), str(level))
pPr.append(ol)
五号=10.5pt,1.3倍行距,段前段后0磅,全黑,A4页边距左3cm右2cm上2cm下2cm,页脚距底1cm。
doc = Document()
section = doc.sections[0]
# 页边距:左3cm 右2cm 上2cm 下2cm
section.left_margin = Cm(3)
section.right_margin = Cm(2)
section.top_margin = Cm(2)
section.bottom_margin = Cm(2)
sty = doc.styles['Normal']
sty.font.size = Pt(10.5)
sty.font.name = 'Times New Roman'
sty.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
sty.paragraph_format.line_spacing = 1.3
sty.paragraph_format.space_before = Pt(0)
sty.paragraph_format.space_after = Pt(0)
#)三号黑体(16pt)、居中、上下各空一行、不设 outline level。
add_empty(doc)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 题目不设 outline_level
run = p.add_run(title_text)
set_cn_font(run, '黑体', size_pt=16)
add_empty(doc)
#)三号黑体(16pt)、居中、上下各空一行、outline_level=1、前加分页符。
add_empty(doc)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
set_outline(p, 1)
# 分页符
pPr = p._element.get_or_add_pPr()
pPr.append(OxmlElement('w:pageBreakBefore'))
run = p.add_run(text)
set_cn_font(run, '黑体', size_pt=16)
add_empty(doc)
##)四号黑体(14pt)、不加粗、顶格(无缩进)、outline_level=2。上下不空行。
p = doc.add_paragraph()
set_outline(p, 2)
p.paragraph_format.first_line_indent = Pt(0)
run = p.add_run(text)
set_cn_font(run, '黑体', size_pt=14, bold=False)
###)小四宋体(12pt)、不加粗、首行缩进Pt(21)(=2个中文字)、outline_level=3。
p = doc.add_paragraph()
set_outline(p, 3)
p.paragraph_format.first_line_indent = Pt(21)
run = p.add_run(text)
set_cn_font(run, '宋体', size_pt=12, bold=False)
五号(10.5pt)宋体/TNR、首行缩进Pt(21)、1.3倍行距、段前段后0磅。
p = doc.add_paragraph()
p.paragraph_format.first_line_indent = Pt(21)
p.paragraph_format.line_spacing = 1.3
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
run = p.add_run(text)
set_cn_font(run, '宋体', size_pt=10.5)
下载嵌入(不压缩)、等比缩放(8-12cm规则)、居中、上方空一行。
import requests
from PIL import Image
def dl_image(url):
h = {'User-Agent': 'Mozilla/5.0 (compatible; mddoc/1.0)'}
r = requests.get(url, headers=h, timeout=30); r.raise_for_status()
return r.content # bytes, 可保存为临时文件
def img_width(path):
img = Image.open(path)
dpi = img.info.get('dpi', (96, 96))
dx = dpi[0] if dpi and dpi[0] > 0 else 96
w_cm = img.width / dx * 2.54
if w_cm > 12: tw = 12.0
elif w_cm < 8: tw = 8.0
else: tw = w_cm
return Cm(tw)
add_empty(doc)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.add_run().add_picture(img_path, width=img_width(img_path))
图片下方(不空行)、小五(9pt)宋体加粗居中、格式 图<章>-<序号> <alt>、逐章编号。
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(f'图{ch}-{idx} {alt_text}')
set_cn_font(run, '宋体', size_pt=9, bold=True)
add_empty(doc) # 图题下方空一行
三线表(顶线1.5pt/表头底线0.75pt/底线1.5pt粗,无竖线)。表题在上方(不空行)、五号(10.5pt)宋体加粗居中、格式表<章>-<序号> <描述>。
# 表题
add_empty(doc)
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(f'表{ch}-{idx} {caption}')
set_cn_font(run, '宋体', size_pt=10.5, bold=True)
# 表格
tb = doc.add_table(rows=nrows, cols=ncols)
tb.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 三线表边框
Pr = tb._tbl.tblPr or OxmlElement('w:tblPr')
B = OxmlElement('w:tblBorders')
for tag, sz in [('top','12'),('bottom','12')]:
e = OxmlElement(f'w:{tag}'); e.set(qn('w:val'),'single')
e.set(qn('w:sz'),sz); e.set(qn('w:space'),'0'); e.set(qn('w:color'),'000000')
B.append(e)
for tag in ('left','right','insideH','insideV'):
e = OxmlElement(f'w:{tag}'); e.set(qn('w:val'),'none')
e.set(qn('w:sz'),'0'); e.set(qn('w:space'),'0'); e.set(qn('w:color'),'auto')
B.append(e)
Pr.append(B)
# 表头行每格底部加细线 0.75pt(cell级别,不影响数据行)
for cell in tb.rows[0].cells:
tcPr = cell._tc.get_or_add_tcPr()
tcB = OxmlElement('w:tcBorders')
btm = OxmlElement('w:bottom')
btm.set(qn('w:val'),'single'); btm.set(qn('w:sz'),'6')
btm.set(qn('w:space'),'0'); btm.set(qn('w:color'),'000000')
tcB.append(btm); tcPr.append(tcB)
# 表头行 — 居中、9pt、无缩进
for j, txt in enumerate(header):
c = tb.rows[0].cells[j]; c.paragraphs[0].clear()
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
run = c.paragraphs[0].add_run(txt)
set_cn_font(run, '宋体', size_pt=9)
c.paragraphs[0].paragraph_format.first_line_indent = Pt(0)
# 数据行 — 左对齐、9pt、无缩进
for i, row in enumerate(data):
for j, txt in enumerate(row):
c = tb.rows[i+1].cells[j]; c.paragraphs[0].clear()
c.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.LEFT
run = c.paragraphs[0].add_run(txt)
set_cn_font(run, '宋体', size_pt=9)
c.paragraphs[0].paragraph_format.first_line_indent = Pt(0)
add_empty(doc) # 表格下方空一行
左顶格"xxxxx"、右顶格文档题目、小五(9pt)黑体。
sec = doc.sections[0]; hdr = sec.header
hdr.is_linked_to_previous = False
r = hdr.paragraphs[0].add_run('xxxxx')
set_cn_font(r, '黑体', size_pt=9)
p2 = hdr.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.RIGHT
r2 = p2.add_run(title_text)
set_cn_font(r2, '黑体', size_pt=9)
有序列表用(1)、(2)、(3)序号;无序列表用 • 符号。按正文格式(首行缩进Pt(21))。
# 有序列表 — (1)、(2)、(3) 格式
for idx, item in enumerate(items, 1):
p = doc.add_paragraph()
p.paragraph_format.first_line_indent = Pt(21)
run = p.add_run(f'({idx}){item}')
set_cn_font(run, '宋体', size_pt=10.5)
# 无序列表
for item in items:
p = doc.add_paragraph()
p.paragraph_format.first_line_indent = Pt(21)
run = p.add_run(f'• {item}')
set_cn_font(run, '宋体', size_pt=10.5)
Courier New 等宽字体、五号、左缩进。
p = doc.add_paragraph()
p.paragraph_format.left_indent = Cm(1)
run = p.add_run(code)
set_cn_font(run, '宋体', en_name='Courier New', size_pt=10.5)
$...$)嵌于段落中,转换为 OMML(Office Math Markup Language)格式,WPS/Word 可直接渲染。
公式在段落中作为 <m:oMath> 元素插入,与文字 run 同级。
# 行内公式通过 split_inline_math() 拆分为 segments
# text segment → 普通 run,math segment → latex_to_omml() → m:oMath 元素
p = doc.add_paragraph()
p.paragraph_format.first_line_indent = Pt(21)
p.paragraph_format.line_spacing = 1.3
for seg in segments:
if seg['type'] == 'text':
if seg['content']:
run = p.add_run(seg['content'])
set_cn_font(run, '宋体', size_pt=10.5)
else: # math — 转换为 OMML 并插入段落 XML
omml = latex_to_omml(seg['content'], display=False)
p._element.append(omml)
$$...$$)独立成行,上下各空一行。公式通过 OMML 居中渲染,编号右对齐。编号格式同图/表:(章-序号),逐章编号。编号括号用五号宋体,数字用五号 Times New Roman。
布局方式:段落设置居中和右对齐 tab stop,OMML <m:oMath> 元素位于两个 tab 之间,实现公式居中、编号右对齐。
add_empty(doc) # 上方空一行
p = doc.add_paragraph()
# 设置 tab stops:居中和右对齐
# 左边距3cm右边距2cm → 可用宽度16cm → 中心8cm(4536twips), 右边16cm(9072twips)
pPr = p._element.get_or_add_pPr()
tabs = OxmlElement('w:tabs')
ct = OxmlElement('w:tab'); ct.set(qn('w:val'), 'center'); ct.set(qn('w:pos'), '4536')
rt = OxmlElement('w:tab'); rt.set(qn('w:val'), 'right'); rt.set(qn('w:pos'), '9072')
tabs.append(ct); tabs.append(rt)
pPr.append(tabs)
# tab → 公式 OMML 居中
run_t1 = p.add_run()
run_t1._r.append(OxmlElement('w:tab'))
omml = latex_to_omml(formula_text, display=False)
p._element.append(omml)
# tab → 编号右对齐:(章-序号)
run_t2 = p.add_run()
run_t2._r.append(OxmlElement('w:tab'))
run_lp = p.add_run('(')
set_cn_font(run_lp, '宋体', en_name='宋体', size_pt=10.5)
run_num = p.add_run(f'{ch}-{eq_num}')
set_cn_font(run_num, '宋体', en_name='Times New Roman', size_pt=10.5)
run_rp = p.add_run(')')
set_cn_font(run_rp, '宋体', en_name='宋体', size_pt=10.5)
add_empty(doc) # 下方空一行
内置 latex_to_omml() 函数将 LaTeX 数学公式转换为 OMML XML,支持以下构造:
a^b、a_b、a_b^c\frac{a}{b}\sqrt{x}、\sqrt[n]{x}\alpha、\beta、\Gamma 等\sum、\prod、\int(含上下限)\sin、\cos、\log、\lim 等\hat、\bar、\vec、\tilde 等\infty、\cdot、\times、\partial、\nabla 等\left(...\right)、\left[...\right]\text{...}从第一章(第一个一级标题所在页)开始编页码,格式 第×页 共×页,页脚边距 1cm。
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm
section = doc.sections[0]
section.footer_distance = Cm(1.1) # 页脚边距
footer = section.footer
footer.is_linked_to_previous = False
fp = footer.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
# "第" 字
run1 = fp.add_run('第')
set_cn_font(run1, '宋体', size_pt=10.5)
# 页码字段
run_page = fp.add_run()
fldChar1 = OxmlElement('w:fldChar'); fldChar1.set(qn('w:fldCharType'), 'begin')
run_page._r.append(fldChar1)
instrText = OxmlElement('w:instrText'); instrText.set(qn('xml:space'), 'preserve')
instrText.text = ' PAGE '
run_page._r.append(instrText)
fldChar2 = OxmlElement('w:fldChar'); fldChar2.set(qn('w:fldCharType'), 'end')
run_page._r.append(fldChar2)
# "页 共"
run2 = fp.add_run('页 共')
set_cn_font(run2, '宋体', size_pt=10.5)
# 总页数字段
run_total = fp.add_run()
fldChar3 = OxmlElement('w:fldChar'); fldChar3.set(qn('w:fldCharType'), 'begin')
run_total._r.append(fldChar3)
instrText2 = OxmlElement('w:instrText'); instrText2.set(qn('xml:space'), 'preserve')
instrText2.text = ' NUMPAGES '
run_total._r.append(instrText2)
fldChar4 = OxmlElement('w:fldChar'); fldChar4.set(qn('w:fldCharType'), 'end')
run_total._r.append(fldChar4)
# "页"
run3 = fp.add_run('页')
set_cn_font(run3, '宋体', size_pt=10.5)
注意:页码从第一章开始计数需在第一个一级标题处插入分节符并重置页码。简单场景可统一从第1页开始。
表头行设为重复标题行,跨页自动重复。续表在右上方标"续表<编号>"。
# 表头行设为重复标题行(跨页自动出现)
tblHeader = OxmlElement('w:tblHeader')
trPr = table.rows[0]._tr.get_or_add_trPr()
trPr.append(tblHeader)
# 对于确实跨页的大表格,在分页处手动加"续表"标记:
# (python-docx无法检测分页位置,建议生成后人工检查)
# 续表右对齐段落示例:
p_cont = doc.add_paragraph()
p_cont.alignment = WD_ALIGN_PARAGRAPH.RIGHT
run_cont = p_cont.add_run(f'续表{tab_label}')
set_cn_font(run_cont, '宋体', size_pt=10.5)
# 是题目(不设 outline),后续 # 是一级标题(outline_level=1)Cm(),不手算 EMU — add_picture(width=Cm(x)) 自动转换set_outline() 设置 — paragraph_format.outline_level 在部分python-docx版本不生效,统一用XML方式写入;读取时也从XML读取章-序号