wechat-robot-skills/skills/docx/scripts/convert_document.py
2026-07-26 02:45:00 +08:00

200 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import shutil
import tempfile
from pathlib import Path
from typing import Any
from _docx_common import (
DOCUMENT_OUTPUT_SUFFIXES,
WORD_INPUT_SUFFIXES,
SkillArgumentParser,
find_program,
input_file,
output_file,
publish_file,
run_cli,
run_program,
run_soffice_convert,
)
def _plain_text(document: Any) -> str:
chunks: list[str] = []
for paragraph in document.paragraphs:
chunks.append(paragraph.text)
for table_index, table in enumerate(document.tables, start=1):
chunks.append(f"\n[表格 {table_index}]")
for row in table.rows:
chunks.append("\t".join(cell.text for cell in row.cells))
return "\n".join(chunks).rstrip() + "\n"
def _markdown(document: Any) -> str:
chunks: list[str] = []
for paragraph in document.paragraphs:
style_name = paragraph.style.name if paragraph.style else ""
if style_name.startswith("Heading "):
try:
level = int(style_name.split()[-1])
except ValueError:
level = 1
chunks.append(f"{'#' * min(max(level, 1), 6)} {paragraph.text}")
elif style_name.startswith("List Bullet"):
chunks.append(f"- {paragraph.text}")
elif style_name.startswith("List Number"):
chunks.append(f"1. {paragraph.text}")
else:
chunks.append(paragraph.text)
chunks.append("")
for table in document.tables:
rows = [[cell.text.replace("|", "\\|") for cell in row.cells] for row in table.rows]
if not rows:
continue
chunks.append("| " + " | ".join(rows[0]) + " |")
chunks.append("| " + " | ".join("---" for _ in rows[0]) + " |")
for row in rows[1:]:
chunks.append("| " + " | ".join(row) + " |")
chunks.append("")
return "\n".join(chunks).rstrip() + "\n"
def _extract_text(
source: Path,
destination: Path,
*,
markdown: bool,
track_changes: str,
timeout: int,
) -> str:
pandoc = shutil.which("pandoc")
if pandoc:
target = "gfm" if markdown else "plain"
completed = run_program(
[
pandoc,
f"--track-changes={track_changes}",
"-t",
target,
"-o",
str(destination),
str(source),
],
timeout=timeout,
)
return "pandoc"
from docx import Document
if track_changes != "accept":
raise FileNotFoundError(
"运行环境缺少 pandoc纯 Python 回退只支持 --track-changes accept"
)
document = Document(str(source))
text = _markdown(document) if markdown else _plain_text(document)
destination.write_text(text, encoding="utf-8")
return "python-docx"
def build_parser() -> argparse.ArgumentParser:
parser = SkillArgumentParser(
description="把 Word 文档转换为 DOCX、PDF、Markdown 或纯文本。"
)
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
parser.add_argument(
"--track-changes",
default="accept",
choices=["accept", "reject", "all"],
)
parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--overwrite", action="store_true")
return parser
def main() -> dict[str, Any]:
args = build_parser().parse_args()
source = input_file(args.input, WORD_INPUT_SUFFIXES)
destination = output_file(
args.output,
DOCUMENT_OUTPUT_SUFFIXES,
overwrite=args.overwrite,
)
if source == destination:
raise ValueError("输出路径不能与输入文件相同")
source_suffix = source.suffix.lower()
output_suffix = destination.suffix.lower()
engine = ""
office_output: dict[str, str] = {"stdout": "", "stderr": ""}
with tempfile.TemporaryDirectory(prefix="docx-convert-") as temp_name:
temp_dir = Path(temp_name)
working_source = source
if source_suffix in {".doc", ".dotx"}:
converted, office_output = run_soffice_convert(
source,
target_format="docx",
output_dir=temp_dir / "docx",
timeout=args.timeout,
filter_name="Office Open XML Text",
)
working_source = converted
source_suffix = ".docx"
descriptor, temp_output_name = tempfile.mkstemp(
prefix="converted-",
suffix=output_suffix,
dir=str(destination.parent),
)
os.close(descriptor)
temp_output = Path(temp_output_name)
temp_output.unlink()
try:
if output_suffix == ".docx":
if source.suffix.lower() == ".docx":
raise ValueError("输入和输出都是 .docx无需转换")
shutil.copy2(working_source, temp_output)
engine = "libreoffice"
elif output_suffix == ".pdf":
converted, office_output = run_soffice_convert(
working_source,
target_format="pdf",
output_dir=temp_dir / "pdf",
timeout=args.timeout,
)
shutil.copy2(converted, temp_output)
engine = "libreoffice"
elif output_suffix in {".txt", ".md"}:
engine = _extract_text(
working_source,
temp_output,
markdown=output_suffix == ".md",
track_changes=args.track_changes,
timeout=args.timeout,
)
else:
raise ValueError("不支持的目标格式")
publish_file(temp_output, destination, overwrite=args.overwrite)
finally:
if temp_output.exists():
temp_output.unlink()
return {
"path": str(destination),
"source": str(source),
"source_format": source.suffix.lower(),
"output_format": output_suffix,
"engine": engine,
"track_changes": args.track_changes,
"office_stdout": office_output["stdout"],
"office_stderr": office_output["stderr"],
}
if __name__ == "__main__":
raise SystemExit(run_cli(main))