doc文件转docx文件

doc文件转docx文件

Windows系统

众所周知,python的word文件解析包python-docx是无法直接解析doc文件的,但是在windows系统下有一个包pypiwin32,利用这个包可以将doc文件转换为docx文件,但是使用起来有两个局限:

  1. Linux系统中没有这个包;
  2. 这个包必须通过显示的文件保存操作来进行转换(没有办法仅通过文件二进制流操作)。

这两个包的安装:

1
2
pip install python-docx
pip install pypiwin32

所以可以使用这个包结合flask将转换程序作为一个服务部署在windows服务器上,后续可以方便的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# coding=utf-8
import json
import logging
import os
import pythoncom
import random
from flask import Flask, request
from win32com import client as wc

app = Flask(__name__)


def doc2docx_win(doc_content):
"""
windows环境下可以调用此方法将doc文件转换为docx文件
:return: docx_content
"""
random_name = random.random() # 使用一个随机的文件名称,避免多线程同时操作一个文件
# 路径自行设定
doc_path = rf'doc2docx/temp_{random_name}.doc'
docx_path = rf'doc2docx/temp_{random_name}.docx'
# if os.path.exists(doc_path): # 如果在服务重启之前,意外终止导致temp文件未能删除,使用os.remove也是删不掉的
# os.remove(doc_path)
# if os.path.exists(docx_path):
# os.remove(docx_path)

pythoncom.CoInitialize() # 多线程/多进程使用win32需要先执行这一步
w = wc.Dispatch('Word.Application')

# 先把二进制保存为文件
with open(doc_path, 'wb') as fw:
fw.write(doc_content)
try:
doc = w.Documents.Open(doc_path) # 有些传进来的文件可能就是损坏的或者下载的不完整的
except Exception:
# 如果文件有问题,要把保存的这个文件删除,否则影响后续的操作
os.remove(doc_path)
return ''
doc.SaveAs(docx_path, 16) # 保存为docx文件

doc.Close() # 只有先关闭doc/docx文件才能删除
w.Quit() # 关闭为win32对象(有时候把word关了可能后续处理会出错,但是作为服务这个一定要关闭,代价是会变慢,但是不关闭服务调用几次之后就会异常)
# pythoncom.CoUninitialize() # 释放资源

with open(docx_path, 'rb') as fr:
docx_content = fr.read()
os.remove(doc_path)
os.remove(docx_path)
return docx_content


@app.route('/doc2docx', methods=['POST'])
def reg():
binary_content = request.get_data()
for _ in range(3):
try:
docx_content = doc2docx_win(binary_content)
except RuntimeError as _e:
logging.info(_e)
except Exception as _e:
logging.exception(_e)
else:
return docx_content
return ''


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

将上面的代码保存为py文件,之后修改后缀名为pyw,直接双击点击即可后台启动该程序。

Linux系统

因为windows作为服务器不如Linux稳定,而且很多企业大多都是Linux服务器,所以将doc转docx的服务部署在Linux服务器上也是一个常见的需求,经过搜索发现Linux系统中有个”libreoffice”软件包,可以支持文件格式的转换,其中就支持将doc文件转换为docx文件。

安装:

1
2
3
sudo apt-get install libreoffice

sudo yum install libreoffice

转换命令:

1
libreoffice --headless --convert-to docx --outdir /path/to/output/directory /path/to/input/document.doc

这里的参数解释如下:

  • --headless:表示LibreOffice在没有图形界面的情况下运行。
  • --convert-to docx:指定转换的格式为.docx
  • :writer_pdf_Export:这是LibreOffice的内部选项,用于指定输出格式。
  • --outdir /path/to/output/directory:指定输出文件的目录。
  • /path/to/input/document.doc:指定要转换的.doc文件的路径。

请确保替换/path/to/output/directory/path/to/input/document.doc为你实际的文件路径。

所以基于此,将命令的执行通过python脚本来实现,并作为服务接收待转换的doc文件二进制流,将转换得到的docx文件的二进制流返回,在python代码中实现一系列的文件操作、转换命令、命令执行出错的异常捕获以及对异常进程的主动kill(避免积累的进程太多造成服务器宕机)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import os
import random
import subprocess
import sys
import time

from flask import Flask, request
from loguru import logger as logging


logging.add("doc2docx.logs",
rotation="30 MB",
retention=1,
encoding='utf-8'
)

app = Flask(__name__)
path = '/home/liutengfei/doc2docx/temp/'


def save_binary_stream_to_file(binary_stream, file_path):
with open(file_path, 'wb') as file:
file.write(binary_stream)


def open_file_read_binary(file_path):
if not os.path.exists(file_path):
logging.info(f'怪事,{file_path}不存在')
return b''
with open(file_path, 'rb') as file:
binary_content = file.read()
os.remove(file_path)
return binary_content


def find_process_by_name(process_name):
# 构建 ps 和 grep 命令
cmd_ps = ['ps', 'aux']
cmd_ggrep = ['grep', process_name]

# 使用 subprocess.Popen 来执行管道命令
# python3.7以上版本使用text=True,3.6使用universal_newlines=True
if sys.version_info.minor >= 7:
with subprocess.Popen(cmd_ps, stdout=subprocess.PIPE, text=True) as proc_ps:
with subprocess.Popen(cmd_ggrep, stdin=proc_ps.stdout, stdout=subprocess.PIPE, text=True) as proc_ggrep:
proc_ps.stdout.close() # 允许 ps 命令继续执行
stdout_ggrep, stderr_ggrep = proc_ggrep.communicate()

# 处理 grep 命令的输出
lines = stdout_ggrep.splitlines()
pids = []
for line in lines:
if 'libreoffice' not in line:
continue
# 提取 PID,通常是第二列
pid = line.split()[1]
pids.append(pid)
return pids
else:
with subprocess.Popen(cmd_ps, stdout=subprocess.PIPE, universal_newlines=True) as proc_ps:
with subprocess.Popen(cmd_ggrep, stdin=proc_ps.stdout, stdout=subprocess.PIPE, universal_newlines=True) as proc_ggrep:
proc_ps.stdout.close() # 允许 ps 命令继续执行
stdout_ggrep, stderr_ggrep = proc_ggrep.communicate()

# 处理 grep 命令的输出
lines = stdout_ggrep.splitlines()
pids = []
for line in lines:
if 'libreoffice' not in line:
continue
# 提取 PID,通常是第二列
pid = line.split()[1]
pids.append(pid)
return pids


def kill_by_pid(pids):
for pid in pids:
try:
subprocess.run(['kill', str(pid)])
except Exception:
logging.info("删除进程失败,可能是进程不存在,忽略")


def convert_doc_to_docx(doc_binary):
random_name = random.random() # 使用一个随机的文件名称,避免多线程同时操作一个文件
doc_path = rf'{path}temp_{random_name}.doc'
save_binary_stream_to_file(doc_binary, doc_path)

# 构建 LibreOffice 命令
cmd = ['libreoffice', '--headless', '--convert-to', 'docx', '--outdir', path, doc_path]

try:
start = time.time()
# 调用 LibreOffice 命令行工具(即使针对有问题doc文件,这里也基本不报错,但是得到docx是乱码,要业务端自己判断)
subprocess.run(cmd, timeout=30, check=True)
end = time.time()
logging.info(f'转换耗时: {end-start}s')
except subprocess.TimeoutExpired as e:
logging.info(f'命令执行超时: {e}')
pids = find_process_by_name(f"{random_name}.doc")
kill_by_pid(pids)
return b''
except subprocess.CalledProcessError as e:
logging.info(f'命令执行失败: {e}')
pids = find_process_by_name(f"{random_name}.doc")
kill_by_pid(pids)
return b''
except Exception as e:
logging.info(f'发生未知错误: {e}')
pids = find_process_by_name(f"{random_name}.doc")
kill_by_pid(pids)
return b''
os.remove(doc_path)

return open_file_read_binary(path+f"temp_{random_name}.docx")


@app.route('/doc2docx', methods=['POST'])
def convert():
binary_content = request.get_data()
logging.info('收到请求...')
start = time.time()
try:
docx_content = convert_doc_to_docx(binary_content)
except RuntimeError as _e:
logging.info(_e)
except Exception as _e:
logging.exception(_e)
else:
end = time.time()
logging.info(f'转换成功, 返回, 总耗时{end-start}s')
return docx_content
return ''


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5006)