使用Python标准库将Word文档转换为HTML:深入解析.docx文件处理脚本

在日常文档处理中,我们经常需要将Word文档转换为HTML格式以便在网页上展示。虽然存在多种第三方库可以实现这一功能,但Python的标准库同样提供了强大而灵活的工具来处理.docx文件。本文将详细解析一个使用纯Python标准库实现的Word到HTML转换脚本,展示如何直接处理.docx文件的内部结构。

1. Word文档格式基础:了解.docx文件本质

.docx文件本质上是一个ZIP压缩包,其中包含多个XML文件和其他资源。这种格式基于Office Open XML标准,使得我们可以通过解压和解析XML来直接访问文档内容。与使用第三方库如python-docx相比,直接使用标准库处理.docx文件提供了更底层的控制能力,能够实现更精细的转换控制。

脚本首先利用zipfile模块解压.docx文件,然后通过xml.etree.ElementTree解析XML结构。这种方法不依赖任何外部依赖,保证了代码的轻量性和可移植性。

2. 脚本核心功能解析

2.1 文档结构分析

脚本通过list_docx_contents函数列出.docx文件中的所有内部文件,这些文件包括文档主体、样式、关系、媒体资源等。这种分析有助于理解Word文档的复杂结构,为准确提取内容奠定基础。

python 复制代码
def list_docx_contents(filepath):
    """列出 Word 文档中的所有文件"""
    with zipfile.ZipFile(filepath, 'r') as docx:
        return docx.namelist()

2.2 文本内容提取

通过extract_text_from_docx函数,脚本从word/document.xml中提取所有文本内容。Word文档使用特定的XML命名空间和结构来存储文本,脚本需要正确解析这些结构才能准确提取内容。

2.3 文档结构解析

analyze_docx_structure函数提供了深入的文档结构分析,包括段落样式、列表属性、文本格式等。这一功能对于理解Word文档的视觉层次结构至关重要,是实现高质量HTML转换的基础。

3. 转换过程关键技术细节

3.1 段落处理与格式转换

脚本的process_paragraph函数负责将Word段落转换为相应的HTML元素。它识别各种段落样式(如标题、正文、列表项)并应用对应的HTML标签:

  • 标题段落(Heading1-Heading6)转换为<h1>-<h6>标签
  • 普通段落转换为<p>标签
  • 列表项转换为<li>标签,并自动管理<ul><ol>父容器

此外,该函数还处理段落级格式设置,包括对齐方式、缩进、行间距和背景色等。

3.2 文本格式处理

process_paragraph_formatting函数深入处理段落内的丰富文本格式:

  • 字体样式 :识别粗体(<strong>)、斜体(<em>)、下划线(<u>)和删除线
  • 上下标 :正确处理上标(<sup>)和下标(<sub>
  • 字体属性:提取字体大小、颜色、高亮和字符间距
  • 特殊内容:处理超链接、图片、表单字段和修订标记(删除和插入内容)

这种精细的格式处理确保了转换后的HTML能够保留原文档的视觉特征。

3.3 表格转换

process_table函数处理Word表格的复杂结构,支持:

  • 基本表格结构(<table><tr><td>
  • 合并单元格(跨行rowspan和跨列colspan)
  • 表头识别(第一行自动使用<th>标签)

该函数使用矩阵跟踪法正确处理单元格合并关系,确保复杂表格结构的准确转换。

3.4 图片和超链接处理

脚本能够提取文档中的图片并将其转换为Base64编码的HTML图片标签,同时正确处理内部和外部超链接:

python 复制代码
def process_image(docx, image_id, doc_rels):
    """处理图片"""
    # 查找图片关系并转换为Base64编码
    # ...

def process_hyperlink(rel_id, text, doc_rels):
    """处理超链接"""
    # 查找关系中的目标URL并生成<a>标签
    # ...

这种方法确保图片和链接在HTML中正确显示,即使图片是嵌入在.docx文件中的。

4. 高级功能实现

4.1 页眉页脚处理

process_headers_footers函数单独处理文档的页眉和页脚内容,将其转换为具有特定样式的HTML元素,保持文档的完整结构。

4.2 修订标记处理

脚本能够识别和处理Word的修订标记(跟踪更改),将删除的内容显示为带删除线的文本,插入的内容显示为带背景高亮,这在协作编辑环境中特别有用。

4.3 样式应用

生成的HTML包含完整的CSS样式定义,确保转换结果在浏览器中的显示效果接近原Word文档。脚本允许自定义样式表,满足不同的视觉需求。

5. 使用方法和输出示例

运行脚本非常简单,只需执行主函数即可完成整个转换过程:

python 复制代码
def main():
    print("Word 文档处理工具")
    print_docx_info('test_input.docx')  # 分析文档结构
    html_content = docx_to_html('test_input.docx')  # 转换为HTML
    save_html('output.html', html_content)  # 保存结果

脚本会输出文档结构分析、转换进度和结果文件路径,提供完整的处理反馈。

6. 与其他方法的比较

与使用第三方库(如python-docx、Apache POI等)相比,这种基于标准库的方法具有独特优势:

  • 零依赖:仅使用Python标准库,无需安装额外包
  • 深度控制:直接访问.docx文件内部结构,可实现高度定制化转换
  • 学习价值:深入了解Word文档的底层结构和Open XML标准

然而,这种方法也需要更多的代码量和对XML结构的深入理解,在开发效率上可能不如使用成熟的第三方库。

7. 实际应用场景

这种Word到HTML转换技术在多个领域有广泛应用:

  • 内容管理系统:将Word格式的内容转换为网页展示
  • 文档自动化:批量处理大量Word文档并发布为网页
  • 协作平台:保留修订记录和评论的文档转换
  • 电子邮件模板:将Word设计的模板转换为HTML邮件格式

8. 总结与扩展建议

本文详细解析了使用Python标准库将Word文档转换为HTML的完整实现。这种方法不依赖任何第三方库,通过直接解析.docx文件的ZIP和XML结构,实现了高质量的文档转换。

脚本的主要优点包括完整的格式支持、精细的转换控制和零外部依赖。如果需要进一步扩展功能,可以考虑添加对更复杂元素(如图表、公式、注释)的支持,或优化输出HTML的样式和响应式设计。

通过深入理解这一实现,开发者可以更好地掌握Word文档处理的底层原理,并根据具体需求定制更专业的文档转换解决方案。

python 复制代码
# Word 文档处理脚本
# 使用 Python 标准库处理 .docx 文件并转换为 HTML

import zipfile
import xml.etree.ElementTree as ET
import html
import base64
import os


def list_docx_contents(filepath):
    """列出 Word 文档中的所有文件"""
    with zipfile.ZipFile(filepath, 'r') as docx:
        return docx.namelist()


def extract_text_from_docx(filepath):
    """从 Word 文档中提取文本内容"""
    with zipfile.ZipFile(filepath) as docx:
        # 读取 document.xml
        xml_content = docx.read('word/document.xml')
        tree = ET.fromstring(xml_content)
        
        # 定义命名空间
        namespaces = {
            'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
        }
        
        # 提取所有段落文本
        paragraphs = tree.findall('.//w:p', namespaces)
        text_parts = []
        
        for para in paragraphs:
            # 提取段落中所有文本
            texts = para.findall('.//w:t', namespaces)
            para_text = ''.join([t.text for t in texts if t.text])
            if para_text:
                text_parts.append(para_text)
        
        return '\n'.join(text_parts)


def analyze_docx_structure(filepath):
    """分析 Word 文档结构"""
    with zipfile.ZipFile(filepath) as docx:
        # 读取 document.xml
        xml_content = docx.read('word/document.xml')
        tree = ET.fromstring(xml_content)
        
        # 定义命名空间
        namespaces = {
            'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
        }
        
        # 分析段落
        paragraphs = tree.findall('.//w:p', namespaces)
        print(f"总共找到 {len(paragraphs)} 个段落")

        for i, para in enumerate(paragraphs[:20]):  # 分析前20个段落
            num_pr = para.find('.//w:numPr', namespaces)
            p_style_elem = para.find('.//w:pStyle', namespaces)
            p_style = p_style_elem.get(f'{{{namespaces["w"]}}}val') if p_style_elem is not None else None

            # 检查段落中是否有加粗文本
            bold_runs = []
            for run in para.findall('.//w:r', namespaces):
                text_elem = run.find('.//w:t', namespaces)
                bold_elem = run.find('.//w:b', namespaces)
                if text_elem is not None and text_elem.text:
                    bold_runs.append((text_elem.text, bold_elem is not None))

            # 提取文本
            texts = [r.find('.//w:t', namespaces) for r in para.findall('.//w:r', namespaces)]
            para_text = ''.join([t.text for t in texts if t is not None and t.text])

            # 检查是否有加粗文本
            has_bold = any(bold for _, bold in bold_runs)
            
            # 显示哪些具体文本是加粗的
            bold_texts = [text for text, is_bold in bold_runs if is_bold]
            
            print(f"段落 {i}: 样式={p_style}, 列表属性={num_pr is not None}, 有加粗={has_bold}, 加粗文本={bold_texts}, 文本='{para_text[:50]}{'...' if len(para_text) > 50 else ''}'")


def process_hyperlink(rel_id, text, doc_rels):
    """处理超链接"""
    # 查找关系中的目标URL
    target_url = None
    for rel in doc_rels.findall('.//Relationship'):
        if rel.get('Id') == rel_id:
            target_url = rel.get('Target')
            break
    
    if target_url:
        return f'<a href="{html.escape(target_url)}">{html.escape(text)}</a>'
    else:
        return html.escape(text)


def process_image(docx, image_id, doc_rels):
    """处理图片"""
    # 查找图片关系
    image_path = None
    for rel in doc_rels.findall('.//Relationship'):
        if rel.get('Id') == image_id:
            image_path = rel.get('Target')
            break
    
    if not image_path:
        return '<!-- 图片未找到 -->'
    
    # 规范化路径(处理类似"../media/image1.png"的相对路径)
    if image_path.startswith('..'):
        image_path = image_path[3:]  # 移除 "../" 前缀
    
    try:
        # 从docx文件中读取图片数据
        image_data = docx.read(f'word/{image_path}')
        # 将图片编码为base64
        encoded_image = base64.b64encode(image_data).decode('utf-8')
        
        # 获取图片扩展名以确定MIME类型
        ext = os.path.splitext(image_path)[1].lower()
        mime_type = 'image/png'  # 默认
        if ext == '.jpg' or ext == '.jpeg':
            mime_type = 'image/jpeg'
        elif ext == '.gif':
            mime_type = 'image/gif'
        elif ext == '.bmp':
            mime_type = 'image/bmp'
        elif ext == '.svg':
            mime_type = 'image/svg+xml'
        
        return f'<img src="data:{mime_type};base64,{encoded_image}" alt="文档图片">'
    except Exception as e:
        return f'<!-- 图片加载失败: {str(e)} -->'


def process_paragraph_formatting(para, namespaces, docx=None, doc_rels=None):
    """处理段落内的格式化文本(粗体、斜体、颜色等)"""
    html_text = ""
    last_text = ""  # 用于跟踪上一个文本,避免重复
    
    # 遍历段落中的所有文本元素
    for run in para.findall('.//w:r', namespaces):
        # 检查是否有删除线文本(修订模式下的删除内容)
        del_text_elem = run.find('.//w:delText', namespaces)
        if del_text_elem is not None and del_text_elem.text is not None:
            # 处理删除的文本(带删除线)
            del_text = del_text_elem.text
            escaped_text = html.escape(del_text)
            html_text += f'<span style="text-decoration:line-through;">{escaped_text}</span>'
            continue
            
        # 检查是否有插入的文本(修订模式下的新增内容)
        ins_elem = run.find('.//w:ins', namespaces)
        if ins_elem is not None:
            # 获取插入的文本
            ins_texts = ins_elem.findall('.//w:t', namespaces)
            for ins_text_elem in ins_texts:
                if ins_text_elem.text:
                    ins_text = ins_text_elem.text
                    escaped_text = html.escape(ins_text)
                    html_text += f'<span style="background-color:#ccffcc;">{escaped_text}</span>'
            continue
            
        # 提取文本
        text_elem = run.find('.//w:t', namespaces)
        if text_elem is None or text_elem.text is None:
            # 检查是否是制表符
            if run.find('.//w:tab', namespaces) is not None:
                html_text += "&emsp;"  # 处理制表符
                continue
                
            # 检查是否是图片
            drawing_elem = run.find('.//w:drawing', namespaces)
            if drawing_elem is not None and docx is not None and doc_rels is not None:
                # 处理图片
                blip_elem = drawing_elem.find('.//a:blip', {
                    'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'
                })
                if blip_elem is not None:
                    embed_id = blip_elem.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                    if embed_id:
                        img_html = process_image(docx, embed_id, doc_rels)
                        html_text += img_html
                continue
                
            # 检查是否是下划线字符(空的文本但有下划线格式)
            underline_elem = run.find('.//w:u', namespaces)
            if underline_elem is not None:
                # 获取下划线类型
                underline_val = underline_elem.get(f'{{{namespaces["w"]}}}val')
                # 添加表单字段占位符
                html_text += '<span class="form-field">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>'
            continue
            
        text = text_elem.text
        
        # 检查是否是重复文本(有时候Word中会有重复的文本节点)
        if text == last_text:
            continue
            
        last_text = text
        escaped_text = html.escape(text)
        
        # 检查超链接
        hyperlink_elem = run.find('.//w:hyperlink', namespaces)
        if hyperlink_elem is not None and doc_rels is not None:
            rel_id = hyperlink_elem.get(f'{{{namespaces["w"]}}}anchor') or hyperlink_elem.get(f'{{{namespaces["w"]}}}id')
            if rel_id:
                hyperlink_html = process_hyperlink(rel_id, text, doc_rels)
                html_text += hyperlink_html
                continue
        
        # 检查格式
        bold_elem = run.find('.//w:b', namespaces)
        bold = False
        if bold_elem is not None:
            # 检查是否明确设置了val属性为false
            val = bold_elem.get(f'{{{namespaces["w"]}}}val')
            if val is None or val != 'false':
                bold = True
        
        # 检查字体,如果使用黑体等加粗字体,也应视为加粗
        rpr_elem = run.find('.//w:rPr', namespaces)
        if rpr_elem is not None:
            # 检查字体设置
            font_elem = rpr_elem.find('.//w:rFonts', namespaces)
            if font_elem is not None:
                # 获取各种字体属性
                ascii_font = font_elem.get(f'{{{namespaces["w"]}}}ascii')
                hansi_font = font_elem.get(f'{{{namespaces["w"]}}}hAnsi')
                east_asia_font = font_elem.get(f'{{{namespaces["w"]}}}eastAsia')
                
                # 如果使用黑体等加粗字体,视为加粗
                bold_fonts = ['黑体', 'Bold', 'Arial Black', 'Impact']
                if (ascii_font and any(bf in ascii_font for bf in bold_fonts)) or \
                   (hansi_font and any(bf in hansi_font for bf in bold_fonts)) or \
                   (east_asia_font and any(bf in east_asia_font for bf in bold_fonts)):
                    bold = True
                
        italic_elem = run.find('.//w:i', namespaces)
        italic = False
        if italic_elem is not None:
            val = italic_elem.get(f'{{{namespaces["w"]}}}val')
            if val is None or val != 'false':
                italic = True
                
        underline_elem = run.find('.//w:u', namespaces)
        underline = False
        if underline_elem is not None:
            val = underline_elem.get(f'{{{namespaces["w"]}}}val')
            if val is None or val != 'none':
                underline = True
                
        strike_elem = run.find('.//w:strike', namespaces)
        strike = False
        if strike_elem is not None:
            val = strike_elem.get(f'{{{namespaces["w"]}}}val')
            if val is None or val != 'false':
                strike = True
        
        # 检查上下标
        vert_align_elem = run.find('.//w:vertAlign', namespaces)
        vert_align = None
        if vert_align_elem is not None:
            align_val = vert_align_elem.get(f'{{{namespaces["w"]}}}val')
            if align_val == 'superscript':
                vert_align = 'sup'
            elif align_val == 'subscript':
                vert_align = 'sub'
        
        # 检查字体大小
        sz_elem = run.find('.//w:sz', namespaces)
        font_size = None
        if sz_elem is not None:
            sz_val = sz_elem.get(f'{{{namespaces["w"]}}}val')
            if sz_val:
                # Word中的字号是半点为单位,需要转换为pt
                font_size = f"{int(sz_val)/2}pt"
        
        # 检查高亮色
        highlight_elem = run.find('.//w:highlight', namespaces)
        background_color = None
        if highlight_elem is not None:
            highlight_val = highlight_elem.get(f'{{{namespaces["w"]}}}val')
            # 需要将Word颜色名称映射为CSS颜色值
            color_map = {
                'yellow': '#ffff00',
                'green': '#00ff00',
                'red': '#ff0000',
                'blue': '#0000ff',
                'cyan': '#00ffff',
                'magenta': '#ff00ff',
                'black': '#000000',
                'white': '#ffffff'
            }
            background_color = color_map.get(highlight_val, highlight_val)
        
        # 检查颜色
        color_elem = run.find('.//w:color', namespaces)
        color = None
        if color_elem is not None:
            color_val = color_elem.get(f'{{{namespaces["w"]}}}val')
            if color_val and color_val != '000000':  # 忽略黑色(默认颜色)
                # 处理自动颜色
                if color_val.lower() == 'auto':
                    color = 'inherit'
                else:
                    color = f"#{color_val}"
        
        # 检查字符间距
        spacing_elem = run.find('.//w:spacing', namespaces)
        letter_spacing = None
        if spacing_elem is not None:
            spacing_val = spacing_elem.get(f'{{{namespaces["w"]}}}val')
            if spacing_val:
                # Word中字符间距单位是 twentieths of a point (1/1440 inch)
                # 转换为CSS的em单位
                spacing_em = int(spacing_val) / 120  # 120 = 20 * 6 (approximation)
                if spacing_em != 0:
                    letter_spacing = f"{spacing_em:.2f}em"
        
        # 检查背景色
        shd_elem = rpr_elem.find('.//w:shd', namespaces) if rpr_elem is not None else None
        shading_color = None
        if shd_elem is not None:
            shd_fill = shd_elem.get(f'{{{namespaces["w"]}}}fill')
            if shd_fill and shd_fill != 'auto' and shd_fill != 'clear':
                shading_color = f"#{shd_fill}"
        
        # 构建样式字符串
        styles = []
        if font_size:
            styles.append(f"font-size:{font_size}")
        if background_color:
            styles.append(f"background-color:{background_color}")
        if color and color != 'inherit':
            styles.append(f"color:{color}")
        elif color == 'inherit':
            styles.append(f"color:{color}")
        if letter_spacing:
            styles.append(f"letter-spacing:{letter_spacing}")
        if shading_color:
            styles.append(f"background-color:{shading_color}")
        
        style_attr = f' style="{";".join(styles)}"' if styles else ''
        
        # 应用格式
        if bold:
            escaped_text = f'<strong>{escaped_text}</strong>'
        if italic:
            escaped_text = f'<em>{escaped_text}</em>'
        if underline:
            escaped_text = f'<u>{escaped_text}</u>'  # 使用标准HTML下划线标签
        if strike:
            escaped_text = f'<span style="text-decoration:line-through">{escaped_text}</span>'
        if vert_align:
            escaped_text = f'<{vert_align}>{escaped_text}</{vert_align}>'
        if style_attr:
            escaped_text = f'<span{style_attr}>{escaped_text}</span>'
            
        html_text += escaped_text
    
    return html_text


def process_paragraph(p, namespaces, docx=None, doc_rels=None):
    """处理段落"""
    # 获取段落属性
    p_pr = p.find('./w:pPr', namespaces)
    
    # 检查段落样式
    p_style_elem = p_pr.find('.//w:pStyle', namespaces) if p_pr is not None else None
    p_style = p_style_elem.get(f'{{{namespaces["w"]}}}val') if p_style_elem is not None else None
    
    # 如果是标题样式,直接返回标题标签
    if p_style and p_style.startswith('Heading'):
        # 处理段落中的文本
        formatted_text = process_paragraph_formatting(p, namespaces, docx, doc_rels)
        
        # 根据样式确定标题级别
        heading_level = 1
        if p_style == 'Heading1':
            heading_level = 1
        elif p_style == 'Heading2':
            heading_level = 2
        elif p_style == 'Heading3':
            heading_level = 3
        elif p_style == 'Heading4':
            heading_level = 4
        elif p_style == 'Heading5':
            heading_level = 5
        elif p_style == 'Heading6':
            heading_level = 6
            
        return f'<h{heading_level}>{formatted_text}</h{heading_level}>'
    
    # 处理段落样式
    style_attrs = []
    
    # 处理文本对齐
    jc_elem = p_pr.find('.//w:jc', namespaces) if p_pr is not None else None
    if jc_elem is not None:
        jc_val = jc_elem.get(f'{{{namespaces["w"]}}}val')
        if jc_val == 'center':
            style_attrs.append('text-align:center')
        elif jc_val == 'right':
            style_attrs.append('text-align:right')
        elif jc_val == 'both':
            style_attrs.append('text-align:justify')
        else:
            style_attrs.append('text-align:left')
    else:
        style_attrs.append('text-align:left')
    
    # 处理段落缩进
    ind_elem = p_pr.find('.//w:ind', namespaces) if p_pr is not None else None
    if ind_elem is not None:
        # 左缩进
        left_ind = ind_elem.get(f'{{{namespaces["w"]}}}left')
        if left_ind:
            # Word中单位为twips,1 twip = 1/1440 inch,约等于1/20 pt
            style_attrs.append(f'margin-left:{int(left_ind)/20}pt')
        
        # 首行缩进
        first_line_ind = ind_elem.get(f'{{{namespaces["w"]}}}firstLine')
        if first_line_ind:
            style_attrs.append(f'text-indent:{int(first_line_ind)/20}pt')
        
        # 悬挂缩进
        hanging_ind = ind_elem.get(f'{{{namespaces["w"]}}}hanging')
        if hanging_ind:
            # 悬挂缩进需要特殊处理
            style_attrs.append(f'text-indent:-{int(hanging_ind)/20}pt;padding-left:{int(hanging_ind)/20}pt')
    
    # 处理行间距
    spacing_elem = p_pr.find('.//w:spacing', namespaces) if p_pr is not None else None
    if spacing_elem is not None:
        # 行间距
        line_val = spacing_elem.get(f'{{{namespaces["w"]}}}line')
        if line_val:
            # Word中的行距单位是twips (1/20 pt)
            # 通常240 twips = 12 pt = 单倍行距
            line_height = int(line_val) / 240
            if line_height > 0:
                # 避免重复添加line-height样式
                existing_line_height = any(attr.startswith('line-height:') for attr in style_attrs)
                if not existing_line_height:
                    style_attrs.append(f'line-height:{line_height}em')
        
        # 行距规则
        line_rule = spacing_elem.get(f'{{{namespaces["w"]}}}lineRule')
        if line_rule == 'auto':
            # 自动行距已在上面处理
            pass
        elif line_rule == 'atLeast':
            # 最小行距
            line_val = spacing_elem.get(f'{{{namespaces["w"]}}}line')
            if line_val:
                min_line_height = int(line_val) / 240
                # 避免重复添加line-height样式
                existing_line_height = any(attr.startswith('line-height:') for attr in style_attrs)
                if not existing_line_height:
                    style_attrs.append(f'line-height:min({min_line_height}em)')
        elif line_rule == 'exact':
            # 固定行距
            line_val = spacing_elem.get(f'{{{namespaces["w"]}}}line')
            if line_val:
                fixed_line_height = int(line_val) / 240
                # 避免重复添加line-height样式
                existing_line_height = any(attr.startswith('line-height:') for attr in style_attrs)
                if not existing_line_height:
                    style_attrs.append(f'line-height:{fixed_line_height}em')
    
    # 处理段落底纹/背景色
    shd_elem = p_pr.find('.//w:shd', namespaces) if p_pr is not None else None
    if shd_elem is not None:
        shd_fill = shd_elem.get(f'{{{namespaces["w"]}}}fill')
        if shd_fill and shd_fill != 'auto' and shd_fill != 'clear':
            style_attrs.append(f'background-color:#{shd_fill}')
    
    style_str = f' style="{";".join(style_attrs)}"' if style_attrs else ''
    
    # 处理段落中的文本
    formatted_text = process_paragraph_formatting(p, namespaces, docx, doc_rels)
    
    # 检查是否为列表项
    num_pr = p_pr.find('.//w:numPr', namespaces) if p_pr is not None else None
    if num_pr is not None:
        # 获取编号信息以确定是有序还是无序列表
        num_id_elem = num_pr.find('.//w:numId', namespaces)
        ilvl_elem = num_pr.find('.//w:ilvl', namespaces)
        
        # 默认使用无序列表
        list_type = 'ul'
        # 如果有编号ID,则尝试判断是否为有序列表
        if num_id_elem is not None:
            num_id_val = num_id_elem.get(f'{{{namespaces["w"]}}}val')
            # 可以根据num_id_val的值来判断是否为有序列表
            # 这里简化处理,默认认为所有列表都是无序列表
            # 实际项目中可以从word/numbering.xml中获取详细信息
            
        return f'<li{style_str}>{formatted_text}</li>'
    else:
        return f'<p{style_str}>{formatted_text}</p>'


def process_table(table, namespaces, docx=None, doc_rels=None):
    """处理表格,支持合并单元格等复杂结构"""
    html_parts = ['<table>']
    
    # 处理表格行
    rows = table.findall('.//w:tr', namespaces)
    
    # 创建一个矩阵来跟踪已被占用的单元格位置,用于处理跨行合并
    cell_matrix = [[False for _ in range(20)] for _ in range(len(rows))]  # 假设最多20列
    
    for i, row in enumerate(rows):
        html_parts.append('<tr>')
        
        # 处理单元格
        cells = row.findall('.//w:tc', namespaces)
        
        cell_index = 0  # 当前行的实际单元格索引
        matrix_col = 0  # 在矩阵中的列位置
        
        # 找到下一个可用的列位置
        while matrix_col < len(cell_matrix[i]) and cell_matrix[i][matrix_col]:
            matrix_col += 1
            
        for cell in cells:
            # 跳过已经被前面的跨行单元格占据的位置
            while matrix_col < len(cell_matrix[i]) and cell_matrix[i][matrix_col]:
                matrix_col += 1
                
            # 确定是表头还是普通单元格
            cell_tag = 'th' if i == 0 else 'td'
            
            # 检查单元格合并属性
            tc_pr = cell.find('.//w:tcPr', namespaces)
            colspan = ''
            rowspan = ''
            
            if tc_pr is not None:
                # 检查水平合并 (gridSpan)
                grid_span = tc_pr.find('.//w:gridSpan', namespaces)
                if grid_span is not None:
                    span_val = grid_span.get(f'{{{namespaces["w"]}}}val')
                    if span_val:
                        colspan = f' colspan="{span_val}"'
                        # 标记这些列位置已被占用
                        span_count = int(span_val)
                        for col_offset in range(span_count):
                            if matrix_col + col_offset < len(cell_matrix[i]):
                                cell_matrix[i][matrix_col + col_offset] = True
                
                # 检查垂直合并 (vMerge)
                v_merge = tc_pr.find('.//w:vMerge', namespaces)
                if v_merge is not None:
                    merge_val = v_merge.get(f'{{{namespaces["w"]}}}val')
                    # 只有当是合并起点时才添加 rowspan
                    if merge_val == 'restart':
                        # 查找跨越的行数
                        rowspan_count = 1
                        # 计算需要合并多少行
                        for next_row_idx in range(i + 1, len(rows)):
                            next_row = rows[next_row_idx]
                            next_cells = next_row.findall('.//w:tc', namespaces)
                            
                            # 检查下一行对应位置是否有继续合并的标记
                            # 先找到下一行中对应位置的单元格
                            next_cell_found = False
                            next_cell_index = 0
                            current_col_pos = 0
                            
                            # 计算在下一行中对应列位置的单元格
                            for next_cell in next_cells:
                                next_tc_pr = next_cell.find('.//w:tcPr', namespaces)
                                if next_tc_pr is not None:
                                    next_grid_span = next_tc_pr.find('.//w:gridSpan', namespaces)
                                    next_colspan = 1
                                    if next_grid_span is not None:
                                        next_span_val = next_grid_span.get(f'{{{namespaces["w"]}}}val')
                                        if next_span_val:
                                            next_colspan = int(next_span_val)
                                    
                                    # 检查当前位置是否是我们寻找的位置
                                    if current_col_pos == matrix_col:
                                        # 检查这个单元格是否是继续合并的单元格
                                        next_v_merge = next_tc_pr.find('.//w:vMerge', namespaces)
                                        if next_v_merge is not None:
                                            next_merge_val = next_v_merge.get(f'{{{namespaces["w"]}}}val')
                                            if next_merge_val is None:  # 继续合并
                                                rowspan_count += 1
                                                # 标记这个位置被跨行单元格占据
                                                for col_offset in range(next_colspan):
                                                    if matrix_col + col_offset < len(cell_matrix[next_row_idx]):
                                                        cell_matrix[next_row_idx][matrix_col + col_offset] = True
                                                next_cell_found = True
                                                break
                                            else:
                                                next_cell_found = True
                                                break
                                        else:
                                            # 没有vMerge标记,说明合并结束
                                            next_cell_found = True
                                            break
                                
                                current_col_pos += next_colspan
                            
                            if not next_cell_found:
                                # 如果没找到对应的单元格,可能是被跨列占据了位置
                                # 检查这个位置是否被标记为已占用
                                if matrix_col < len(cell_matrix[next_row_idx]) and not cell_matrix[next_row_idx][matrix_col]:
                                    # 位置未被占用,说明合并结束
                                    break
                                else:
                                    # 位置被占用,继续检查下一行
                                    rowspan_count += 1
                                    # 标记这个位置被跨行单元格占据
                                    if matrix_col < len(cell_matrix[next_row_idx]):
                                        cell_matrix[next_row_idx][matrix_col] = True
                            elif next_cell_found:
                                next_v_merge = None
                                if next_cell_index < len(next_cells):
                                    next_tc_pr = next_cells[next_cell_index].find('.//w:tcPr', namespaces)
                                    if next_tc_pr is not None:
                                        next_v_merge = next_tc_pr.find('.//w:vMerge', namespaces)
                                
                                if next_v_merge is not None:
                                    next_merge_val = next_v_merge.get(f'{{{namespaces["w"]}}}val')
                                    if next_merge_val is None:  # 继续合并
                                        rowspan_count += 1
                                        # 标记这个位置被跨行单元格占据
                                        if matrix_col < len(cell_matrix[next_row_idx]):
                                            cell_matrix[next_row_idx][matrix_col] = True
                                    else:
                                        break
                                else:
                                    break
                        
                        if rowspan_count > 1:
                            rowspan = f' rowspan="{rowspan_count}"'
                    else:
                        # 不是合并起点,跳过这个单元格
                        cell_index += 1
                        continue
                else:
                    # 标记当前位置被使用
                    if matrix_col < len(cell_matrix[i]):
                        cell_matrix[i][matrix_col] = True
            
            # 如果没有设置rowspan,也要标记当前位置被使用
            if not rowspan and matrix_col < len(cell_matrix[i]):
                cell_matrix[i][matrix_col] = True
            
            # 提取单元格中的段落
            cell_paragraphs = cell.findall('.//w:p', namespaces)
            cell_content = ""
            
            for para in cell_paragraphs:
                para_content = process_paragraph_formatting(para, namespaces, docx, doc_rels)
                if para_content:
                    cell_content += para_content + "<br>" if cell_content else para_content
            
            # 如果没有内容,添加空白字符以确保单元格可见
            if not cell_content:
                cell_content = "&nbsp;"
            
            html_parts.append(f'<{cell_tag}{colspan}{rowspan}>{cell_content}</{cell_tag}>')
            
            cell_index += 1
            matrix_col += 1
            
            # 调整matrix_col到下一个可用位置
            while matrix_col < len(cell_matrix[i]) and cell_matrix[i][matrix_col]:
                matrix_col += 1
        
        html_parts.append('</tr>')
    
    html_parts.append('</table>')
    return '\n'.join(html_parts)


def process_textbox(textBox, namespaces, docx=None, doc_rels=None):
    """处理文本框"""
    # 文本框本质上是一个容器,包含段落或其他元素
    html_parts = ['<div class="textbox">']
    
    # 处理文本框中的所有段落
    paragraphs = textBox.findall('.//w:p', namespaces)
    for p in paragraphs:
        html_parts.append(process_paragraph(p, namespaces, docx, doc_rels))
    
    html_parts.append('</div>')
    return '\n'.join(html_parts)


def process_headers_footers(docx, namespaces, doc_rels):
    """处理页眉和页脚"""
    header_content = []
    footer_content = []
    
    # 尝试读取页眉
    try:
        header_files = [f for f in docx.namelist() if f.startswith('word/header') and f.endswith('.xml')]
        for header_file in header_files:
            header_xml = docx.read(header_file)
            header_tree = ET.fromstring(header_xml)
            # 处理页眉中的段落
            paragraphs = header_tree.findall('.//w:p', namespaces)
            for p in paragraphs:
                header_text = process_paragraph(p, namespaces, docx, doc_rels)
                if header_text.strip():
                    header_content.append(header_text)
    except:
        pass  # 页眉处理失败时忽略
    
    # 尝试读取页脚
    try:
        footer_files = [f for f in docx.namelist() if f.startswith('word/footer') and f.endswith('.xml')]
        for footer_file in footer_files:
            footer_xml = docx.read(footer_file)
            footer_tree = ET.fromstring(footer_xml)
            # 处理页脚中的段落
            paragraphs = footer_tree.findall('.//w:p', namespaces)
            for p in paragraphs:
                footer_text = process_paragraph(p, namespaces, docx, doc_rels)
                if footer_text.strip():
                    footer_content.append(footer_text)
    except:
        pass  # 页脚处理失败时忽略
    
    return header_content, footer_content


def process_revisions(tree, namespaces):
    """处理修订(删除和插入的内容)"""
    revision_content = []
    
    # 处理删除的内容
    del_elements = tree.findall('.//w:del', namespaces)
    for del_elem in del_elements:
        del_texts = del_elem.findall('.//w:delText', namespaces)
        for del_text in del_texts:
            if del_text.text:
                revision_content.append(f'<span style="text-decoration:line-through;background-color:#ffcccc;">{html.escape(del_text.text)}</span>')
    
    # 处理插入的内容
    ins_elements = tree.findall('.//w:ins', namespaces)
    for ins_elem in ins_elements:
        ins_texts = ins_elem.findall('.//w:t', namespaces)
        for ins_text in ins_texts:
            if ins_text.text:
                revision_content.append(f'<span style="background-color:#ccffcc;">{html.escape(ins_text.text)}</span>')
    
    return revision_content


def docx_to_html(filepath):
    """将 Word 文档转换为 HTML"""
    with zipfile.ZipFile(filepath) as docx:
        # 读取 document.xml
        xml_content = docx.read('word/document.xml')
        tree = ET.fromstring(xml_content)
        
        # 读取关系文件
        try:
            rels_content = docx.read('word/_rels/document.xml.rels')
            doc_rels = ET.fromstring(rels_content)
        except:
            doc_rels = None
        
        # 定义命名空间
        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'
        }
        
        # 创建 HTML 结构
        html_output = ['<!DOCTYPE html>', '<html>', '<head>', '<meta charset="UTF-8">', 
                '<title>Word 文档转换</title>',
                '<style>',
                'body { font-family: sans-serif; margin: 40px auto; max-width: 800px; line-height: 1.6; }',
                'h1 { color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px; }',
                'h2 { color: #666; border-bottom: 1px solid #eee; padding-bottom: 5px; }',
                'h3 { color: #999; }',
                'h4 { color: #333; }',
                'h5 { color: #666; }',
                'h6 { color: #999; }',
                'p { margin: 1em 0; }',
                'ul, ol { margin: 1em 0; padding-left: 40px; }',
                'li { margin: 0.5em 0; }',
                'table { border-collapse: collapse; width: 100%; margin: 1em 0; }',
                'td, th { border: 1px solid #ddd; padding: 8px; text-align: left; }',
                'strong { font-weight: bold; }',
                'em { font-style: italic; }',
                '.bold { font-weight: bold; }',
                '.italic { font-style: italic; }',
                'u { text-decoration: underline; }',
                '.underline { text-decoration: underline; }',
                'img { max-width: 100%; height: auto; }',
                '.header, .footer { background-color: #f5f5f5; padding: 10px; margin: 10px 0; border: 1px solid #ddd; }',
                '.header { border-bottom: 2px solid #ccc; }',
                '.footer { border-top: 2px solid #ccc; }',
                '.textbox { border: 1px solid #ccc; padding: 10px; margin: 10px 0; }',
                '.floating-element { position: relative; float: right; margin: 10px 0 10px 10px; }',
                '.form-field { border-bottom: 1px solid #000; display: inline-block; min-width: 50px; text-align: center; margin: 0 2px; padding: 0 5px; }',
                '</style>',
                '</head>', '<body>']
        
        # 处理页眉和页脚
        header_footer_content = process_headers_footers(docx, namespaces, doc_rels)
        
        # 添加页眉内容到页面顶部
        header_content = [item for item in header_footer_content if 'class="header"' in item]
        footer_content = [item for item in header_footer_content if 'class="footer"' in item]
        
        # 添加页眉
        html_output.extend(header_content)
        
        # 处理所有元素
        # 注意:在Word XML中,内容在<w:body>标签内
        body = tree.find('.//w:body', namespaces)
        if body is not None:
            # 跟踪列表状态
            in_list = False
            list_type = None
            
            for elem in body:
                if elem.tag.endswith('p'):  # 段落
                    p_pr = elem.find('./w:pPr', namespaces)
                    p_style_elem = p_pr.find('.//w:pStyle', namespaces) if p_pr is not None else None
                    p_style = p_style_elem.get(f'{{{namespaces["w"]}}}val') if p_style_elem is not None else None
                    
                    num_pr = p_pr.find('.//w:numPr', namespaces) if p_pr is not None else None
                    
                    # 检查是否为标题
                    is_heading = p_style and p_style.startswith('Heading')
                    
                    # 检查是否为列表项
                    is_list_item = num_pr is not None
                    
                    # 如果之前在列表中,但现在不是列表项,则关闭列表
                    # 标题也不应在列表中,所以也要关闭列表
                    if in_list and (not is_list_item or is_heading):
                        html_output.append(f'</{list_type}>')
                        in_list = False
                        list_type = None
                    
                    # 如果之前不在列表中,但现在是列表项,则开启列表
                    if not in_list and is_list_item and not is_heading:
                        # 尝试区分有序和无序列表
                        # 简单处理:如果有编号信息则认为是有序列表,否则为无序列表
                        list_type = 'ol' if num_pr is not None else 'ul'
                        html_output.append(f'<{list_type}>')
                        in_list = True
                    
                    html_output.append(process_paragraph(elem, namespaces, docx, doc_rels))
                    
                elif elem.tag.endswith('tbl'):  # 表格
                    # 如果在列表中,则先关闭列表
                    if in_list:
                        html_output.append(f'</{list_type}>')
                        in_list = False
                        list_type = None
                        
                    html_output.append(process_table(elem, namespaces, docx, doc_rels))
            
            # 如果循环结束后还在列表中,则关闭列表
            if in_list:
                html_output.append(f'</{list_type}>')
        else:
            # 如果找不到body标签,则直接处理tree的子元素
            for elem in tree:
                if elem.tag.endswith('p'):  # 段落
                    html_output.append(process_paragraph(elem, namespaces, docx, doc_rels))
                elif elem.tag.endswith('tbl'):  # 表格
                    html_output.append(process_table(elem, namespaces, docx, doc_rels))
                
        # 添加页脚内容到页面底部
        html_output.extend(footer_content)
        
        html_output.extend(['</body>', '</html>'])
        return '\n'.join(html_output)


def print_docx_info(filepath):
    """打印 Word 文档的基本信息"""
    print(f"正在分析文档: {filepath}")
    
    # 列出文档内容
    contents = list_docx_contents(filepath)
    print("\n文档包含以下文件:")
    for item in contents[:10]:  # 只显示前10个
        print(f"  {item}")
    
    if len(contents) > 10:
        print(f"  ... 还有 {len(contents) - 10} 个文件")
    
    # 提取并显示文本内容
    print("\n文档文本内容预览:")
    text = extract_text_from_docx(filepath)
    print(text[:500] + "..." if len(text) > 500 else text)


def save_html(filepath, html_content):
    """保存 HTML 到文件"""
    with open(filepath, 'w', encoding='utf-8') as f:
        f.write(html_content)
    print(f"HTML 文件已保存到: {filepath}")


def main():
    """主函数"""
    print("Word 文档处理工具")
    print("==================")
    
    try:
        # 显示文档信息
        print_docx_info('test_input.docx')
        
        # 分析文档结构
        print("\n文档结构分析:")
        analyze_docx_structure('test_input.docx')
        
        # 转换为 HTML
        print("\n正在将 Word 文档转换为 HTML...")
        html_content = docx_to_html('test_input.docx')
        
        # 保存 HTML 文件
        save_html('output.html', html_content)
        print("转换完成!")
        
    except FileNotFoundError:
        print("未找到 test_input.docx 文件,请确保文件存在于项目根目录中")
    except Exception as e:
        print(f"处理文档时发生错误: {e}")
        import traceback
        traceback.print_exc()


# 程序入口点
if __name__ == '__main__':
    main()
相关推荐
Echo_NGC22372 小时前
【传统JSCC+Deep JSCC】联合信源信道编码完全指南
人工智能·python·深度学习·神经网络·conda·无人机·jscc
祁思妙想2 小时前
Python中CORS 跨域中间件的配置和作用原理
开发语言·python·中间件
天才测试猿2 小时前
Postman常见问题及解决方法
自动化测试·软件测试·python·测试工具·职场和发展·接口测试·postman
wtsolutions2 小时前
Sheet-to-Doc 支持 JSON 和 JSONL 格式:批量生成 Word 文档的新方式
json·word·wtsolutions·sheet-to-doc
棒棒的皮皮3 小时前
【OpenCV】Python图像处理形态学之腐蚀
图像处理·python·opencv·计算机视觉
坐吃山猪3 小时前
Python命令行工具argparse
开发语言·python
创作者mateo3 小时前
python进阶之文件处理
开发语言·python
前端程序猿之路3 小时前
模型应用开发的基础工具与原理之Web 框架
前端·python·语言模型·学习方法·web·ai编程·改行学it