#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
完整的 DOC/DOCX 文件读取器
支持 macOS,无需复杂依赖
"""
import os
import subprocess
import sys
import re
from pathlib import Path
class DocReader:
"""DOC/DOCX 文件读取器"""
def __init__(self):
self.supported_formats = ['.doc', '.docx', '.rtf']
def clean_text(self, text):
"""
清理文档文本,移除HYPERLINK等格式标记
Args:
text (str): 原始文本
Returns:
str: 清理后的文本
"""
if not text:
return text
# 首先清理HYPERLINK格式
text = self.clean_hyperlinks(text)
# 2. 清理多余的空格和换行
text = re.sub(r'\s+', ' ', text) # 多个空格合并为一个
text = re.sub(r'\n\s*\n', '\n', text) # 多个换行合并
# 3. 清理Word文档中常见的格式标记
# 清理页码、页眉页脚标记
text = re.sub(r'第\s*\d+\s*页.*?共\s*\d+\s*页', '', text)
# 4. 清理其他可能的Word格式标记
# 清理字段代码 { }
text = re.sub(r'\{[^}]*\}', '', text)
# 5. 恢复正常的词语间距(针对中文)
# 修复被分割的中文词语,如"民 法典" -> "民法典"
text = re.sub(r'([一-龯])\s+([一-龯])', r'\1\2', text)
# 6. 清理行首行尾空格
lines = []
for line in text.split('\n'):
line = line.strip()
if line:
lines.append(line)
return '\n'.join(lines)
def clean_hyperlinks(self, text):
"""
专门清理HYPERLINK格式的方法
示例输入:
'双方在离婚协议中明确约定子女可以就本条第一款中的相关财产直接主张权利,一方不履行离婚协议约定的义务,子女请求参照适用 HYPERLINK "https://law.wkinfo.com.cn/document/show?collection=legislation&aid=MTAxMDAxMzcyODc%3D&language=中文" 民 法典 HYPERLINK "https://law.wkinfo.com.cn/document/show?collection=legislation&aid=MTAxMDAxMzcyODc%3D&language=中文" \\l "No1664_B3P1Z4T522" 第五百二十二条第二款规定'
期望输出:
'双方在离婚协议中明确约定子女可以就本条第一款中的相关财产直接主张权利,一方不履行离婚协议约定的义务,子女请求参照适用民法典第五百二十二条第二款规定'
"""
if not text:
return text
# 复杂的HYPERLINK模式匹配
# 模式1: HYPERLINK "url" 显示文本 HYPERLINK "url" \l "anchor" 更多文本
pattern1 = r'HYPERLINK\s+"[^"]*"\s*([^H]*?)\s*HYPERLINK\s+"[^"]*"\s*\\l\s+"[^"]*"\s*([^H]*?)(?=\s*HYPERLINK|$)'
def replace_hyperlink_pair(match):
text1 = match.group(1).strip()
text2 = match.group(2).strip()
# 合并文本,去掉多余空格
combined = (text1 + ' ' + text2).strip()
return re.sub(r'\s+', ' ', combined)
text = re.sub(pattern1, replace_hyperlink_pair, text, flags=re.IGNORECASE)
# 模式2: 单个HYPERLINK "url" 显示文本
pattern2 = r'HYPERLINK\s+"[^"]*"\s*([^H\n]*?)(?=\s*HYPERLINK|\s*$|\n)'
text = re.sub(pattern2, r'\1', text, flags=re.IGNORECASE)
# 模式3: 清理剩余的HYPERLINK标记
pattern3 = r'HYPERLINK\s+"[^"]*"(?:\s*\\l\s+"[^"]*")?'
text = re.sub(pattern3, '', text, flags=re.IGNORECASE)
# 清理多余空格
text = re.sub(r'\s+', ' ', text)
return text.strip()
def read_document(self, file_path):
"""
读取文档内容的主方法
Args:
file_path (str): 文件路径
Returns:
str: 文档内容,如果失败返回错误信息
"""
file_path = str(file_path)
# 检查文件是否存在
if not os.path.exists(file_path):
return f"❌ 错误:文件不存在 - {file_path}"
# 检查文件大小
file_size = os.path.getsize(file_path)
if file_size == 0:
return "❌ 错误:文件为空"
# 获取文件扩展名
_, ext = os.path.splitext(file_path.lower())
if ext not in self.supported_formats:
return f"❌ 错误:不支持的文件格式 {ext}"
print(f"📄 正在读取文件: {os.path.basename(file_path)}")
print(f"📊 文件大小: {file_size:,} 字节")
print(f"🔧 文件格式: {ext}")
# 尝试不同的读取方法
methods = [
self._read_with_textutil, # macOS 系统工具(推荐)
self._read_with_python_docx, # python-docx(仅DOCX)
self._read_with_antiword, # antiword(仅DOC)
self._read_as_binary, # 二进制读取(最后尝试)
]
for i, method in enumerate(methods, 1):
try:
print(f"🔄 尝试方法 {i}: {method.__name__}")
content = method(file_path, ext)
if content and content.strip():
# 清理文本格式
cleaned_content = self.clean_text(content)
print(f"✅ 成功读取,清理后内容长度: {len(cleaned_content)} 字符")
return cleaned_content
else:
print(f"⚠️ 方法 {i} 返回空内容")
except Exception as e:
print(f"❌ 方法 {i} 失败: {str(e)}")
continue
return "❌ 所有读取方法都失败了"
def _read_with_textutil(self, file_path, ext):
"""使用 macOS 系统自带的 textutil 工具"""
try:
# textutil 是 macOS 自带的文档转换工具
cmd = ['textutil', '-convert', 'txt', '-stdout', file_path]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8',
timeout=30 # 30秒超时
)
if result.returncode == 0:
content = result.stdout.strip()
if content:
return content
else:
print(f"textutil 错误: {result.stderr}")
except subprocess.TimeoutExpired:
print("textutil 执行超时")
except FileNotFoundError:
print("textutil 命令不存在(非 macOS 系统?)")
except Exception as e:
print(f"textutil 异常: {e}")
return None
def _read_with_python_docx(self, file_path, ext):
"""使用 python-docx 读取 DOCX 文件"""
if ext != '.docx':
return None
try:
from docx import Document
doc = Document(file_path)
paragraphs = []
for paragraph in doc.paragraphs:
text = paragraph.text.strip()
if text:
paragraphs.append(text)
# 也尝试读取表格内容
for table in doc.tables:
for row in table.rows:
row_text = []
for cell in row.cells:
cell_text = cell.text.strip()
if cell_text:
row_text.append(cell_text)
if row_text:
paragraphs.append(' | '.join(row_text))
return '\n'.join(paragraphs) if paragraphs else None
except ImportError:
print("python-docx 未安装,跳过此方法")
except Exception as e:
print(f"python-docx 读取失败: {e}")
return None
def _read_with_antiword(self, file_path, ext):
"""使用 antiword 读取 DOC 文件"""
if ext != '.doc':
return None
try:
cmd = ['antiword', file_path]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8',
timeout=30
)
if result.returncode == 0:
content = result.stdout.strip()
if content:
return content
else:
print(f"antiword 错误: {result.stderr}")
except FileNotFoundError:
print("antiword 未安装,可以通过 'brew install antiword' 安装")
except subprocess.TimeoutExpired:
print("antiword 执行超时")
except Exception as e:
print(f"antiword 异常: {e}")
return None
def _read_as_binary(self, file_path, ext):
"""作为二进制文件读取并尝试提取文本"""
try:
with open(file_path, 'rb') as f:
content = f.read()
# 尝试解码为文本
possible_encodings = ['utf-8', 'utf-16', 'latin-1', 'cp1252']
for encoding in possible_encodings:
try:
text = content.decode(encoding, errors='ignore')
# 简单的文本清理
lines = []
for line in text.split('\n'):
# 过滤掉包含太多控制字符的行
clean_line = ''.join(c for c in line if c.isprintable() or c.isspace())
clean_line = clean_line.strip()
if len(clean_line) > 5: # 只保留有意义的行
lines.append(clean_line)
if lines:
result = '\n'.join(lines)
if len(result.strip()) > 50: # 确保有足够内容
return result
except Exception:
continue
except Exception as e:
print(f"二进制读取失败: {e}")
return None
def install_dependencies():
"""安装推荐的依赖"""
print("🔧 正在检查和安装依赖...")
try:
import docx
print("✅ python-docx 已安装")
except ImportError:
print("📦 安装 python-docx...")
subprocess.run([sys.executable, '-m', 'pip', 'install', 'python-docx'])
# 检查 antiword(macOS)
try:
subprocess.run(['antiword', '--version'], capture_output=True, check=True)
print("✅ antiword 已安装")
except (subprocess.CalledProcessError, FileNotFoundError):
print("⚠️ antiword 未安装,建议运行: brew install antiword")
def test_hyperlink_cleaning():
"""测试HYPERLINK清理功能"""
reader = DocReader()
# 测试用例
test_text = '''双方在离婚协议中明确约定子女可以就本条第一款中的相关财产直接主张权利,一方不履行离婚协议约定的义务,子女请求参照适用 HYPERLINK "https://law.wkinfo.com.cn/document/show?collection=legislation&aid=MTAxMDAxMzcyODc%3D&language=中文" 民 法典 HYPERLINK "https://law.wkinfo.com.cn/document/show?collection=legislation&aid=MTAxMDAxMzcyODc%3D&language=中文" \\l "No1664_B3P1Z4T522" 第五百二十二条第二款规定,由该方承担继续履行或者因无法履行而赔偿损失等民事责任的,人民法院依法予以支持。'''
print("🧪 测试HYPERLINK清理功能:")
print("原文:")
print(test_text)
print("\n清理后:")
cleaned = reader.clean_text(test_text)
print(cleaned)
print("=" * 50)
def main():
"""主函数 - 使用示例"""
# 如果参数是test,运行测试
if len(sys.argv) > 1 and sys.argv[1] == 'test':
test_hyperlink_cleaning()
return
# 创建读取器实例
reader = DocReader()
# 获取文件路径
if len(sys.argv) > 1:
file_path = sys.argv[1]
else:
# 交互式输入
file_path = input("请输入 DOC/DOCX 文件路径: ").strip().strip('"\'')
if not file_path:
print("❌ 请提供文件路径")
return
# 读取文档
print("=" * 50)
content = reader.read_document(file_path)
print("=" * 50)
if content.startswith("❌"):
print(content)
else:
print("📖 文档内容:")
print("-" * 30)
print(content)
print("-" * 30)
# 可选:保存到文本文件
output_path = os.path.splitext(file_path)[0] + '_extracted.txt'
try:
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"💾 内容已保存到: {output_path}")
except Exception as e:
print(f"⚠️ 保存失败: {e}")
if __name__ == "__main__":
main()
- 读取管线 read_document
- 文件校验:存在性 / 非空检查,扩展名在 [‘.doc’, ‘.docx’, ‘.rtf’] 内。
- 依次尝试 4 种方法(打印过程日志):
• _read_with_textutil:macOS 自带转换器,textutil -convert txt -stdout,30s 超时。
• _read_with_python_docx:仅 docx,遍历段落、表格单元格,拼成文本。
• _read_with_antiword:仅 doc,依赖外部 antiword(提供安装提示)。
• _read_as_binary:原始字节 → 轮流尝试 [‘utf-8’,‘utf-16’,‘latin-1’,‘cp1252’] 解码 → 过滤不可打印字符、短行,保障结果长度>50。 - 任一方法有内容就进入清洗:clean_text();成功即返回,否则继续下一个方法。都失败则返回错误提示。
设计优点:“优雅降级+平台内置工具优先”,在 macOS 上几乎不用装依赖就能跑;同时对老 .doc 与表格内容都有考虑。
-
文本清洗 clean_text
• 入口首先调用 clean_hyperlinks() 去掉 Word 特有的 HYPERLINK 字段。
• 之后做通用清理:
• 合并多空格、压缩空行;
• 去掉页码模式:第 X 页 … 共 Y 页;
• 去掉字段代码花括号 {…};
• 中文粘连:([一-龯])\s+([一-龯]) -> \1\2,把被空格拆开的中文字符拼回;
• 去掉每行首尾空白、丢弃纯空行。 -
HYPERLINK 清理 clean_hyperlinks
• 目标:把类似
HYPERLINK “url” 民 法典 HYPERLINK “url2” \l “锚点” 第五百二十二条…
变成无链接、仅正文的:民法典第五百二十二条…
• 使用了三段正则,处理成对链接、单个链接和残留标记,并在末尾做一次空格合并。 -
依赖管理 install_dependencies
• 检查并尝试安装 python-docx(通过 pip 调用)。
• 检查 antiword 是否可用,不可用时提示 brew install antiword。 -
命令行入口 main
• python script.py test:运行内置 HYPERLINK 清理用例(有前后对比输出)。
• python script.py /path/to/file.docx:读取并打印结果,同时保存 *_extracted.txt。
2952

被折叠的 条评论
为什么被折叠?



