Python使用标准库实现将Word文档转换为HTML

作者:

文章目录
  • .docx文件本质上是一个ZIP压缩包,其中包含多个XML文件和其他资源。这种格式基于Office Open XML标准,使得我们可以通过解压和解析XML来直接访问文档内容。与使用第三方库如python-docx相比,直接使用标准库处理.docx文件提供了更底层的控制能力,能够实现更精细的转换控制。 脚本首先利用zipfile模块解压.docx文件,然后通过xml.etree.ElementTree解析XML结构。这种方法不依赖任何外部依赖,保证了代码的轻量性和可移植性。
  • 运行脚本非常简单,只需执行主函数即可完成整个转换过程: 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) # 保存结果 脚本会输出文档结构分析、转换进度和结果文件路径,提供完整的处理反馈。
  • 与使用第三方库(如python-docx、Apache POI等)相比,这种基于标准库的方法具有独特优势: 零依赖:仅使用Python标准库,无需安装额外包 深度控制:直接访问.docx文件内部结构,可实现高度定制化转换 学习价值:深入了解Word文档的底层结构和Open XML标准 然而,这种方法也需要更多的代码量和对XML结构的深入理解,在开发效率上可能不如使用成熟的第三方库。
  • 这种Word到HTML转换技术在多个领域有广泛应用: 内容管理系统:将Word格式的内容转换为网页展示 文档自动化:批量处理大量Word文档并发布为网页 协作平台:保留修订记录和评论的文档转换 电子邮件模板:将Word设计的模板转换为HTML邮件格式
  • 本文详细解析了使用Python标准库将Word文档转换为HTML的完整实现。这种方法不依赖任何第三方库,通过直接解析.docx文件的ZIP和XML结构,实现了高质量的文档转换。 脚本的主要优点包括完整的格式支持、精细的转换控制和零外部依赖。如果需要进一步扩展功能,可以考虑添加对更复杂元素(如图表、公式、注释)的支持,或优化输出HTML的样式和响应式设计。 通过深入理解这一实现,开发者可以更好地掌握Word文档处理的底层原理,并根据具体需求定制更专业的文档转换解决方案。
  • # 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)}” rel=”external nofollow” >{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() 以上就是Python使用标准库实现将Word文档转换为HTML的详细内容,更多关于Python Word转HTML的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: Python实现将Markdown转为Word、HTML、PDF、PNG和JPG 从基础到进阶详解Python高效实现Word转HTML的全流程方案 利用Python高效实现Word转HTML的全流程方案 Python实现高效将Word文档转换为HTML Python高效实现HTML转为Word和PDF Python轻松实现Word文档对比并生成可视化HTML报告 Python实现HTML转Word的示例代码 基于Python实现Word转HTML 如何利用Python将html转为pdf、word文件
  • 目录
    • 1. Word文档格式基础:了解.docx文件本质
    • 2. 脚本核心功能解析
      • 2.1 文档结构分析
      • 2.2 文本内容提取
      • 2.3 文档结构解析
    • 3. 转换过程关键技术细节
      • 3.1 段落处理与格式转换
      • 3.2 文本格式处理
      • 3.3 表格转换
      • 3.4 图片和超链接处理
    • 4. 高级功能实现
      • 4.1 页眉页脚处理
      • 4.2 修订标记处理
      • 4.3 样式应用
    • 5. 使用方法和输出示例
      • 6. 与其他方法的比较
        • 7. 实际应用场景
          • 8. 总结与扩展建议
            • 9.完整代码

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

              def process_image(docx, image_id, doc_rels):
                  """处理图片"""
                  # 查找图片关系并转换为Base64编码
                  # ...
              
              def process_hyperlink(rel_id, text, doc_rels):
                  """处理超链接"""
                  # 查找关系中的目标URL并生成<a>标签
                  # ...
              

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

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

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

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

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

              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)  # 保存结果
              

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

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

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

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

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

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

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

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

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

              # 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)}" rel="external nofollow" >{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()
              

              以上就是Python使用标准库实现将Word文档转换为HTML的详细内容,更多关于Python Word转HTML的资料请关注风君子博客其它相关文章!

              您可能感兴趣的文章:

              • Python实现将Markdown转为Word、HTML、PDF、PNG和JPG
              • 从基础到进阶详解Python高效实现Word转HTML的全流程方案
              • 利用Python高效实现Word转HTML的全流程方案
              • Python实现高效将Word文档转换为HTML
              • Python高效实现HTML转为Word和PDF
              • Python轻松实现Word文档对比并生成可视化HTML报告
              • Python实现HTML转Word的示例代码
              • 基于Python实现Word转HTML
              • 如何利用Python将html转为pdf、word文件

              站内搜索