夜雨聆风学习资料网

ARTICLE · 1096500

用 Python 一键提取 Word 文档全部内容,文字、表格、图片全都有

用 Python 一键提取 Word 文档全部内容,文字、表格、图片全都有

平时写公众号、整理资料、做知识库,最烦的就是从 Word 里往外抠内容。

复制文字吧,表格全乱;想存图片吧,得一张张右键另存;要是几十个文档,手都点废。

后来我用 Python 写了个小工具,不管 Word 里有多少段落、表格、图片,跑一遍全部提取出来,还能按原顺序整理好。今天把方法分享给你,代码可以直接拿去用。


一、为什么选 python-docx

Python 处理 Word 的库有好几个,但最省心的是 python-docx。

它不用你装 Office,也不依赖 COM 组件,纯 Python 解析 .docx 文件。安装就一行:

pip install python-docx

注意:它只支持 .docx,老版 .doc 不行。如果你手头是 .doc,先用 Word 另存为 .docx。


二、提取文字:别只会 doc.paragraphs

很多人上手就写:

from docx import Documentdoc = Document("demo.docx")for p in doc.paragraphs:    print(p.text)

这样确实能拿到文字,但有两个坑:

  1. 表格里的文字拿不到,因为表格不在 doc.paragraphs 里。

  2. 顺序会乱。如果文档是“段落 → 表格 → 段落”,doc.paragraphs 会把所有段落先列出来,表格全跑到后面去。

正确做法是直接遍历文档底层的 XML 节点,按真实顺序走:

from docx.oxml.table import CT_Tblfrom docx.oxml.text.paragraph import CT_Pfrom docx.table import Table, _Cellfrom docx.text.paragraph import Paragraphfrom docx.document import Document as _Documentdef iter_blocks(parent):    """按文档真实顺序,依次产出 Paragraph 或 Table"""    if isinstance(parent, _Document):        elm = parent.element.body    elif isinstance(parent, _Cell):        elm = parent._tc    else:        raise TypeError("不支持的容器")    for child in elm.iterchildren():        if isinstance(child, CT_P):            yield Paragraph(child, parent)        elif isinstance(child, CT_Tbl):            yield Table(child, parent)

这样你就能按顺序拿到每一个段落和表格。


三、识别标题和样式

提取文字时,如果能顺便把标题级别也识别出来,后面整理成 Markdown 或公众号文章就轻松多了。

Word 的标题样式名一般是 Heading 1、Heading 2,中文版是 标题 1、标题 2。写个正则兼容一下:

import reHEADING_RE = re.compile(r'^(?:Heading|标题)\s*([1-6])$', re.I)def get_heading_level(paragraph):    style_name = (paragraph.style.name or '').strip()    m = HEADING_RE.match(style_name)    if m:        return int(m.group(1))    if style_name in ('Title', '标题'):        return 1    return None

拿到级别后,你就可以决定输出成 #、##,或者公众号的 <h1>、<h2>。


四、提取表格:转成二维列表

表格提取的核心是 table.rows 和 row.cells。但要注意,合并单元格会导致同一个单元格被重复读取,这里先不做复杂去重,一般够用。

def extract_table(table):    data = []    for row in table.rows:        cells = []        for cell in row.cells:            # 单元格内可能有多个段落,用换行拼起来            text = '\n'.join(p.text.strip() for p in cell.paragraphs if p.text.strip())            cells.append(text)        data.append(cells)    return data

拿到二维列表后,你想存 CSV、转 Markdown、还是直接写进公众号表格,都随你。


五、提取图片:Word 其实是个压缩包

这是最容易被忽略的部分。

.docx 文件本质是个 zip 包,图片全藏在 word/media/ 目录里。段落里通过一个 rId 去引用图片。所以思路是:找到段落里的图片引用,拿到 rId,再去文档部件里取二进制数据。

import osfrom docx.oxml.ns import qndef save_images_from_run(run, doc, out_dir, counter):    """抽出一个 run 里所有图片,保存到本地,返回文件名列表"""    names = []    for blip in run._element.findall('.//' + qn('a:blip')):        rid = blip.get(qn('r:embed'))        if not rid:            continue        part = doc.part.related_parts.get(rid)        if part is None:            continue        ext = os.path.splitext(str(part.partname))[1] or '.png'        counter[0] += 1        name = f"img_{counter[0]:03d}{ext}"        with open(os.path.join(out_dir, name), 'wb') as f:            f.write(part.blob)        names.append(name)    return names

然后在遍历段落时,对每个 run 调用这个函数,就能把图片按顺序存下来。


六、完整代码:一键提取所有内容

把上面的拼起来,写一个 extract_word.py:

# -*- coding: utf-8 -*-"""extract_word.py提取 Word 文档中的文字、表格、图片,按原顺序输出为结构化数据用法:    python extract_word.py demo.docx"""import osimport reimport shutilfrom docx import Documentfrom docx.oxml.ns import qnfrom docx.oxml.table import CT_Tblfrom docx.oxml.text.paragraph import CT_Pfrom docx.table import Table, _Cellfrom docx.text.paragraph import Paragraphfrom docx.document import Document as _DocumentHEADING_RE = re.compile(r'^(?:Heading|标题)\s*([1-6])$', re.I)def iter_blocks(parent):    """按文档真实顺序依次产出 Paragraph / Table"""    if isinstance(parent, _Document):        elm = parent.element.body    elif isinstance(parent, _Cell):        elm = parent._tc    else:        raise TypeError("不支持的容器")    for child in elm.iterchildren():        if isinstance(child, CT_P):            yield Paragraph(child, parent)        elif isinstance(child, CT_Tbl):            yield Table(child, parent)def get_heading_level(paragraph):    style_name = (paragraph.style.name or '').strip()    m = HEADING_RE.match(style_name)    if m:        return int(m.group(1))    if style_name in ('Title', '标题'):        return 1    return Nonedef extract_table(table):    data = []    for row in table.rows:        cells = []        for cell in row.cells:            text = '\n'.join(p.text.strip() for p in cell.paragraphs if p.text.strip())            cells.append(text)        data.append(cells)    return datadef save_images_from_run(run, doc, out_dir, counter):    names = []    for blip in run._element.findall('.//' + qn('a:blip')):        rid = blip.get(qn('r:embed'))        if not rid:            continue        part = doc.part.related_parts.get(rid)        if part is None:            continue        ext = os.path.splitext(str(part.partname))[1] or '.png'        counter[0] += 1        name = f"img_{counter[0]:03d}{ext}"        with open(os.path.join(out_dir, name), 'wb') as f:            f.write(part.blob)        names.append(name)    return namesdef extract_docx(docx_path, img_dir='images'):    doc = Document(docx_path)    if os.path.isdir(img_dir):        shutil.rmtree(img_dir)    os.makedirs(img_dir, exist_ok=True)    counter = [0]    blocks = []    for block in iter_blocks(doc):        if isinstance(block, Paragraph):            text = block.text.strip()            has_img = bool(block._element.findall('.//' + qn('a:blip')))            imgs = []            for run in block.runs:                imgs.extend(save_images_from_run(run, doc, img_dir, counter))            if not text and not has_img:                continue            blocks.append({                'type': 'paragraph',                'text': text,                'heading_level': get_heading_level(block),                'images': imgs,            })        elif isinstance(block, Table):            blocks.append({                'type': 'table',                'data': extract_table(block),            })    return blocksif __name__ == '__main__':    import sys    src = sys.argv[1] if len(sys.argv) > 1 else 'demo.docx'    result = extract_docx(src)    for b in result:        if b['type'] == 'paragraph':            prefix = '#' * b['heading_level'] if b['heading_level'] else ''            print(f"{prefix}{b['text']}")            for img in b['images']:                print(f"  [图片] {img}")        elif b['type'] == 'table':            print("[表格]")            for row in b['data']:                print("  | " + " | ".join(row) + " |")
运行:
python extract_word.py 我的文档.docx

你会看到文字按顺序打印出来,表格变成 | 分隔的行,图片存进 images/ 文件夹。


七、提取完怎么用?几个常见场景

1. 转成 Markdown 发公众号

拿到 blocks 后,写个简单的转换函数:

def to_markdown(blocks, img_dir='images'):    lines = []    for b in blocks:        if b['type'] == 'paragraph':            if b['heading_level']:                lines.append('#' * b['heading_level'] + ' ' + b['text'])            elif b['text']:                lines.append(b['text'])            for img in b['images']:                lines.append(f'![图片]({img_dir}/{img})')        elif b['type'] == 'table':            for i, row in enumerate(b['data']):                lines.append('| ' + ' | '.join(row) + ' |')                if i == 0:                    lines.append('| ' + ' | '.join(['---'] * len(row)) + ' |')    return '\n\n'.join(lines)

Markdown 可以直接粘进支持 Markdown 的编辑器,也可以再用工具转成公众号 HTML。

2. 批量提取多个文档

把 extract_docx 套个循环,遍历文件夹里所有 .docx,结果写进 JSON 或数据库,就能做知识库了。

3. 只提取图片做素材库

如果你只想要图片,把文字部分跳过,只调 save_images_from_run 就行。


八、几个容易踩的坑

1. 老版 .doc 不支持

python-docx 只认 .docx。遇到 .doc 先用 Word 批量另存为 .docx,或者用 win32com 调 Office 转换。

2. 合并单元格会重复读

row.cells 遇到合并单元格时,同一个单元格会被多个位置返回。如果表格合并很复杂,需要额外判断 cell._tc 是否相同来去重。

3. 图片可能重复

如果同一张图在文档里被引用多次,按 rId 存会得到多个副本。想要去重的话,加一个 rid -> filename 的字典缓存即可。

4. 页眉页脚不在 doc.paragraphs 里

页眉页脚属于 section.header 和 section.footer,需要单独遍历。一般公众号文章用不到,知道有这回事就行。


写在最后

用 Python 提取 Word 内容,核心就三件事:按顺序遍历、识别样式、抠出图片。

代码不长,但能省下大量复制粘贴的时间。你可以直接拿上面的脚本去改,比如加上页眉页脚提取、支持批量处理、或者接上公众号草稿箱接口一键发文。

工具的意义就是让重复劳动自动化,把时间留给真正重要的内容创作。

相关学习资料