V10.584 強化 PChome Nick 比對窄門
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s

This commit is contained in:
OoO
2026-06-04 14:33:26 +08:00
parent 133232e2f4
commit a1e56a2c6b
8 changed files with 156 additions and 6 deletions

View File

@@ -526,6 +526,7 @@ FOCUSED_IDENTITY_VARIANT_REVIEW_BYPASS_REASONS = {
"ts6_lubricant_100g_3pack",
"ts6_peach_bright_gel_45g_3pack",
"ts6_white_wash_peach_gel_kit",
"ts6_cooling_clean_mousse_100g",
"vaseline_baby_jelly_368g_3pack",
}
@@ -601,7 +602,10 @@ FOCUSED_IDENTITY_TOTAL_PRICE_REASONS = {
"ts6_lubricant_100g_3pack",
"ts6_peach_bright_gel_45g_3pack",
"ts6_white_wash_peach_gel_kit",
"ts6_cooling_clean_mousse_100g",
"vaseline_baby_jelly_368g_3pack",
"nivea_creme_100ml",
"schick_womens_sensitive_blade_3pack",
}
SEARCH_BROAD_ANCHORS = {
@@ -1665,6 +1669,15 @@ def _has_paulas_choice_body_lotion_210ml_2pack_alignment(
return has_two_pack(left) and has_two_pack(right)
def _has_nivea_creme_100ml_alignment(left: ProductIdentity, right: ProductIdentity) -> bool:
brand_tokens = left.brand_tokens | right.brand_tokens
if not ({"nivea", "妮維雅"} & brand_tokens):
return False
if not _has_shared_volume(left, right, 100):
return False
return all("妮維雅霜" in item.searchable_name and "隨身版" in item.searchable_name for item in (left, right))
def _has_refill_pack(identity: ProductIdentity) -> bool:
text = identity.normalized_name
return bool(
@@ -2332,8 +2345,11 @@ def score_marketplace_match(
if brand_reason:
reasons.append(brand_reason)
reasons.extend(spec_reasons)
if left.product_type and right.product_type and left.product_type != right.product_type:
nivea_creme_100ml_alignment = _has_nivea_creme_100ml_alignment(left, right)
if left.product_type and right.product_type and left.product_type != right.product_type and not nivea_creme_100ml_alignment:
reasons.append("type_conflict")
if nivea_creme_100ml_alignment:
reasons.append("nivea_creme_100ml_type_alignment")
model_line_conflict = _has_model_line_conflict(left, right)
if model_line_conflict:
reasons.append("model_line_conflict")
@@ -2548,7 +2564,7 @@ def score_marketplace_match(
hard_veto = True
if chinese_name_score < 0.16 and token_score < 0.72:
hard_veto = True
if left.product_type and right.product_type and left.product_type != right.product_type:
if left.product_type and right.product_type and left.product_type != right.product_type and not nivea_creme_100ml_alignment:
hard_veto = True
if sun_protection_line_conflict:
hard_veto = True
@@ -4562,6 +4578,14 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
and _has_exact_count_alignment(left, right)
):
return "ts6_white_mousse_180g_3pack"
if (
{"ts6", "護一生"} & (left.brand_tokens & right.brand_tokens)
and "沁涼潔淨慕斯" in left_text
and "沁涼潔淨慕斯" in right_text
and _has_shared_weight(left, right, 100)
and _has_exact_count_alignment(left, right)
):
return "ts6_cooling_clean_mousse_100g"
if (
{"ts6", "護一生"} & (left.brand_tokens & right.brand_tokens)
and "蜜愛潤滑液" in left_text
@@ -4659,6 +4683,15 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
and _has_shared_volume(left, right, 473)
):
return "cetaphil_long_lotion_473ml"
if (
{"nivea", "妮維雅"} & (left.brand_tokens & right.brand_tokens)
and "妮維雅霜" in left_text
and "妮維雅霜" in right_text
and "隨身版" in left_text
and "隨身版" in right_text
and _has_shared_volume(left, right, 100)
):
return "nivea_creme_100ml"
if (
"nailmatic" in (left.brand_tokens & right.brand_tokens)
and "小精靈" in left_text
@@ -4801,6 +4834,17 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
and "唇頰兩用" in pair_text
):
return "benefit_lip_tint"
if (
{"schick", "舒適牌"} & (left.brand_tokens & right.brand_tokens)
and "舒綺" in left_text
and "舒綺" in right_text
and "除毛刀片" in left_text
and "除毛刀片" in right_text
and "敏感肌" in left_text
and "敏感肌" in right_text
and _has_shared_count(left, right, 3, "")
):
return "schick_womens_sensitive_blade_3pack"
if (
{"herb24", "草本"} & (left.brand_tokens & right.brand_tokens)
and "晨霧純精油擴香儀ii" in left_text

View File

@@ -11,6 +11,7 @@ API 參考:
- 圖片 URL: https://img.pchome.com.tw/cs{Pic.B}
"""
import html
import re
import time
import logging
@@ -49,14 +50,34 @@ class PChomeProduct:
return data
def _clean_subtitle_text(value: str) -> str:
"""Normalize PChome Nick text before using it as identity evidence."""
text = html.unescape(str(value or ""))
text = re.sub(r"<\s*br\s*/?\s*>", " ", text, flags=re.I)
text = re.sub(r"<[^>]+>", " ", text)
text = re.sub(r"[★☆][^★☆]{0,80}[★☆]", " ", text)
return re.sub(r"\s+", " ", text).strip()
def _compact_identity_text(value: str) -> str:
return re.sub(r"[^0-9a-zA-Z\u4e00-\u9fff]+", "", str(value or "").lower())
def _build_match_name(name: str, subtitle: str) -> str:
"""Build an identity-rich title without duplicating the PChome display name."""
display_name = str(name or '').strip()
nick = str(subtitle or '').strip()
nick = _clean_subtitle_text(subtitle)
if not nick or nick == display_name:
return display_name
if display_name and nick.startswith(display_name):
display_compact = _compact_identity_text(display_name)
nick_compact = _compact_identity_text(nick)
if display_name and (
nick.startswith(display_name)
or (display_compact and nick_compact.startswith(display_compact))
):
return nick
if display_compact and display_compact == nick_compact:
return display_name
return f"{display_name} {nick}".strip()
@@ -348,7 +369,7 @@ class PChomeCrawler:
image_url = f"{self.IMAGE_BASE_URL}{pic_path}" if pic_path else ''
name = data.get('Name', '') or ''
subtitle = data.get('Nick', '') or ''
subtitle = _clean_subtitle_text(data.get('Nick', '') or '')
return PChomeProduct(
product_id=product_id,