Загрузить файлы в «/»

This commit is contained in:
m.milnikov 2026-04-11 09:46:04 +00:00
parent 687df175aa
commit b0111c451e
5 changed files with 1347 additions and 0 deletions

View File

@ -0,0 +1,88 @@
{
"meta": {
"source": "Таблица основных типов ISO-контейнеров (внешние/внутренние размеры, дверной проём, масса, объём)",
"usage": "Сверка п.21 FCL (проходимость груза в дверной проём 20DV, 40DV, 40HC) и оценка payload/объёма."
},
"types": {
"20DV": {
"label": "20' Standard (Dry Cube) — соответствует 20DV в критериях",
"external_mm": { "length": 6058, "width": 2438, "height": 2591 },
"internal_mm": { "length": 5905, "width": 2350, "height": 2381 },
"door_opening_mm": { "width": 2336, "height": 2291 },
"max_gross_kg": 30480,
"tare_kg": 2370,
"payload_kg": 28110,
"volume_m3": 33.2
},
"20HC": {
"label": "20' High Cube",
"external_mm": { "length": 6058, "width": 2438, "height": 2896 },
"internal_mm": { "length": 5905, "width": 2350, "height": 2693 },
"door_opening_mm": { "width": 2340, "height": 2597 },
"max_gross_kg": 30480,
"tare_kg": 2340,
"payload_kg": 28140,
"volume_m3": 37.5
},
"20HC_PW": {
"label": "20' High Cube Pallet Wide",
"external_mm": { "length": 6058, "width": 2484, "height": 2896 },
"internal_mm": { "length": 5898, "width": 2426, "height": 2698 },
"door_opening_mm": { "width": 2374, "height": 2585 },
"max_gross_kg": 30480,
"tare_kg": 2580,
"payload_kg": 27900,
"volume_m3": 38.6
},
"40DV": {
"label": "40' Standard (Dry Van) — соответствует 40DV в критериях",
"external_mm": { "length": 12192, "width": 2438, "height": 2591 },
"internal_mm": { "length": 12039, "width": 2350, "height": 2372 },
"door_opening_mm": { "width": 2336, "height": 2291 },
"max_gross_kg": 30480,
"tare_kg": 4000,
"payload_kg": 26480,
"volume_m3": 67.8
},
"40HC": {
"label": "40' High Cube — соответствует 40HC в критериях",
"external_mm": { "length": 12192, "width": 2438, "height": 2896 },
"internal_mm": { "length": 12039, "width": 2350, "height": 2693 },
"door_opening_mm": { "width": 2340, "height": 2597 },
"max_gross_kg": 32500,
"tare_kg": 4200,
"payload_kg": 28300,
"volume_m3": 76.5
},
"40HC_PW": {
"label": "40' High Cube Pallet Wide",
"external_mm": { "length": 12192, "width": 2500, "height": 2896 },
"internal_mm": { "length": 12039, "width": 2432, "height": 2693 },
"door_opening_mm": { "width": 2432, "height": 2597 },
"max_gross_kg": 35000,
"tare_kg": 4400,
"payload_kg": 30600,
"volume_m3": 79.3
},
"45HC_PW_A": {
"label": "45' High Cube Pallet Wide (Type A)",
"external_mm": { "length": 13716, "width": 2500, "height": 2750 },
"internal_mm": { "length": 13513, "width": 2444, "height": 2549 },
"door_opening_mm": { "width": 2416, "height": 2439 },
"max_gross_kg": 34000,
"tare_kg": 4180,
"payload_kg": 29820,
"volume_m3": 85.1
},
"45HC_PW_B": {
"label": "45' High Cube Pallet Wide (Type B)",
"external_mm": { "length": 13716, "width": 2500, "height": 2896 },
"internal_mm": { "length": 13513, "width": 2444, "height": 2670 },
"door_opening_mm": { "width": 2416, "height": 2580 },
"max_gross_kg": 34000,
"tare_kg": 5080,
"payload_kg": 28920,
"volume_m3": 89.5
}
}
}

178
container_reference.py Normal file
View File

@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
"""Справочник ISO-контейнеров: промпт RAG и сверка п.21 FCL (дверной проём vs габариты груза)."""
from __future__ import annotations
import json
import os
import re
from typing import Any, Dict, List, Optional, Tuple
_DIR = os.path.dirname(os.path.abspath(__file__))
_JSON_PATH = os.path.join(_DIR, "container_iso_reference.json")
_iso_cache: Optional[Dict[str, Any]] = None
def load_iso_reference() -> Dict[str, Any]:
global _iso_cache
if _iso_cache is not None:
return _iso_cache
try:
with open(_JSON_PATH, "r", encoding="utf-8") as f:
_iso_cache = json.load(f)
except Exception:
_iso_cache = {"types": {}}
return _iso_cache
def iso_reference_prompt_block() -> str:
"""Компактный блок для system prompt LLM (сверка п.21 и выбора контейнера)."""
data = load_iso_reference()
types = data.get("types") or {}
if not types:
return ""
lines: List[str] = [
"СПРАВОЧНИК ISO-КОНТЕЙНЕРОВ (мм, кг, м³) — для сверки с п.21 FCL (дверной проём) и оценки объёма/полезной нагрузки. "
"Не выдумывай числа; при расчётах используй только эти значения.",
"Ключи 20DV / 40DV / 40HC соответствуют формулировкам в критериях (проём 20DV, 40DV, 40HC).",
]
for key in ("20DV", "20HC", "20HC_PW", "40DV", "40HC", "40HC_PW", "45HC_PW_A", "45HC_PW_B"):
t = types.get(key)
if not isinstance(t, dict):
continue
door = t.get("door_opening_mm") or {}
inn = t.get("internal_mm") or {}
dw = door.get("width")
dh = door.get("height")
il = inn.get("length")
iw = inn.get("width")
ih = inn.get("height")
vol = t.get("volume_m3")
pay = t.get("payload_kg")
lines.append(
f"- {key} ({t.get('label', '')}): дверь {dw}×{dh} мм; "
f"внутри {il}×{iw}×{ih} мм; объём ~{vol} м³; payload ~{pay} кг"
)
return "\n".join(lines)
def _edges_mm_from_dim(d: Dict[str, Any]) -> Optional[Tuple[float, float, float]]:
"""Длина/ширина/высота груза в мм (из см в shipment)."""
if not isinstance(d, dict):
return None
def _num(v: Any) -> Optional[float]:
if v is None:
return None
if isinstance(v, (int, float)):
return float(v)
s = str(v).strip().replace(" ", "").replace(",", ".")
try:
return float(s)
except ValueError:
return None
l = _num(d.get("length_cm"))
w = _num(d.get("width_cm"))
h = _num(d.get("height_cm"))
if l is None or w is None or h is None:
return None
return l * 10.0, w * 10.0, h * 10.0
def _box_fits_door(
edges_mm: Tuple[float, float, float],
door_w_mm: float,
door_h_mm: float,
) -> bool:
"""Два наименьших ребра груза должны укладываться в ширину/высоту проёма (сортировка)."""
s0, s1, s2 = sorted(edges_mm)
dw, dh = sorted((door_w_mm, door_h_mm))
return s0 <= dw + 0.5 and s1 <= dh + 0.5
def _codes_from_container_type(ct: str) -> List[str]:
"""Из строки вида «2×40HC, 1×20DC» извлекает коды из справочника."""
if not isinstance(ct, str) or not ct.strip():
return ["20DV", "40DV", "40HC"]
t = ct.upper()
found: List[str] = []
def _add(size: str, kind: Optional[str]) -> None:
k = (kind or "DV").upper()
if size == "20":
code = "20" + ("HC" if k in ("HC", "HQ") else "DV")
elif size == "40":
code = "40" + ("HC" if k in ("HC", "HQ") else "DV")
elif size == "45":
code = "45HC_PW_B"
else:
return
if code not in found:
found.append(code)
for m in re.finditer(
r"(\d{1,3})\s*[x×х*]\s*(20|40|45)\s*(DC|DV|GP|HC|HQ|OT|FR|RF|RH)?",
t,
re.IGNORECASE,
):
_add(m.group(2), m.group(3))
for m in re.finditer(
r"\b(20|40|45)\s*(DC|DV|GP|HC|HQ|OT|FR|RF|RH)\b",
t,
re.IGNORECASE,
):
_add(m.group(1), m.group(2))
if not found:
return ["20DV", "40DV", "40HC"]
return found
def door_clearance_summary(shipment: Dict[str, Any]) -> str:
"""
Текст для п.21: сверка габаритов мест с дверным проёмом 20DV/40DV/40HC из справочника.
"""
data = load_iso_reference()
types = data.get("types") or {}
dims = shipment.get("dimensions")
if not isinstance(dims, list) or not dims:
return (
"Нет габаритов грузовых мест — проверка проходимости по справочнику дверных проёмов "
"(20DV 2336×2291 мм, 40DV 2336×2291 мм, 40HC 2340×2597 мм) невозможна."
)
ct_raw = (shipment.get("container_type") or "").strip()
codes = _codes_from_container_type(ct_raw)
parts: List[str] = []
for i, d in enumerate(dims, start=1):
edges = _edges_mm_from_dim(d)
if not edges:
parts.append(f"Место {i}: нет полных Д×Ш×В в см.")
continue
place_bits: List[str] = []
for code in codes:
spec = types.get(code)
if not isinstance(spec, dict):
continue
door = spec.get("door_opening_mm") or {}
dw = float(door.get("width") or 0)
dh = float(door.get("height") or 0)
if dw <= 0 or dh <= 0:
continue
ok = _box_fits_door(edges, dw, dh)
label = spec.get("label", code)
status = "проходит" if ok else "не проходит"
place_bits.append(f"{code} ({int(dw)}×{int(dh)} мм): {status}")
if place_bits:
parts.append(f"Место {i} (рёбра {int(min(edges))}×{int(sorted(edges)[1])}×{int(max(edges))} мм): " + "; ".join(place_bits))
else:
parts.append(f"Место {i}: нет данных дверного проёма в справочнике для кодов {', '.join(codes)}.")
note = (
" Оценка: два наименьших габарита сравниваются с шириной и высотой дверного проёма (ориентация коробки)."
)
prefix = (
f"Справочник дверных проёмов (п.21). Контейнеры в заявке: {ct_raw or 'не указаны — проверка по 20DV/40DV/40HC'}. "
)
return prefix + " ".join(parts) + note

View File

@ -0,0 +1,801 @@
{
"version": 1,
"source_xlsx": "Запрос для ИИ Общий(3).xlsx",
"by_shipping_type": {
"Автомобильная перевозка (LTL)": {
"sheet": "Авто LTL интерфейс",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество грузовых мест и вес, объем"
},
"right": {
"num": "7",
"label": "Габариты грузовых мест ДхШхВ"
}
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Штабелирвоание с другими отправками"
},
"right": {
"num": "11",
"label": "Штабелирвоание между собой"
}
},
{
"type": "pair",
"left": {
"num": "9",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "13",
"label": "MSDS"
},
"right": {
"num": "14",
"label": "DGM"
}
},
{
"type": "pair",
"left": {
"num": "15",
"label": "Название бренда"
},
"right": {
"num": "16",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "17",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "18",
"label": "Требуется ли замена документов"
}
}
]
},
"Автомобильная перевозка (FTL)": {
"sheet": "Авто FTL интерфейс",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество и типоразмер машин"
},
"right": {
"num": "7",
"label": "Количество грузовых мест и вес, объем"
}
},
{
"type": "left_12",
"num": "8",
"label": "Габариты грузовых мест ДхШхВ"
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "11",
"label": "MSDS"
},
"right": {
"num": "13",
"label": "DGM"
}
},
{
"type": "pair",
"left": {
"num": "14",
"label": "Название бренда"
},
"right": {
"num": "15",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "16",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "17",
"label": "Требуется ли замена документов"
}
}
]
},
"Морская перевозка (LCL)": {
"sheet": "Море, море+жд, жд LCL интерфейc",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество и типоразмер машин"
},
"right": {
"num": "7",
"label": "Количество грузовых мест и вес, объем"
}
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Штабелирвоание с другими отправками"
},
"right": {
"num": "11",
"label": "Штабелирвоание между собой"
}
},
{
"type": "pair",
"left": {
"num": "9",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "13",
"label": "MSDS"
},
"right": {
"num": "14",
"label": "DGM"
}
},
{
"type": "pair",
"left": {
"num": "15",
"label": "Название бренда"
},
"right": {
"num": "16",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "17",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "18",
"label": "Требуется ли замена документов"
}
}
]
},
"Железнодорожная перевозка (LCL)": {
"sheet": "Море, море+жд, жд LCL интерфейc",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество и типоразмер машин"
},
"right": {
"num": "7",
"label": "Количество грузовых мест и вес, объем"
}
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Штабелирвоание с другими отправками"
},
"right": {
"num": "11",
"label": "Штабелирвоание между собой"
}
},
{
"type": "pair",
"left": {
"num": "9",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "13",
"label": "MSDS"
},
"right": {
"num": "14",
"label": "DGM"
}
},
{
"type": "pair",
"left": {
"num": "15",
"label": "Название бренда"
},
"right": {
"num": "16",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "17",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "18",
"label": "Требуется ли замена документов"
}
}
]
},
"Мультимодальная перевозка море + ж/д (LCL)": {
"sheet": "Море, море+жд, жд LCL интерфейc",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество и типоразмер машин"
},
"right": {
"num": "7",
"label": "Количество грузовых мест и вес, объем"
}
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Штабелирвоание с другими отправками"
},
"right": {
"num": "11",
"label": "Штабелирвоание между собой"
}
},
{
"type": "pair",
"left": {
"num": "9",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "13",
"label": "MSDS"
},
"right": {
"num": "14",
"label": "DGM"
}
},
{
"type": "pair",
"left": {
"num": "15",
"label": "Название бренда"
},
"right": {
"num": "16",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "17",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "18",
"label": "Требуется ли замена документов"
}
}
]
},
"Морская перевозка (FCL)": {
"sheet": "Море, море+жд, жд FCL интерфейc",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество и типоразмер контейнеров"
},
"right": {
"num": "7",
"label": "Количество грузовых мест и вес, объем"
}
},
{
"type": "left_12",
"num": "8",
"label": "Габариты грузовых мест ДхШхВ"
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "11",
"label": "MSDS"
},
"right": {
"num": "13",
"label": "Technical Description of Goods in Railway Transport"
}
},
{
"type": "pair",
"left": {
"num": "14",
"label": "Название бренда"
},
"right": {
"num": "15",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "16",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "17",
"label": "Требуется ли замена документов"
}
},
{
"type": "pair",
"left": {
"num": "18",
"label": "Место таможенного оформления при экспорте из РФ"
},
"right": {
"num": "19",
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
}
},
{
"type": "left_12",
"num": "20",
"label": "Превышение габаритов дверного проема контейнеров 20DV, 40DV, 40HC"
}
]
},
"Железнодорожная перевозка (FCL)": {
"sheet": "Море, море+жд, жд FCL интерфейc",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество и типоразмер контейнеров"
},
"right": {
"num": "7",
"label": "Количество грузовых мест и вес, объем"
}
},
{
"type": "left_12",
"num": "8",
"label": "Габариты грузовых мест ДхШхВ"
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "11",
"label": "MSDS"
},
"right": {
"num": "13",
"label": "Technical Description of Goods in Railway Transport"
}
},
{
"type": "pair",
"left": {
"num": "14",
"label": "Название бренда"
},
"right": {
"num": "15",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "16",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "17",
"label": "Требуется ли замена документов"
}
},
{
"type": "pair",
"left": {
"num": "18",
"label": "Место таможенного оформления при экспорте из РФ"
},
"right": {
"num": "19",
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
}
},
{
"type": "left_12",
"num": "20",
"label": "Превышение габаритов дверного проема контейнеров 20DV, 40DV, 40HC"
}
]
},
"Мультимодальная перевозка море + ж/д (FCL)": {
"sheet": "Море, море+жд, жд FCL интерфейc",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество и типоразмер контейнеров"
},
"right": {
"num": "7",
"label": "Количество грузовых мест и вес, объем"
}
},
{
"type": "left_12",
"num": "8",
"label": "Габариты грузовых мест ДхШхВ"
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "12",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "11",
"label": "MSDS"
},
"right": {
"num": "13",
"label": "Technical Description of Goods in Railway Transport"
}
},
{
"type": "pair",
"left": {
"num": "14",
"label": "Название бренда"
},
"right": {
"num": "15",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "16",
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
},
"right": {
"num": "17",
"label": "Требуется ли замена документов"
}
},
{
"type": "pair",
"left": {
"num": "18",
"label": "Место таможенного оформления при экспорте из РФ"
},
"right": {
"num": "19",
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
}
},
{
"type": "left_12",
"num": "20",
"label": "Превышение габаритов дверного проема контейнеров 20DV, 40DV, 40HC"
}
]
},
"Авиаперевозка": {
"sheet": "Авиа Интерфейс",
"blocks": [
{
"type": "pair",
"left": {
"num": "4",
"label": "Страна, город, адрес забора"
},
"right": {
"num": "19",
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
}
},
{
"type": "pair",
"left": {
"num": "6",
"label": "Количество грузовых мест и вес, объем"
},
"right": {
"num": "7",
"label": "Габариты грузовых мест ДхШхВ"
}
},
{
"type": "pair",
"left": {
"num": "10",
"label": "Штабелирвоание с другими отправками"
},
"right": {
"num": "11",
"label": "Штабелирвоание между собой"
}
},
{
"type": "pair",
"left": {
"num": "9",
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
},
"right": {
"num": "13",
"label": "Батарейки в составе груза / отдельно"
}
},
{
"type": "pair",
"left": {
"num": "12",
"label": "MSDS"
},
"right": {
"num": "14",
"label": "DGM"
}
},
{
"type": "pair",
"left": {
"num": "15",
"label": "Название бренда"
},
"right": {
"num": "16",
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
}
},
{
"type": "pair",
"left": {
"num": "17",
"label": "Перелет через третью страну со сменой авинаклданых"
},
"right": {
"num": "18",
"label": "Наличие экспортной лицезии у отправителя"
}
},
{
"type": "pair",
"left": {
"num": "19",
"label": "Дополнительный сервис"
},
"right": {
"num": "20",
"label": "Экспедирование в аэропорту назначения"
}
},
{
"type": "pair",
"left": {
"num": "22",
"label": "Спец требования при доставке до адреса в РФ"
},
"right": {
"num": "23",
"label": "Таможенное оформление"
}
},
{
"type": "pair",
"left": {
"num": "24",
"label": "Место таможенного оформления при экспорте из РФ"
},
"right": {
"num": "25",
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
}
}
]
}
}
}

154
document_processor(1).py Normal file
View File

@ -0,0 +1,154 @@
from pypdf import PdfReader
from docx import Document
import openpyxl
from typing import Union
import io
import logging
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
class DocumentProcessor:
@staticmethod
def _cell_to_text(v) -> str:
if v is None:
return ""
s = str(v).strip()
return s if s else ""
@staticmethod
def _sheet_rows_text_openpyxl(sheet) -> list[str]:
out: list[str] = []
for row in sheet.iter_rows(values_only=True):
vals = [DocumentProcessor._cell_to_text(c) for c in row]
vals = [x for x in vals if x]
if vals:
out.append(" | ".join(vals))
return out
@staticmethod
def _sheet_cols_text_openpyxl(sheet) -> list[str]:
out: list[str] = []
max_col = sheet.max_column or 0
max_row = sheet.max_row or 0
for c in range(1, max_col + 1):
vals: list[str] = []
for r in range(1, max_row + 1):
v = DocumentProcessor._cell_to_text(sheet.cell(r, c).value)
if v:
vals.append(v)
if vals:
col_letter = openpyxl.utils.get_column_letter(c)
out.append(f"COL {col_letter} TOP_DOWN: " + " || ".join(vals))
out.append(f"COL {col_letter} BOTTOM_UP: " + " || ".join(reversed(vals)))
return out
@staticmethod
def _sheet_rows_text_xlrd(sheet) -> list[str]:
out: list[str] = []
for row_idx in range(sheet.nrows):
row = sheet.row_values(row_idx)
vals = [DocumentProcessor._cell_to_text(c) for c in row]
vals = [x for x in vals if x]
if vals:
out.append(" | ".join(vals))
return out
@staticmethod
def _sheet_cols_text_xlrd(sheet) -> list[str]:
out: list[str] = []
for c in range(sheet.ncols):
vals: list[str] = []
for r in range(sheet.nrows):
v = DocumentProcessor._cell_to_text(sheet.cell_value(r, c))
if v:
vals.append(v)
if vals:
out.append(f"COL {c+1} TOP_DOWN: " + " || ".join(vals))
out.append(f"COL {c+1} BOTTOM_UP: " + " || ".join(reversed(vals)))
return out
def normalize_email_html(html_content: str) -> str:
"""
Очищает HTML письма и нормализует таблицы
"""
soup = BeautifulSoup(html_content, "html.parser")
# удаляем скрипты и стили
for tag in soup(["script", "style"]):
tag.decompose()
# нормализуем таблицы
for table in soup.find_all("table"):
table["style"] = "border-collapse: collapse; width: 100%;"
for cell in table.find_all(["td", "th"]):
cell["style"] = "border:1px solid #ccc;padding:6px;"
return str(soup)
def extract_text(self, content: bytes, filename: str) -> str:
ext = filename.lower().split('.')[-1]
# Изображения не обрабатываем
if ext in ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp']:
return ""
if ext == 'pdf':
return self._extract_pdf(content)
elif ext in ['docx', 'doc']:
return self._extract_docx(content)
elif ext in ['xlsx', 'xls']:
return self._extract_excel(content)
elif ext == 'txt':
return content.decode('utf-8')
else:
raise ValueError(f"Unsupported format: {ext}")
def _extract_pdf(self, content: bytes) -> str:
pdf = PdfReader(io.BytesIO(content))
return "\n".join(page.extract_text() for page in pdf.pages)
def _extract_docx(self, content: bytes) -> str:
doc = Document(io.BytesIO(content))
return "\n".join(paragraph.text for paragraph in doc.paragraphs)
def _extract_excel(self, content: bytes) -> str:
try:
# сначала пробуем openpyxl (для .xlsx)
wb = openpyxl.load_workbook(io.BytesIO(content))
text = []
for sheet in wb.worksheets:
text.append(f"=== SHEET: {sheet.title} ===")
# Сохраняем совместимость: построчное чтение.
rows_text = self._sheet_rows_text_openpyxl(sheet)
if rows_text:
text.append("[ROWS]")
text.extend(rows_text)
# Новый режим: чтение по колонкам (общие данные часто внизу столбца).
cols_text = self._sheet_cols_text_openpyxl(sheet)
if cols_text:
text.append("[COLUMNS]")
text.extend(cols_text)
return "\n".join(text)
except Exception as e:
# если не получилось, возможно это .xls, пробуем xlrd
try:
import xlrd
workbook = xlrd.open_workbook(file_contents=content)
text = []
for sheet in workbook.sheets():
text.append(f"=== SHEET: {sheet.name} ===")
rows_text = self._sheet_rows_text_xlrd(sheet)
if rows_text:
text.append("[ROWS]")
text.extend(rows_text)
cols_text = self._sheet_cols_text_xlrd(sheet)
if cols_text:
text.append("[COLUMNS]")
text.extend(cols_text)
return "\n".join(text)
except ImportError:
logger.error("xlrd not installed, cannot parse .xls files")
return ""
except Exception as e2:
logger.error(f"Failed to parse Excel with xlrd: {e2}")
return ""

View File

@ -0,0 +1,126 @@
"""
Однократная/повторная генерация criteria_interface_layout.json из Excel
«Запрос для ИИ Общий(2).xlsx» (листы с «интерфейс» в названии).
Запуск (путь к xlsx можно передать аргументом):
py extract_criteria_interface_layout.py "C:\\Users\\...\\Запрос для ИИ Общий(2).xlsx"
"""
from __future__ import annotations
import json
import os
import sys
import openpyxl
SHEET_TO_SHIPPING_TYPES: dict[str, list[str]] = {
"Авто LTL интерфейс": ["Автомобильная перевозка (LTL)"],
"Авто FTL интерфейс": ["Автомобильная перевозка (FTL)"],
"Море, море+жд, жд LCL интерфейc": [
"Морская перевозка (LCL)",
"Железнодорожная перевозка (LCL)",
"Мультимодальная перевозка море + ж/д (LCL)",
],
"Море, море+жд, жд FCL интерфейc": [
"Морская перевозка (FCL)",
"Железнодорожная перевозка (FCL)",
"Мультимодальная перевозка море + ж/д (FCL)",
],
"Авиа Интерфейс": ["Авиаперевозка"],
}
def _row_cells(ws, r: int, max_c: int = 12) -> list[tuple[int, str]]:
out: list[tuple[int, str]] = []
for c in range(1, max_c + 1):
v = ws.cell(r, c).value
if v is not None and str(v).strip():
out.append((c, str(v).strip()))
return out
def parse_interface_sheet(ws) -> list[dict]:
"""Разбор листа интерфейса в последовательность визуальных блоков."""
blocks: list[dict] = []
r = 1
max_r = ws.max_row or 1
while r <= max_r:
cells = _row_cells(ws, r)
if not cells:
r += 1
continue
# Заголовок: 7 ячеек в колонках 1,2,3,4,5,7,9 и все значения — «номера пунктов»
if (
len(cells) == 7
and {c[0] for c in cells} == {1, 2, 3, 4, 5, 7, 9}
and all(str(c[1]).strip().isdigit() or c[1].strip().replace(".", "").isdigit() for c in cells)
):
nums = [c[1] for c in sorted(cells, key=lambda x: (x[0] not in (1, 2, 3, 4, 5, 7, 9), x[0]))]
# порядок как в Excel слева направо по колонкам
nums = [c[1] for c in sorted(cells, key=lambda x: x[0])]
r2 = r + 1
cells2 = _row_cells(ws, r2)
if len(cells2) == 7 and {c[0] for c in cells2} == {1, 2, 3, 4, 5, 7, 9}:
labels = [c[1] for c in sorted(cells2, key=lambda x: x[0])]
blocks.append({"type": "header", "nums": nums, "labels": labels})
r += 2
continue
# Только правая пара (6 = номер, 7 = текст) — например «20 Место таможни»
if len(cells) == 2 and cells[0][0] == 6 and cells[1][0] == 7:
blocks.append({"type": "right_67", "num": cells[0][1], "label": cells[1][1]})
r += 1
continue
# Две пары: 1-2 и 6-7
if len(cells) == 4 and {cells[i][0] for i in range(4)} == {1, 2, 6, 7}:
blocks.append(
{
"type": "pair",
"left": {"num": cells[0][1], "label": cells[1][1]},
"right": {"num": cells[2][1], "label": cells[3][1]},
}
)
r += 1
continue
# Только левая пара 1-2 (один блок на строку)
if len(cells) == 2 and cells[0][0] == 1 and cells[1][0] == 2:
blocks.append({"type": "left_12", "num": cells[0][1], "label": cells[1][1]})
r += 1
continue
r += 1
return blocks
def main() -> None:
xlsx = (
sys.argv[1]
if len(sys.argv) > 1
else os.path.join(os.path.dirname(__file__), "Запрос для ИИ Общий(3).xlsx")
)
if not os.path.isfile(xlsx):
print("Файл не найден:", xlsx)
sys.exit(1)
wb = openpyxl.load_workbook(xlsx, read_only=False, data_only=True)
out: dict = {"version": 1, "source_xlsx": os.path.basename(xlsx), "by_shipping_type": {}}
for sheet_name, type_names in SHEET_TO_SHIPPING_TYPES.items():
if sheet_name not in wb.sheetnames:
print("Пропуск: лист не найден:", sheet_name)
continue
blocks = parse_interface_sheet(wb[sheet_name])
for tn in type_names:
out["by_shipping_type"][tn] = {"sheet": sheet_name, "blocks": blocks}
out_path = os.path.join(os.path.dirname(__file__), "criteria_interface_layout.json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print("Записано:", out_path, "типов:", len(out["by_shipping_type"]))
if __name__ == "__main__":
main()