| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- # -*- coding: utf-8 -*-
- """Strict customer taxonomy and classification helpers for Wuling outreach.
- Source intentionally uses unicode escapes for Chinese labels so scripted edits do
- not depend on the Windows console code page. Runtime values are normal unicode.
- """
- from __future__ import annotations
- import re
- from typing import Any, Mapping, Sequence, Tuple
- def zh(value: str) -> str:
- return value.encode("ascii").decode("unicode_escape")
- AUTO_CHANNEL = zh(r"\u6c7d\u8f66\u6e20\u9053\u5408\u4f5c\u4f19\u4f34")
- BULK_OPERATOR = zh(r"\u6279\u91cf\u91c7\u8d2d\u4e0e\u8fd0\u8425\u5ba2\u6237")
- USED_TRANSITION = zh(r"\u4e8c\u624b\u8f66\u8f6c\u578b\u5019\u9009")
- PLATFORM_CHANNEL = zh(r"\u5e73\u53f0\u4e0e\u884c\u4e1a\u6e20\u9053")
- ECOSYSTEM_SUPPORT = zh(r"\u751f\u6001\u652f\u6301\u8d44\u6e90")
- CAR_IMPORTER = zh(r"\u6c7d\u8f66\u8fdb\u53e3\u5546")
- NATIONAL_AGENT = zh(r"\u5168\u56fd\u4ee3\u7406\u5546")
- NATIONAL_DISTRIBUTOR = zh(r"\u5168\u56fd\u5206\u9500\u5546")
- REGIONAL_DISTRIBUTOR = zh(r"\u533a\u57df\u5206\u9500\u5546")
- MULTI_BRAND_DEALER = zh(r"\u591a\u54c1\u724c\u7ecf\u9500\u5546")
- COMMERCIAL_VEHICLE_CHANNEL = zh(r"\u5546\u7528\u8f66\u6e20\u9053\u5546")
- CHINA_BRAND_DEALER = zh(r"\u4e2d\u56fd\u54c1\u724c\u7ecf\u9500\u5546")
- NEV_SMALL_VEHICLE_CHANNEL = zh(r"\u65b0\u80fd\u6e90\u6216\u5c0f\u578b\u8f66\u6e20\u9053\u5546")
- CAR_RENTAL = zh(r"\u6c7d\u8f66\u79df\u8d41\u516c\u53f8")
- LONG_TERM_RENTAL = zh(r"\u957f\u671f\u79df\u8d41\u516c\u53f8")
- CORPORATE_FLEET = zh(r"\u4f01\u4e1a\u8f66\u961f")
- LOGISTICS_DELIVERY = zh(r"\u7269\u6d41\u914d\u9001\u4f01\u4e1a")
- GOV_PROCUREMENT = zh(r"\u653f\u5e9c\u6216\u673a\u6784\u91c7\u8d2d\u65b9")
- USED_CHAIN = zh(r"\u8fde\u9501\u4e8c\u624b\u8f66\u4f01\u4e1a")
- USED_PHYSICAL = zh(r"\u5b9e\u4f53\u4e8c\u624b\u8f66\u4f01\u4e1a")
- USED_IMPORTER = zh(r"\u8fdb\u53e3\u4e8c\u624b\u8f66\u4f01\u4e1a")
- NEW_USED_COMPREHENSIVE = zh(r"\u65b0\u8f66\u4e0e\u4e8c\u624b\u8f66\u7efc\u5408\u4f01\u4e1a")
- CAR_MARKETPLACE = zh(r"\u6c7d\u8f66\u4ea4\u6613\u5e73\u53f0")
- CAR_ASSOCIATION = zh(r"\u6c7d\u8f66\u534f\u4f1a")
- CHAMBER = zh(r"\u5546\u4f1a")
- DEALER_ALLIANCE = zh(r"\u8f66\u5546\u8054\u76df")
- DEALER_REFERRAL = zh(r"\u7ecf\u9500\u5546\u8d44\u6e90\u5f15\u8350\u673a\u6784")
- AFTER_SALES_NETWORK = zh(r"\u552e\u540e\u670d\u52a1\u7f51\u7edc")
- PARTS_WAREHOUSE = zh(r"\u5907\u4ef6\u4f9b\u5e94\u4e0e\u4ed3\u50a8\u4f01\u4e1a")
- IMPORT_CERTIFICATION = zh(r"\u8fdb\u53e3\u8ba4\u8bc1\u4e0e\u4e0a\u724c\u673a\u6784")
- FINANCE_INSURANCE = zh(r"\u91d1\u878d\u4fdd\u9669\u673a\u6784")
- VEHICLE_LOGISTICS = zh(r"\u8f66\u8f86\u7269\u6d41\u4f01\u4e1a")
- CUSTOMER_TAXONOMY = {
- AUTO_CHANNEL: [
- CAR_IMPORTER,
- NATIONAL_AGENT,
- NATIONAL_DISTRIBUTOR,
- REGIONAL_DISTRIBUTOR,
- MULTI_BRAND_DEALER,
- COMMERCIAL_VEHICLE_CHANNEL,
- CHINA_BRAND_DEALER,
- NEV_SMALL_VEHICLE_CHANNEL,
- ],
- BULK_OPERATOR: [
- CAR_RENTAL,
- LONG_TERM_RENTAL,
- CORPORATE_FLEET,
- LOGISTICS_DELIVERY,
- GOV_PROCUREMENT,
- ],
- USED_TRANSITION: [
- USED_CHAIN,
- USED_PHYSICAL,
- USED_IMPORTER,
- NEW_USED_COMPREHENSIVE,
- ],
- PLATFORM_CHANNEL: [
- CAR_MARKETPLACE,
- CAR_ASSOCIATION,
- CHAMBER,
- DEALER_ALLIANCE,
- DEALER_REFERRAL,
- ],
- ECOSYSTEM_SUPPORT: [
- AFTER_SALES_NETWORK,
- PARTS_WAREHOUSE,
- IMPORT_CERTIFICATION,
- FINANCE_INSURANCE,
- VEHICLE_LOGISTICS,
- ],
- }
- VALID_ATTRIBUTES = set(CUSTOMER_TAXONOMY.keys())
- VALID_CUSTOMER_TYPES = {item for items in CUSTOMER_TAXONOMY.values() for item in items}
- TYPE_TO_ATTRIBUTE = {item: attr for attr, items in CUSTOMER_TAXONOMY.items() for item in items}
- KEY_NAME = zh(r"\u516c\u53f8\u59d3\u540d")
- KEY_ATTRIBUTE = zh(r"\u5ba2\u6237\u5c5e\u6027")
- KEY_TYPE = zh(r"\u5ba2\u6237\u7c7b\u578b")
- KEY_BUSINESS = zh(r"\u4e3b\u8425\u4e1a\u52a1")
- KEY_NOTE = zh(r"\u5907\u6ce8")
- OLD_TO_NEW_TYPE = {
- zh(r"\u533a\u57df\u65b0\u8f66\u7ecf\u9500\u516c\u53f8"): REGIONAL_DISTRIBUTOR,
- zh(r"\u533a\u57df\u591a\u54c1\u724c\u6388\u6743\u7ecf\u9500\u516c\u53f8"): MULTI_BRAND_DEALER,
- zh(r"\u5168\u56fd\u591a\u54c1\u724c\u6c7d\u8f66\u5206\u9500\u96c6\u56e2"): NATIONAL_DISTRIBUTOR,
- zh(r"\u5168\u56fd\u591a\u54c1\u724c\u8fdb\u53e3\u4e0e\u5206\u9500\u96c6\u56e2"): NATIONAL_DISTRIBUTOR,
- zh(r"\u6c7d\u8f66\u8fdb\u53e3\u4e0e\u5206\u9500\u516c\u53f8"): CAR_IMPORTER,
- zh(r"\u6c7d\u8f66\u8fdb\u53e3\u4e0e\u5206\u9500\u8fd0\u8425\u516c\u53f8"): CAR_IMPORTER,
- zh(r"\u8f7b\u578b\u5546\u7528\u8f66\u8fdb\u53e3\u4e0e\u5206\u9500\u516c\u53f8"): COMMERCIAL_VEHICLE_CHANNEL,
- zh(r"\u591a\u54c1\u724c\u6c7d\u8f66\u4e0e\u5546\u7528\u8f66\u5206\u9500\u516c\u53f8"): COMMERCIAL_VEHICLE_CHANNEL,
- zh(r"\u6c7d\u8f66\u5bfc\u8d2d\u3001\u5185\u5bb9\u4e0e\u4ea4\u6613\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u6c7d\u8f66\u4e0e\u5546\u7528\u8f66\u4ea4\u6613\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u7efc\u5408\u6c7d\u8f66\u4ea4\u6613\u4e0e\u4e13\u4e1a\u76ee\u5f55\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u7efc\u5408\u5206\u7c7b\u4fe1\u606f\u4e0e\u6c7d\u8f66\u4ea4\u6613\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u8ba4\u8bc1\u4e8c\u624b\u8f66\u4ea4\u6613\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u96c6\u56e2\u5316\u4e8c\u624b\u8f66\u4ea4\u6613\u4e0e\u7f6e\u6362\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u6c7d\u8f66\u4ea4\u6613\u4e0e\u7ecf\u9500\u5546\u83b7\u5ba2\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u8ba4\u8bc1\u4e8c\u624b\u8f66\u4e0e\u6c7d\u8f66\u4ea4\u6613\u5e73\u53f0"): CAR_MARKETPLACE,
- zh(r"\u6c7d\u8f66\u8fdb\u53e3\u5546\u884c\u4e1a\u534f\u4f1a"): CAR_ASSOCIATION,
- zh(r"\u6c7d\u8f66\u5de5\u4e1a\u4e0e\u884c\u4e1a\u534f\u4f1a"): CAR_ASSOCIATION,
- zh(r"\u65e0\u53f8\u673a\u6c7d\u8f66\u79df\u8d41\u884c\u4e1a\u8054\u5408\u4f1a"): CAR_ASSOCIATION,
- zh(r"\u884c\u4e1aB2B\u64ae\u5408\u4e0e\u4f1a\u8bae\u5e73\u53f0"): DEALER_REFERRAL,
- zh(r"\u6c7d\u8f66\u884c\u4e1a\u5c55\u4f1a\u4e0e\u4f01\u4e1a\u6d3b\u52a8\u5e73\u53f0"): DEALER_REFERRAL,
- zh(r"\u4e8c\u624b\u8f66\u5546"): USED_PHYSICAL,
- zh(r"\u4e8c\u624b\u8f66\u5546/\u4e8c\u624b\u8f66\u5e73\u53f0"): USED_PHYSICAL,
- zh(r"\u5546\u7528\u8f66/\u8f66\u961f\u6e20\u9053"): COMMERCIAL_VEHICLE_CHANNEL,
- zh(r"\u79df\u8d41/\u8f66\u961f\u516c\u53f8"): CAR_RENTAL,
- zh(r"\u8fdb\u53e3\u5546/\u6c7d\u8f66\u96c6\u56e2"): CAR_IMPORTER,
- zh(r"\u591a\u54c1\u724c\u7ecf\u9500\u5546/showroom"): MULTI_BRAND_DEALER,
- }
- QUESTION_MARK_RE = re.compile(r"\?{3,}")
- SPLIT_CLASS_RE = re.compile(r"[\n\r;/??,?]+")
- def clean(value: Any) -> str:
- if value is None:
- return ""
- return re.sub(r"\s+", " ", str(value).strip())
- def contains_any(text: str, keywords: Sequence[str]) -> bool:
- lower = text.casefold()
- return any(keyword.casefold() in lower for keyword in keywords)
- def is_valid_pair(attribute: str, customer_type: str) -> bool:
- return clean(customer_type) in CUSTOMER_TAXONOMY.get(clean(attribute), [])
- def type_to_attribute(customer_type: str) -> str:
- return TYPE_TO_ATTRIBUTE.get(clean(customer_type), "")
- def _split_class_values(value: str) -> list[str]:
- return [item for item in (clean(part) for part in SPLIT_CLASS_RE.split(value or "")) if item]
- def _match_old_type(text: str) -> str:
- for old, new in OLD_TO_NEW_TYPE.items():
- if old.casefold() in text.casefold():
- return new
- return ""
- def _record_text(record: Mapping[str, Any], sheet_name: str = "", sources: Sequence[str] = ()) -> str:
- preferred = [
- sheet_name,
- " ".join(str(item) for item in sources),
- str(record.get("name", "")),
- str(record.get("attribute", "")),
- str(record.get("type", "")),
- str(record.get("business", "")),
- str(record.get("note", "")),
- str(record.get("link", "")),
- str(record.get("website", "")),
- str(record.get(KEY_NAME, "")),
- str(record.get(KEY_ATTRIBUTE, "")),
- str(record.get(KEY_TYPE, "")),
- str(record.get(KEY_BUSINESS, "")),
- str(record.get(KEY_NOTE, "")),
- str(record.get("??", "")),
- ]
- return " ".join(item for item in preferred if item).casefold()
- def _platform_type(text: str) -> str:
- if contains_any(text, ["chamber", "commerce chamber", zh(r"\u5546\u4f1a")]):
- return CHAMBER
- if contains_any(text, ["association", "federation", "f?d?ration", "federation", "syndicat", zh(r"\u534f\u4f1a")]):
- return CAR_ASSOCIATION
- if contains_any(text, ["alliance", "union", zh(r"\u8054\u76df")]):
- return DEALER_ALLIANCE
- if contains_any(text, ["lead", "referral", "b2b", "event", "exhibition", "salon", zh(r"\u5f15\u8350"), zh(r"\u8d44\u6e90"), zh(r"\u64ae\u5408"), zh(r"\u5c55\u4f1a"), zh(r"\u83b7\u5ba2")]):
- return DEALER_REFERRAL
- return CAR_MARKETPLACE
- def _ecosystem_type(text: str) -> str:
- if contains_any(text, ["spare", "parts", "piece", "pi?ce", "warehouse", "storage", zh(r"\u5907\u4ef6"), zh(r"\u914d\u4ef6"), zh(r"\u4ed3\u50a8")]):
- return PARTS_WAREHOUSE
- if contains_any(text, ["homolog", "certification", "registration", "immatriculation", zh(r"\u8ba4\u8bc1"), zh(r"\u4e0a\u724c")]):
- return IMPORT_CERTIFICATION
- if contains_any(text, ["insurance", "assurance", "finance", "financement", "bank", zh(r"\u4fdd\u9669"), zh(r"\u91d1\u878d")]):
- return FINANCE_INSURANCE
- if contains_any(text, ["vehicle logistics", "transport automobile", "shipping", zh(r"\u8f66\u8f86\u7269\u6d41")]):
- return VEHICLE_LOGISTICS
- return AFTER_SALES_NETWORK
- def _bulk_type(text: str) -> str:
- if contains_any(text, ["government", "public agency", "tender", "procurement", zh(r"\u653f\u5e9c"), zh(r"\u673a\u6784"), zh(r"\u91c7\u8d2d")]):
- return GOV_PROCUREMENT
- if contains_any(text, ["logistics", "delivery", "courier", "express", zh(r"\u7269\u6d41"), zh(r"\u914d\u9001")]):
- return LOGISTICS_DELIVERY
- if contains_any(text, ["corporate fleet", "fleet", "flotte", zh(r"\u4f01\u4e1a\u8f66\u961f"), zh(r"\u8f66\u961f")]):
- return CORPORATE_FLEET
- if contains_any(text, ["long-term", "long term", "lld", "leasing longue", zh(r"\u957f\u671f\u79df\u8d41")]):
- return LONG_TERM_RENTAL
- return CAR_RENTAL
- def _used_type(text: str) -> str:
- if contains_any(text, ["chain", "network", "r?seau", "reseau", "branches", "succursale", "multi-city", zh(r"\u8fde\u9501"), zh(r"\u591a\u95e8\u5e97")]):
- return USED_CHAIN
- if contains_any(text, ["import", "importation", zh(r"\u8fdb\u53e3")]):
- return USED_IMPORTER
- if contains_any(text, ["new vehicle", "v?hicules neufs", "vehicules neufs", "neuf", zh(r"\u65b0\u8f66")]):
- return NEW_USED_COMPREHENSIVE
- return USED_PHYSICAL
- def _auto_channel_type(text: str) -> str:
- if contains_any(text, ["commercial vehicle", "utilitaire", "camion", "truck", "van", "bus", "mpv", zh(r"\u5546\u7528\u8f66"), zh(r"\u8d27\u8f66"), zh(r"\u5361\u8f66")]):
- return COMMERCIAL_VEHICLE_CHANNEL
- if contains_any(text, ["byd", "jac", "dfsk", "foton", "changan", "chery", "geely", "mg", "haval", "gwm", "leapmotor", zh(r"\u4e2d\u56fd\u54c1\u724c")]):
- return CHINA_BRAND_DEALER
- if contains_any(text, ["ev", "electric", "hybrid", "new energy", "mini", "micro", "small car", zh(r"\u65b0\u80fd\u6e90"), zh(r"\u5c0f\u578b"), zh(r"\u5fae\u578b")]):
- return NEV_SMALL_VEHICLE_CHANNEL
- if contains_any(text, ["national agent", "exclusive agent", "importateur exclusif", zh(r"\u5168\u56fd\u4ee3\u7406")]):
- return NATIONAL_AGENT
- if contains_any(text, ["national distributor", "nationwide", "dealer network", "r?seau", "reseau", "group", "groupe", "succursale", zh(r"\u5168\u56fd\u5206\u9500"), zh(r"\u5168\u56fd"), zh(r"\u96c6\u56e2"), zh(r"\u7f51\u7edc")]):
- return NATIONAL_DISTRIBUTOR
- if contains_any(text, ["importateur", "importer", "importation", zh(r"\u8fdb\u53e3")]):
- return CAR_IMPORTER
- if contains_any(text, ["distributeur", "distribution", zh(r"\u5206\u9500")]):
- return REGIONAL_DISTRIBUTOR
- return MULTI_BRAND_DEALER
- def classify_from_text(text: str) -> Tuple[str, str]:
- text = clean(text).casefold()
- old = _match_old_type(text)
- if old:
- return type_to_attribute(old), old
- # Strong non-dealer classes first, then dealership/channel evidence.
- if contains_any(text, ["association", "federation", "f?d?ration", "chamber", "marketplace", "annuaire", "directory", "classified", "portal", "b2b", "exhibition", zh(r"\u5e73\u53f0"), zh(r"\u534f\u4f1a"), zh(r"\u5546\u4f1a"), zh(r"\u8054\u76df"), zh(r"\u5f15\u8350"), zh(r"\u8d44\u6e90"), zh(r"\u76ee\u5f55")]):
- item = _platform_type(text)
- return PLATFORM_CHANNEL, item
- if contains_any(text, ["after-sales", "service network", "maintenance", "repair", "garage", "spare", "parts", "homolog", "certification", "registration", "insurance", "assurance", "finance", "vehicle logistics", zh(r"\u552e\u540e"), zh(r"\u7ef4\u4fee"), zh(r"\u5907\u4ef6"), zh(r"\u914d\u4ef6"), zh(r"\u8ba4\u8bc1"), zh(r"\u4e0a\u724c"), zh(r"\u91d1\u878d"), zh(r"\u4fdd\u9669"), zh(r"\u8f66\u8f86\u7269\u6d41")]):
- item = _ecosystem_type(text)
- return ECOSYSTEM_SUPPORT, item
- if contains_any(text, ["rent", "rental", "location", "leasing", "fleet", "flotte", "logistics", "delivery", "procurement", zh(r"\u79df\u8d41"), zh(r"\u79df\u8f66"), zh(r"\u8f66\u961f"), zh(r"\u7269\u6d41"), zh(r"\u914d\u9001"), zh(r"\u91c7\u8d2d")]):
- item = _bulk_type(text)
- return BULK_OPERATOR, item
- if contains_any(text, ["occasion", "used", "second hand", "second-hand", "pre-owned", "reprise", "parc occasion", "occaz", zh(r"\u4e8c\u624b"), zh(r"\u7f6e\u6362")]):
- item = _used_type(text)
- return USED_TRANSITION, item
- if contains_any(text, ["auto", "automobile", "cars", "motors", "concessionnaire", "dealer", "showroom", "vehicle", "v?hicule", "vehicule", "neuf", "import", "distributeur", "distribution", "multimarque", "multi-brand", "brand", "marque", zh(r"\u6c7d\u8f66"), zh(r"\u65b0\u8f66"), zh(r"\u5c55\u5385"), zh(r"\u7ecf\u9500"), zh(r"\u5206\u9500"), zh(r"\u8fdb\u53e3"), zh(r"\u591a\u54c1\u724c")]):
- item = _auto_channel_type(text)
- return AUTO_CHANNEL, item
- return "", ""
- def normalize_existing_classification(attribute: str, customer_type: str, evidence_text: str = "") -> Tuple[str, str]:
- attr = clean(attribute)
- typ = clean(customer_type)
- if is_valid_pair(attr, typ) and "\n" not in attr and "\n" not in typ:
- return attr, typ
- for part in _split_class_values(typ):
- old = OLD_TO_NEW_TYPE.get(part)
- if old:
- return type_to_attribute(old), old
- if part in VALID_CUSTOMER_TYPES:
- return type_to_attribute(part), part
- if attr in VALID_ATTRIBUTES and typ in CUSTOMER_TAXONOMY.get(attr, []):
- return attr, typ
- return classify_from_text(" ".join([attribute or "", customer_type or "", evidence_text or ""]))
- def classify_attribute_type(record: Mapping[str, Any], sheet_name: str = "", sources: Sequence[str] = ()) -> Tuple[str, str]:
- attr = clean(record.get("attribute") or record.get(KEY_ATTRIBUTE) or "")
- typ = clean(record.get("type") or record.get(KEY_TYPE) or "")
- text = _record_text(record, sheet_name, sources)
- normalized = normalize_existing_classification(attr, typ, text)
- if normalized != ("", ""):
- return normalized
- return classify_from_text(text)
|