強化 PChome 變體錯配防線
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
OoO
2026-05-25 20:53:47 +08:00
parent 630d4c73bc
commit 50daad84a7
5 changed files with 314 additions and 12 deletions

View File

@@ -475,6 +475,7 @@ FOCUSED_IDENTITY_REVIEW_ONLY_REASONS = {
"johnsons_baby_lotion_variant_catalog",
"im_meme_fixx_cool_setting_spray",
"so_natural_fixx_setting_spray_catalog",
"kate_powder_case_catalog",
"kate_monster_lipstick_catalog",
"opi_gel_polish_series_catalog",
"romand_juicy_lip_tint_2_catalog",
@@ -527,6 +528,8 @@ VARIANT_SENSITIVE_KEYWORDS = {
"私密清潔凝露",
"私密潔淨凝露",
"私密淨白清潔凝露",
"私密防護慕絲",
"慕絲",
"定妝噴霧",
"妝前防護乳",
"妝前乳",
@@ -543,8 +546,13 @@ VARIANT_SENSITIVE_KEYWORDS = {
"腮紅液",
"打亮液",
"蜜粉餅",
"粉餅盒",
"粉底棒",
"遮瑕棒",
"遮瑕蜜",
"護手霜",
"滋養霜",
"修護乳",
"修容打亮棒",
"防曬",
"防曬乳",
@@ -556,6 +564,8 @@ VARIANT_SENSITIVE_KEYWORDS = {
VARIANT_OPTION_COLOR_WORDS = {
"茉莉花",
"梔子花",
"白茶蘭花",
"白茶",
"白麝香",
"黑麝香",
"清新花園",
@@ -563,8 +573,18 @@ VARIANT_OPTION_COLOR_WORDS = {
"青檸羅勒",
"炭木香",
"無花果",
"鼠尾草",
"海鹽",
"檸檬草",
"茶樹",
"櫻花",
"繡球花",
"魔髮奇緣",
"清甜柚香",
"杏仁牛奶",
"杏仁",
"薄荷",
"橙花",
"完熟白桃",
"琥珀橙",
"干邑棕",
@@ -603,6 +623,12 @@ VARIANT_OPTION_COLOR_WORDS = {
"自然色",
"明亮色",
"透明色",
"清爽型",
"滋潤型",
"橡棕",
"暗灰",
"灰棕",
"淺玫粉",
"極光之藍",
"月光銀影",
}
@@ -1976,6 +2002,15 @@ def score_marketplace_match(
aroma_scent_variant_conflict = _has_aroma_scent_variant_conflict(left, right)
if aroma_scent_variant_conflict:
reasons.append("aroma_scent_variant_conflict")
ingredient_line_conflict = _has_core_ingredient_line_conflict(left, right)
if ingredient_line_conflict:
reasons.append("core_ingredient_line_conflict")
branded_powder_line_conflict = _has_branded_powder_line_conflict(left, right)
if branded_powder_line_conflict:
reasons.append("branded_powder_line_conflict")
cleanser_lotion_line_conflict = _has_cleanser_lotion_line_conflict(left, right)
if cleanser_lotion_line_conflict:
reasons.append("cleanser_lotion_line_conflict")
wax_lamp_size_letter_conflict = _has_wax_lamp_size_letter_conflict(left, right)
if wax_lamp_size_letter_conflict:
reasons.append("size_letter_variant_conflict")
@@ -2043,6 +2078,12 @@ def score_marketplace_match(
hard_veto = True
if aroma_scent_variant_conflict:
hard_veto = True
if ingredient_line_conflict:
hard_veto = True
if branded_powder_line_conflict:
hard_veto = True
if cleanser_lotion_line_conflict:
hard_veto = True
if wax_lamp_size_letter_conflict:
hard_veto = True
if nitori_diffuser_model_conflict:
@@ -3051,7 +3092,26 @@ def _has_hoi_candle_line_conflict(left: ProductIdentity, right: ProductIdentity)
def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not any(term in pair_text for term in ("香氛固體凝膠", "香氛凝膠", "空氣芳香劑", "車用香氛")):
if any(term in pair_text for term in ("護手霜", "融蠟燈", "蠟燭暖燈")):
return False
if not any(
term in pair_text
for term in (
"香氛固體凝膠",
"香氛凝膠",
"空氣芳香劑",
"車用香氛",
"車用擴香",
"擴香蕊",
"擴香罐",
"香薰蠟燭",
"香氛蠟燭",
"蠟燭",
"滾珠精油",
"香氛精油",
"植物精油",
)
):
return False
if _is_multi_variant_catalog_listing(left) or _is_multi_variant_catalog_listing(right):
return False
@@ -3070,8 +3130,19 @@ def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdent
"青檸羅勒",
"炭木香",
"無花果",
"白茶蘭花",
"白茶",
"檸檬草",
"茶樹",
"鼠尾草",
"海鹽",
"橙花",
"薄荷",
"杏仁",
"薰衣草",
"茉莉",
"櫻花",
"繡球花",
"玫瑰",
"雪松",
"檀香",
@@ -3085,6 +3156,52 @@ def _has_aroma_scent_variant_conflict(left: ProductIdentity, right: ProductIdent
return False
def _has_core_ingredient_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not any(term in pair_text for term in ("油膏", "護膚油", "身體油", "精油", "", "乳霜")):
return False
ingredient_groups = {
"coconut_oil": ("椰子油", "coconut"),
"shea_butter": ("乳木果油", "shea"),
}
left_groups = {
group
for group, terms in ingredient_groups.items()
if any(term in left.searchable_name for term in terms)
}
right_groups = {
group
for group, terms in ingredient_groups.items()
if any(term in right.searchable_name for term in terms)
}
return bool(left_groups and right_groups and not (left_groups & right_groups))
def _has_branded_powder_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
if not ({"港香蘭"} & (left.brand_tokens & right.brand_tokens)):
return False
if "爽身粉" not in left.searchable_name or "爽身粉" not in right.searchable_name:
return False
named_lines = ("漢本", "艾魔菈")
left_lines = {line for line in named_lines if line in left.searchable_name}
right_lines = {line for line in named_lines if line in right.searchable_name}
return bool(left_lines and right_lines and not (left_lines & right_lines))
def _has_cleanser_lotion_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
if not (left.brand_tokens & right.brand_tokens):
return False
if not _has_overlapping_base_spec(left, right):
return False
cleanser_terms = ("潔膚露", "潔膚", "潔淨露", "潔面", "洗面乳", "cleanser")
lotion_terms = ("修護乳", "乳液", "身體乳", "潤膚乳", "lotion")
left_cleanser = any(term in left.searchable_name for term in cleanser_terms)
right_cleanser = any(term in right.searchable_name for term in cleanser_terms)
left_lotion = any(term in left.searchable_name for term in lotion_terms)
right_lotion = any(term in right.searchable_name for term in lotion_terms)
return bool((left_cleanser and right_lotion) or (right_cleanser and left_lotion))
def _standalone_size_letter_tokens(identity: ProductIdentity) -> set[str]:
text = identity.searchable_name
return {
@@ -3174,17 +3291,38 @@ def _has_catalog_specific_variant_selection_gap(left: ProductIdentity, right: Pr
"美體主張",
"私密潔浴露",
"私密潔浴",
"私密防護慕絲",
"私密慕絲",
"慕絲",
"嬰兒潤膚乳",
"定妝噴霧",
"染眉膏",
"眼線膠筆",
"粉餅盒",
"遮瑕蜜",
"護手霜",
"護唇膏",
"護唇棒",
"唇釉",
"唇膏",
"蜜粉",
"防曬素顏霜",
"車用香氛",
"車用擴香",
"車用擴香蕊",
"香氛擴香罐",
"擴香罐",
"擴香蕊",
"水性指甲油",
"指甲油",
"融蠟小夜燈",
"融蠟燈",
"滋養霜",
)
):
return False
left_catalog = _is_multi_variant_catalog_listing(left)
right_catalog = _is_multi_variant_catalog_listing(right)
left_catalog = _is_catalog_or_delimited_variant_listing(left)
right_catalog = _is_catalog_or_delimited_variant_listing(right)
return left_catalog != right_catalog
@@ -3560,6 +3698,12 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
and "全天候超完美定妝噴霧" in right_text
):
return "so_natural_fixx_setting_spray_catalog"
if (
{"kate", "凱婷"} & (left.brand_tokens & right.brand_tokens)
and "粉餅盒" in left_text
and "粉餅盒" in right_text
):
return "kate_powder_case_catalog"
if (
{"kate", "凱婷"} & (left.brand_tokens & right.brand_tokens)
and "怪獸級持色唇膏" in left_text
@@ -3749,6 +3893,42 @@ def _is_multi_variant_catalog_listing(identity: ProductIdentity) -> bool:
return any(phrase in text for phrase in MULTI_VARIANT_LISTING_PHRASES)
def _normalize_variant_option(value: str) -> set[str]:
compact = re.sub(r"[^a-z0-9]", "", (value or "").lower())
if not compact:
return set()
return {compact}
def _variant_option_compare_key(option: str) -> str:
if option.isdigit():
return option.lstrip("0") or "0"
return option
def _variant_options_overlap(left_options: set[str], right_options: set[str]) -> bool:
if left_options & right_options:
return True
left_keys = {_variant_option_compare_key(option) for option in left_options}
right_keys = {_variant_option_compare_key(option) for option in right_options}
return bool(left_keys & right_keys)
def _is_catalog_or_delimited_variant_listing(identity: ProductIdentity) -> bool:
if _is_multi_variant_catalog_listing(identity):
return True
text = identity.searchable_name
if re.search(r"(?<![a-z0-9])([a-z]?\d{1,3}[a-z]?)\s*(?:~||至|-)\s*([a-z]?\d{1,3}[a-z]?)(?![a-z0-9])", text, re.I):
return True
options = _explicit_variant_option_tokens(identity)
if len(options) < 2:
return bool(
re.search(r"[//、,.&]", text)
and any(term in text for term in ("粉餅盒", "眼線膠筆", "眉筆", "唇膏", "唇釉", "遮瑕蜜", "車用擴香", "車用香氛"))
)
return bool(re.search(r"[//、,.&]", text))
def _has_catalog_variant_listing_alignment(left: ProductIdentity, right: ProductIdentity) -> bool:
if not (_is_multi_variant_catalog_listing(left) and _is_multi_variant_catalog_listing(right)):
return False
@@ -3823,16 +4003,15 @@ def _has_variant_descriptor_conflict(left: ProductIdentity, right: ProductIdenti
def _explicit_variant_option_tokens(identity: ProductIdentity) -> set[str]:
text = identity.searchable_name
options: set[str] = set()
for match in re.finditer(r"(?<![a-z0-9])([a-z]?\d{1,3}[a-z]?)\s*(?:~||至|-)\s*([a-z]?\d{1,3}[a-z]?)(?![a-z0-9])", text, re.I):
for group in (match.group(1), match.group(2)):
options.update(_normalize_variant_option(group))
for match in re.finditer(r"(?:#|no\.?|色號|號色)\s*([a-z]?\d{1,3}[a-z]?)(?![a-z0-9])", text, re.I):
value = re.sub(r"[^a-z0-9]", "", match.group(1).lower())
if value:
options.add(value)
options.update(_normalize_variant_option(match.group(1)))
for match in re.finditer(r"(?<![a-z0-9])((?:0?\d){1,2})(?=[\u4e00-\u9fff])", text, re.I):
if text[match.end(1):match.end(1) + 4] in {"號護唇膏", "號護脣膏"}:
continue
value = re.sub(r"[^a-z0-9]", "", match.group(1).lower())
if value:
options.add(value)
options.update(_normalize_variant_option(match.group(1)))
for color_word in VARIANT_OPTION_COLOR_WORDS:
if color_word in text:
options.add(color_word)
@@ -3891,9 +4070,21 @@ def _has_explicit_variant_option_conflict(
return False
if left_options == right_options:
return False
if left_options & right_options:
if _variant_options_overlap(left_options, right_options):
if _has_catalog_options_against_generic_count_alignment(left, right, left_options, right_options):
return False
pair_text = f"{left.searchable_name} {right.searchable_name}"
if any(term in pair_text for term in ("眉筆", "眼線膠筆", "唇膏", "唇釉", "粉餅盒", "遮瑕蜜")) and (
(
len(left_options) > len(right_options)
and _is_catalog_or_delimited_variant_listing(left)
)
or (
len(right_options) > len(left_options)
and _is_catalog_or_delimited_variant_listing(right)
)
):
return False
if (
len(left_options) > len(right_options)
and _has_variant_option_selection_gap(left, left_options)
@@ -3923,15 +4114,37 @@ def _has_named_variant_selection_review(
return True
left_options = _explicit_variant_option_tokens(left)
right_options = _explicit_variant_option_tokens(right)
if left_options and right_options:
for catalog_identity, catalog_options, specific_options in (
(left, left_options, right_options),
(right, right_options, left_options),
):
if (
_is_catalog_or_delimited_variant_listing(catalog_identity)
and len(catalog_options) > len(specific_options)
and _variant_options_overlap(catalog_options, specific_options)
and _is_variant_sensitive_identity(left, right, shared_anchor)
):
return True
if bool(left_options) != bool(right_options):
option_identity = left if left_options else right
catalog_identity = right if left_options else left
if (
_is_variant_sensitive_identity(left, right, shared_anchor)
and _is_multi_variant_catalog_listing(catalog_identity)
and _is_catalog_or_delimited_variant_listing(catalog_identity)
and _explicit_variant_option_tokens(option_identity)
):
return True
if (
_is_variant_sensitive_identity(left, right, shared_anchor)
and _has_overlapping_base_spec(left, right)
and _explicit_variant_option_tokens(option_identity)
and any(
term in f"{left.searchable_name} {right.searchable_name}"
for term in ("粉餅盒", "護手霜", "護唇膏", "護唇棒", "滋養霜", "眼線膠筆", "遮瑕蜜")
)
):
return True
if bool(left_options) == bool(right_options):
return False