doc_convert.py: conversor .doc->.docx de Fray Marcos, con el arreglo de los smartTag (#209)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""doc_convert.py — convierte un .doc viejo a .docx usando Microsoft Word (COM).
|
||||
|
||||
Necesario para Marcos (marroro@telefonica.net), que envía .doc (Word antiguo) y
|
||||
python-docx no sabe leer .doc. El contenido ya viene en EML (lo etiqueta Inma al
|
||||
revisar); aquí solo cambiamos el FORMATO de archivo.
|
||||
|
||||
Requiere Microsoft Word instalado + pywin32 (igual que marcos_review).
|
||||
API: doc_to_docx(path, force=False) -> Path | None
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
WD_FORMAT_DOCX = 16 # wdFormatDocumentDefault (.docx)
|
||||
|
||||
|
||||
def doc_to_docx(path, force: bool = False):
|
||||
"""Convierte un .doc a .docx con Word COM. Idempotente: si el .docx ya existe y
|
||||
no force, lo devuelve sin reconvertir. Devuelve la ruta .docx, o None si falla."""
|
||||
path = Path(path)
|
||||
if path.suffix.lower() != ".doc":
|
||||
return None
|
||||
out = path.with_suffix(".docx")
|
||||
if out.exists() and not force:
|
||||
return out
|
||||
try:
|
||||
import win32com.client # type: ignore
|
||||
except ImportError:
|
||||
return None
|
||||
word = win32com.client.Dispatch("Word.Application")
|
||||
word.Visible = False
|
||||
try:
|
||||
d = word.Documents.Open(str(path))
|
||||
d.SaveAs(str(out), FileFormat=WD_FORMAT_DOCX)
|
||||
d.Close(SaveChanges=False)
|
||||
finally:
|
||||
word.Quit()
|
||||
if out.exists():
|
||||
quitar_smarttags(out)
|
||||
return out if out.exists() else None
|
||||
|
||||
|
||||
def quitar_smarttags(docx_path) -> int:
|
||||
"""Desenvuelve los <w:smartTag> del .docx. Devuelve cuántos ha quitado.
|
||||
|
||||
⚠️ CRÍTICO, no es cosmético. Los .doc de Fray Marcos traen `smartTag` (el viejo
|
||||
reconocimiento de nombres de Word) envolviendo expresiones como "la Iglesia" o
|
||||
"la Cananea". **python-docx solo lee los <w:r> que cuelgan directamente del párrafo,
|
||||
así que el texto de dentro del smartTag DESAPARECE sin dar error**: se publicaron
|
||||
frases mutiladas en las cartas 736, 737 y 739 ("...no va dirigida a los apóstoles,
|
||||
sino a dureza de la respuesta..."). Detectado el 12-ago-2026.
|
||||
"""
|
||||
import re, shutil, zipfile
|
||||
from pathlib import Path
|
||||
|
||||
docx_path = Path(docx_path)
|
||||
with zipfile.ZipFile(docx_path) as z:
|
||||
piezas = {n: z.read(n) for n in z.namelist()}
|
||||
xml = piezas.get("word/document.xml", b"").decode("utf-8", "replace")
|
||||
nuevo, n = re.subn(r"</?w:smartTag(?:Pr)?\b[^>]*>|<w:attr\b[^>]*/>", "", xml)
|
||||
if not n:
|
||||
return 0
|
||||
piezas["word/document.xml"] = nuevo.encode("utf-8")
|
||||
tmp = docx_path.with_suffix(".docx.tmp")
|
||||
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for nombre, datos in piezas.items():
|
||||
z.writestr(nombre, datos)
|
||||
shutil.move(str(tmp), str(docx_path))
|
||||
return n
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) < 2:
|
||||
print("uso: python doc_convert.py path/al/archivo.doc", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
res = doc_to_docx(Path(sys.argv[1]), force="--force" in sys.argv)
|
||||
print(f"OK: {res}" if res else "FALLO (¿Word/pywin32?)")
|
||||
sys.exit(0 if res else 1)
|
||||
Reference in New Issue
Block a user