v1.0.178: генератор тестовых файлов (схема Опуса). 61 файл: 10 контрактов × (договор+спека v1+v2+допники) + ошибки + ZIP + дубликаты. Комментарий в каждом файле.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
corrupt.py — Порча docx/pdf, конвертация в .doc.
|
||||
|
||||
Схема Опуса: 17 типов ошибочных кейсов.
|
||||
"""
|
||||
|
||||
import zipfile, io, os, shutil, tempfile, subprocess
|
||||
|
||||
def corrupt_xml(docx_path, out_path):
|
||||
"""
|
||||
Битый XML внутри docx: обрезать тег в word/document.xml.
|
||||
Парсер должен вернуть ✗, pipeline не падает.
|
||||
"""
|
||||
with zipfile.ZipFile(docx_path, 'r') as zin:
|
||||
data = {name: zin.read(name) for name in zin.namelist()}
|
||||
|
||||
# Найти document.xml и обрезать последние 200 байт
|
||||
doc_key = 'word/document.xml'
|
||||
if doc_key in data:
|
||||
xml = data[doc_key]
|
||||
# Обрезать закрывающие теги
|
||||
cut = max(len(xml) - 500, len(xml) // 2)
|
||||
data[doc_key] = xml[:cut]
|
||||
|
||||
with zipfile.ZipFile(out_path, 'w', zipfile.ZIP_DEFLATED) as zout:
|
||||
for name, content in data.items():
|
||||
zout.writestr(name, content)
|
||||
|
||||
def make_empty_docx(out_path):
|
||||
"""Пустой docx — без текста, без таблиц."""
|
||||
from docx import Document
|
||||
doc = Document()
|
||||
doc.save(out_path)
|
||||
|
||||
def make_empty_file(out_path):
|
||||
"""Файл нулевого размера."""
|
||||
with open(out_path, 'wb') as f:
|
||||
pass
|
||||
|
||||
def make_text_only_docx(out_path, title="Соглашение об услугах"):
|
||||
"""Docx только с текстом, без таблиц. Крайний случай для парсера."""
|
||||
from docx import Document
|
||||
doc = Document()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run(title).bold = True
|
||||
doc.add_paragraph("Настоящий документ является соглашением об оказании услуг.")
|
||||
doc.add_paragraph("Стороны: Заказчик и Исполнитель.")
|
||||
doc.add_paragraph("Стоимость услуг: 100 000 рублей.")
|
||||
doc.add_paragraph("Дата: 01.01.2026")
|
||||
doc.save(out_path)
|
||||
|
||||
def make_table_only_docx(out_path):
|
||||
"""Docx только с таблицей, без параграфов. Крайний случай для парсера."""
|
||||
from docx import Document
|
||||
doc = Document()
|
||||
table = doc.add_table(rows=3, cols=6)
|
||||
table.style = 'Table Grid'
|
||||
headers = ["№", "Наименование", "Цена", "Объем", "Сумма", "Дата"]
|
||||
for i, h in enumerate(headers):
|
||||
table.rows[0].cells[i].text = h
|
||||
table.rows[1].cells[0].text = "1"
|
||||
table.rows[1].cells[1].text = "Тестовая услуга"
|
||||
table.rows[1].cells[2].text = "1000,00"
|
||||
table.rows[1].cells[3].text = "2"
|
||||
table.rows[1].cells[4].text = "2000,00"
|
||||
table.rows[1].cells[5].text = "01.06.2026"
|
||||
doc.save(out_path)
|
||||
|
||||
def make_no_number_docx(out_path, template_builder, ctx):
|
||||
"""
|
||||
Документ БЕЗ номера договора в тексте.
|
||||
classify должен вернуть ошибку или 'other'.
|
||||
"""
|
||||
doc = template_builder(ctx)
|
||||
# Удаляем параграф с номером (второй параграф после заголовка)
|
||||
# Простой подход: заменяем текст номера на пробел
|
||||
for p in doc.paragraphs:
|
||||
if ctx["number"] in p.text:
|
||||
p.text = p.text.replace(f"№ {ctx['number']}", "№ ")
|
||||
break
|
||||
doc.save(out_path)
|
||||
|
||||
def make_nonstandard_title_docx(out_path):
|
||||
"""
|
||||
Документ с нестандартным заголовком — classify должен классифицировать
|
||||
по содержимому, а не по заголовку.
|
||||
"""
|
||||
from docx import Document
|
||||
doc = Document()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run("Соглашение о предоставлении технологических услуг").bold = True
|
||||
doc.add_paragraph("№ 99999")
|
||||
doc.add_paragraph('ООО "Нестандарт" и ООО "НУБЕС" заключили настоящее соглашение.')
|
||||
doc.add_paragraph("Предмет: оказание услуг ЦОД.")
|
||||
# Таблица услуг
|
||||
table = doc.add_table(rows=2, cols=6)
|
||||
table.style = 'Table Grid'
|
||||
for i, h in enumerate(["№", "Наименование", "Цена", "Объем", "Сумма", "Дата"]):
|
||||
table.rows[0].cells[i].text = h
|
||||
table.rows[1].cells[0].text = "1"
|
||||
table.rows[1].cells[1].text = "Аренда стойко-места"
|
||||
table.rows[1].cells[2].text = "200 000,00"
|
||||
table.rows[1].cells[3].text = "2"
|
||||
table.rows[1].cells[4].text = "400 000,00"
|
||||
table.rows[1].cells[5].text = "01.06.2026"
|
||||
doc.save(out_path)
|
||||
|
||||
def make_giant_docx(out_path, rows=500):
|
||||
"""Документ с гигантской таблицей — тест производительности."""
|
||||
from docx import Document
|
||||
doc = Document()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run("Договор № GIANT").bold = True
|
||||
doc.add_paragraph("Спецификация с большим количеством позиций.")
|
||||
table = doc.add_table(rows=1, cols=6)
|
||||
table.style = 'Table Grid'
|
||||
for i, h in enumerate(["№", "Наименование", "Цена", "Объем", "Сумма", "Дата"]):
|
||||
table.rows[0].cells[i].text = h
|
||||
for r in range(rows):
|
||||
row = table.add_row()
|
||||
row.cells[0].text = str(r + 1)
|
||||
row.cells[1].text = f"Услуга {r+1}"
|
||||
row.cells[2].text = f"{1000 + r * 10},00"
|
||||
row.cells[3].text = "1"
|
||||
row.cells[4].text = f"{1000 + r * 10},00"
|
||||
row.cells[5].text = "01.06.2026"
|
||||
doc.save(out_path)
|
||||
|
||||
def make_trash_docx(out_path, label="Счёт-фактура"):
|
||||
"""Мусорный документ — не договор. Негативная классификация."""
|
||||
from docx import Document
|
||||
doc = Document()
|
||||
p = doc.add_paragraph()
|
||||
p.add_run(label).bold = True
|
||||
doc.add_paragraph("№ СФ-2026-001 от 15.06.2026")
|
||||
doc.add_paragraph("Поставщик: ООО Рога и Копыта")
|
||||
doc.add_paragraph("Покупатель: ЗАО XXX001")
|
||||
doc.add_paragraph("Сумма: 150 000,00 руб.")
|
||||
doc.save(out_path)
|
||||
|
||||
def convert_to_doc(docx_path, out_path):
|
||||
"""Конвертировать docx → doc через LibreOffice."""
|
||||
out_dir = os.path.dirname(out_path) or "."
|
||||
out_name = os.path.basename(out_path)
|
||||
tmp = tempfile.mkdtemp()
|
||||
shutil.copy(docx_path, os.path.join(tmp, "temp.docx"))
|
||||
try:
|
||||
subprocess.run(
|
||||
["libreoffice", "--headless", "--convert-to", "doc", "--outdir", tmp,
|
||||
os.path.join(tmp, "temp.docx")],
|
||||
timeout=30, capture_output=True
|
||||
)
|
||||
src = os.path.join(tmp, "temp.doc")
|
||||
if os.path.exists(src):
|
||||
shutil.move(src, out_path)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return False
|
||||
|
||||
def make_broken_pdf(docx_path, out_path):
|
||||
"""Сгенерировать PDF из docx и обрезать байты — битый PDF."""
|
||||
# Сначала генерируем нормальный PDF через LibreOffice
|
||||
out_dir = os.path.dirname(out_path) or "."
|
||||
tmp = tempfile.mkdtemp()
|
||||
shutil.copy(docx_path, os.path.join(tmp, "temp.docx"))
|
||||
try:
|
||||
subprocess.run(
|
||||
["libreoffice", "--headless", "--convert-to", "pdf", "--outdir", tmp,
|
||||
os.path.join(tmp, "temp.docx")],
|
||||
timeout=30, capture_output=True
|
||||
)
|
||||
src = os.path.join(tmp, "temp.pdf")
|
||||
if os.path.exists(src):
|
||||
with open(src, 'rb') as f:
|
||||
data = f.read()
|
||||
# Обрезать последние 30% байт
|
||||
cut = int(len(data) * 0.7)
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(data[:cut])
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return False
|
||||
Reference in New Issue
Block a user