127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
"""Pure helpers for splitting and normalizing raw supplier cells."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import dataclass
|
||
|
||
|
||
_SEPARATOR_RE = re.compile(r"[//、,,;;|\\\n]+")
|
||
_LABEL_RE = re.compile(r"(?P<label>[\u4e00-\u9fffA-Za-z0-9]+)\s*[::]")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SupplierMention:
|
||
name: str
|
||
role: str | None
|
||
material_type: str | None
|
||
source_text: str
|
||
method: str
|
||
confidence: float
|
||
|
||
|
||
def compact_text(value: str) -> str:
|
||
return re.sub(r"[\s()()【】\[\]{}]+", "", value.casefold()).strip()
|
||
|
||
|
||
def normalize_name(value: str) -> str:
|
||
value = re.sub(r"\s+", "", value).strip(" ::、,,;;/\\")
|
||
for suffix in ("有限责任公司", "有限公司", "公司"):
|
||
if value.endswith(suffix) and len(value) > len(suffix) + 1:
|
||
value = value[: -len(suffix)]
|
||
break
|
||
if value.endswith("市") and len(value) > 2:
|
||
# Keep city in canonical display names; this is only a normalized key.
|
||
pass
|
||
return value
|
||
|
||
|
||
def normalized_key(value: str) -> str:
|
||
return compact_text(normalize_name(value))
|
||
|
||
|
||
def supplier_match_key(value: str) -> str:
|
||
key = normalized_key(value)
|
||
return re.sub(r"(佛山|广州|深圳|东莞|中山|珠海)市", r"\1", key)
|
||
|
||
|
||
def resolve_canonical_name(name: str, purchase_order_names: list[str]) -> tuple[str, bool]:
|
||
"""Resolve a short/raw name to one unambiguous procurement full name."""
|
||
key = supplier_match_key(name)
|
||
if not key:
|
||
return name, False
|
||
exact = [item for item in purchase_order_names if supplier_match_key(item) == key]
|
||
if len(exact) == 1:
|
||
return exact[0], True
|
||
contains = [item for item in purchase_order_names if key in supplier_match_key(item)]
|
||
if len(contains) == 1:
|
||
return contains[0], True
|
||
return name, False
|
||
|
||
|
||
def infer_location(value: str) -> tuple[str | None, str | None]:
|
||
"""Infer only explicit locations present in a supplier name."""
|
||
compact = compact_text(value)
|
||
city_map = {
|
||
"佛山": ("佛山", "广东"),
|
||
"广州": ("广州", "广东"),
|
||
"深圳": ("深圳", "广东"),
|
||
"东莞": ("东莞", "广东"),
|
||
"中山": ("中山", "广东"),
|
||
"珠海": ("珠海", "广东"),
|
||
}
|
||
for token, location in city_map.items():
|
||
if token in compact:
|
||
return location
|
||
return None, None
|
||
|
||
|
||
def split_supplier_cell(value: str | None, *, material_type: str | None = None) -> list[SupplierMention]:
|
||
if not value or value.strip() in {"/", "-", "—", "无", "未知"}:
|
||
return []
|
||
text = value.strip()
|
||
mentions: list[SupplierMention] = []
|
||
current_role: str | None = None
|
||
current_material = material_type
|
||
for chunk in _SEPARATOR_RE.split(text):
|
||
chunk = chunk.strip()
|
||
if not chunk:
|
||
continue
|
||
bare_label = chunk.rstrip(" ::")
|
||
if bare_label in {"内料", "包材", "包装", "瓶子", "泵头", "盖子", "软管", "彩盒", "物料", "加工厂", "供应商"}:
|
||
current_role = "加工厂" if "工厂" in bare_label else "供应商"
|
||
if bare_label not in {"加工厂", "供应商"}:
|
||
current_material = bare_label
|
||
continue
|
||
if chunk.endswith((":", ":")) and bare_label:
|
||
current_role = "加工厂" if "工厂" in bare_label else "供应商"
|
||
material_label = re.sub(r"(?:加工厂|工厂|供应商)$", "", bare_label)
|
||
if material_label:
|
||
current_material = material_label
|
||
continue
|
||
labels = list(_LABEL_RE.finditer(chunk))
|
||
if labels:
|
||
label = labels[-1].group("label")
|
||
current_role = "加工厂" if "工厂" in label else "供应商"
|
||
material_label = re.sub(r"(?:加工厂|工厂|供应商)$", "", label)
|
||
if material_label:
|
||
current_material = material_label
|
||
chunk = chunk[labels[-1].end() :].strip()
|
||
# A comma can separate material labels from the actual name.
|
||
if not chunk or chunk in {"/", "-", "—"}:
|
||
continue
|
||
name = normalize_name(chunk)
|
||
if not name:
|
||
continue
|
||
mentions.append(
|
||
SupplierMention(
|
||
name=name,
|
||
role=current_role,
|
||
material_type=current_material,
|
||
source_text=chunk,
|
||
method="delimiter" if len(_SEPARATOR_RE.findall(text)) else "raw",
|
||
confidence=0.95 if len(_SEPARATOR_RE.findall(text)) else 0.75,
|
||
)
|
||
)
|
||
return mentions
|