Word 文档图片批量提取工具

需求背景

在文档处理过程中,经常需要从 Word 文档中提取图片。手动保存不仅效率低,还容易出错。本文介绍一个基于 Python 的工具,可以自动从 .docx 文件中提取图片,并根据文档中的图片标题自动命名。

实现思路

核心原理

.docx 文件本质上是一个 ZIP 压缩包,包含以下关键文件:

  • word/document.xml - 文档内容和结构
  • word/_rels/document.xml.rels - 资源关联关系
  • word/media/ - 图片文件存储目录

实现步骤

  1. 解析文档结构:解压 .docx 文件,读取 XML 文件
  2. 提取图片信息:从 document.xml 中查找图片元素(<a:blip>),获取 relationship ID
  3. 匹配图片标题:在图片所在段落及后续段落中,使用正则表达式匹配"图X-X:标题"格式
  4. 关联文件路径:通过 document.xml.rels 将 relationship ID 映射到实际的图片文件路径
  5. 保存图片:从 word/media/ 目录提取图片,使用标题命名并保存

核心代码

Python 脚本

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
智能提取Word文档图片
"""

import sys
import os
import re
from pathlib import Path
from zipfile import ZipFile
import xml.etree.ElementTree as ET

def sanitize_filename(filename):
    """清理文件名"""
    illegal_chars = r'[<>:"/\\|?*]'
    filename = re.sub(illegal_chars, '_', filename)
    filename = filename.strip()
    if len(filename) > 200:
        filename = filename[:200]
    return filename

def extract_image_title_mapping(document_xml):
    """从 document.xml 中提取图片和标题的对应关系"""
    namespaces = {
        'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
        'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
        'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
    }
    
    root = ET.fromstring(document_xml.encode('utf-8'))
    image_title_map = {}
    paragraphs = root.findall('.//w:p', namespaces)
    
    for i, para in enumerate(paragraphs):
        blips = para.findall('.//a:blip', namespaces)
        
        if blips:
            for blip in blips:
                r_embed = blip.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                
                if r_embed:
                    title = None
                    current_text = ''.join(para.itertext())
                    
                    # 匹配图片标题格式:图1-1:标题 或 图1-1 标题
                    title_match = re.search(r'图\s*(\d+)[--]\s*(\d+)\s*[::]\s*([^。.\n\r]{2,50})', current_text)
                    if not title_match:
                        title_match = re.search(r'图\s*(\d+)[--]\s*(\d+)\s+([^。.\n\r]{2,50})', current_text)
                    
                    if title_match:
                        fig_num = f"{title_match.group(1)}-{title_match.group(2)}"
                        title_text = title_match.group(3).strip()
                        title_text = re.sub(r'\s+', ' ', title_text)
                        title_text = title_text.strip('。.、,,::')
                        title = f"图{fig_num}_{title_text}"
                    
                    # 如果当前段落没找到,检查后续2个段落
                    if not title and i + 1 < len(paragraphs):
                        for next_para in paragraphs[i+1:i+3]:
                            next_text = ''.join(next_para.itertext())
                            title_match = re.search(r'图\s*(\d+)[--]\s*(\d+)\s*[::]\s*([^。.\n\r]{2,50})', next_text)
                            if not title_match:
                                title_match = re.search(r'图\s*(\d+)[--]\s*(\d+)\s+([^。.\n\r]{2,50})', next_text)
                            
                            if title_match:
                                fig_num = f"{title_match.group(1)}-{title_match.group(2)}"
                                title_text = title_match.group(3).strip()
                                title_text = re.sub(r'\s+', ' ', title_text)
                                title_text = title_text.strip('。.、,,::')
                                title = f"图{fig_num}_{title_text}"
                                break
                    
                    if title:
                        image_title_map[r_embed] = sanitize_filename(title)
    
    return image_title_map

def get_image_file_from_rid(rels_xml, rid):
    """从 _rels/document.xml.rels 中根据 rId 获取实际的图片文件路径"""
    namespaces = {
        'r': 'http://schemas.openxmlformats.org/package/2006/relationships'
    }
    
    root = ET.fromstring(rels_xml.encode('utf-8'))
    
    for rel in root.findall('.//r:Relationship', namespaces):
        if rel.get('Id') == rid:
            target = rel.get('Target')
            if target:
                return 'word/' + target.replace('../', '')
    
    return None

def extract_images_fixed(docx_path, output_dir=None):
    """提取图片主函数"""
    docx_path = Path(docx_path)
    
    if not docx_path.exists():
        print(f"错误:文件不存在 - {docx_path}")
        return
    
    if output_dir is None:
        output_dir = docx_path.parent / 'images'
    else:
        output_dir = Path(output_dir)
    
    output_dir.mkdir(parents=True, exist_ok=True)
    print(f"输出目录:{output_dir}\n")
    
    with ZipFile(docx_path, 'r') as docx_zip:
        document_xml = docx_zip.read('word/document.xml').decode('utf-8', errors='ignore')
        rels_xml = docx_zip.read('word/_rels/document.xml.rels').decode('utf-8', errors='ignore')
        
        image_files = [f for f in docx_zip.namelist() if f.startswith('word/media/')]
        print(f"文档中共有 {len(image_files)} 张图片\n")
        
        # 提取图片和标题的映射关系
        image_title_map = extract_image_title_mapping(document_xml)
        print(f"识别到 {len(image_title_map)} 张带标题的图片\n")
        
        # 提取图片
        extracted_count = 0
        
        for rid, title in image_title_map.items():
            try:
                image_path = get_image_file_from_rid(rels_xml, rid)
                
                if not image_path or image_path not in image_files:
                    print(f"✗ {title} - 未找到对应的图片文件")
                    continue
                
                image_data = docx_zip.read(image_path)
                
                # 确定文件扩展名
                ext = Path(image_path).suffix
                if not ext:
                    if image_data[:8] == b'\x89PNG\r\n\x1a\n':
                        ext = '.png'
                    elif image_data[:2] == b'\xff\xd8':
                        ext = '.jpg'
                    elif image_data[:6] in [b'GIF87a', b'GIF89a']:
                        ext = '.gif'
                    else:
                        ext = '.png'
                
                # 保存图片
                output_path = output_dir / f"{title}{ext}"
                
                # 处理重名
                counter = 1
                while output_path.exists():
                    output_path = output_dir / f"{title}_{counter}{ext}"
                    counter += 1
                
                with open(output_path, 'wb') as f:
                    f.write(image_data)
                
                file_size = len(image_data) / 1024
                print(f"✓ {output_path.name} ({file_size:.1f} KB)")
                extracted_count += 1
                
            except Exception as e:
                print(f"✗ {title} - 提取失败:{e}")
        
        print(f"\n提取完成!成功提取 {extracted_count} 张图片")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("使用方法:python extract_images.py <Word文档路径> [输出目录]")
        sys.exit(1)
    
    word_path = sys.argv[1]
    output_dir = sys.argv[2] if len(sys.argv) > 2 else None
    
    extract_images_fixed(word_path, output_dir)

Windows 批处理脚本(可选)

@echo off
if "%~1"=="" (
    echo 请拖拽Word文档到此脚本
    pause
    exit /b
)

set "word_file=%~1"
set "output_dir=%~2"
set "script_name=extract_images.py"

REM 检测Python
python --version >nul 2>&1
if %errorlevel% equ 0 (
    set "PYTHON_CMD=python"
    goto :RUN
)

py --version >nul 2>&1
if %errorlevel% equ 0 (
    set "PYTHON_CMD=py"
    goto :RUN
)

echo Python未安装
pause
exit /b

:RUN
if "%output_dir%"=="" (
    set "output_dir=%~dp1images"
)

"%PYTHON_CMD%" "%script_name%" "%word_file%" "%output_dir%"
pause

使用方法

方式一:Python 命令行

# 基本用法(图片保存到文档同目录的images文件夹)
python extract_images.py "报告.docx"

# 指定输出目录
python extract_images.py "报告.docx" "D:/output/images"

方式二:Windows 拖拽(使用批处理脚本)

  1. 将 Word 文档拖拽到 提取图片.bat
  2. 图片自动保存到文档同目录的 images 文件夹

注意事项

  1. 仅支持 .docx 格式,不支持 .doc 格式
  2. 图片标题格式:支持 图1-1:标题图1-1 标题 格式
  3. 标题位置:标题应在图片所在段落或后续2个段落中
  4. 重名处理:自动添加序号后缀(如 图1-1_标题_1.png

总结

这个工具通过解析 Word 文档的 XML 结构,实现了图片的自动提取和智能命名。核心难点在于正确关联图片元素和标题文本,通过 relationship ID 建立映射关系即可解决。

代码使用 Python 标准库,无需安装额外依赖,开箱即用。