V10.489 strengthen PChome manual review matching

This commit is contained in:
OoO
2026-05-29 11:24:27 +08:00
parent 81701e85f4
commit c1b2fa82f5
7 changed files with 217 additions and 2 deletions

View File

@@ -1386,6 +1386,58 @@ def _has_pack_quantity_difference(left: ProductIdentity, right: ProductIdentity)
return False
NAMED_COMPONENT_QUANTITY_GROUPS = (
("嬰兒沐浴精", "嬰幼童洗髮精"),
("魅惑麋香", "湛藍海洋", "花妍巧語", "絲絨玫瑰"),
)
def _named_component_quantity_map(identity: ProductIdentity, terms: Iterable[str]) -> dict[str, int]:
text = identity.searchable_name
present_terms = tuple(term for term in terms if term in text)
if len(present_terms) < 2:
return {}
quantities: dict[str, int] = {}
for term in present_terms:
term_index = text.find(term)
if term_index < 0:
continue
suffix = text[term_index + len(term):term_index + len(term) + 28]
explicit_count = re.search(
r"(?:\d+(?:\.\d+)?\s*(?:ml|g|mg|毫升|公克|毫克))?\s*(?:x|乘)\s*(\d+)",
suffix,
flags=re.I,
)
if explicit_count:
quantities[term] = int(explicit_count.group(1))
if len(quantities) == len(present_terms):
return quantities
pack_counts = [
count
for count, unit in identity.counts
if _count_unit_family(unit) in COUNT_UNITS or unit in COUNT_UNITS
]
if not quantities and pack_counts and max(pack_counts) == len(present_terms) and re.search(r"[+//、]", text):
return {term: 1 for term in present_terms}
return {}
def _has_named_component_quantity_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
"""同名組合品若命名元件相同但數量反轉,不能視為同一價格標的。"""
for terms in NAMED_COMPONENT_QUANTITY_GROUPS:
left_quantities = _named_component_quantity_map(left, terms)
right_quantities = _named_component_quantity_map(right, terms)
shared_terms = set(left_quantities) & set(right_quantities)
if len(shared_terms) < 2:
continue
if any(left_quantities[term] != right_quantities[term] for term in shared_terms):
return True
return False
def _spec_score(left: ProductIdentity, right: ProductIdentity) -> tuple[float, bool, tuple[str, ...]]:
volume_score, volume_conflict = _spec_component(left.volumes_ml, right.volumes_ml)
weight_score, weight_conflict = _spec_component(left.weights_g, right.weights_g)
@@ -1977,6 +2029,9 @@ def score_marketplace_match(
reasons.append("catalog_count_omission")
if _has_pack_quantity_difference(left, right):
reasons.append("pack_quantity_difference")
named_component_quantity_conflict = _has_named_component_quantity_conflict(left, right)
if named_component_quantity_conflict:
reasons.append("named_component_quantity_conflict")
variant_descriptor_conflict = _has_variant_descriptor_conflict(left, right, shared_anchor)
sun_protection_line_conflict = (
variant_descriptor_conflict
@@ -2037,6 +2092,9 @@ def score_marketplace_match(
ingredient_line_conflict = _has_core_ingredient_line_conflict(left, right)
if ingredient_line_conflict:
reasons.append("core_ingredient_line_conflict")
clarins_body_oil_line_conflict = _has_clarins_body_oil_line_conflict(left, right)
if clarins_body_oil_line_conflict:
reasons.append("clarins_body_oil_line_conflict")
branded_powder_line_conflict = _has_branded_powder_line_conflict(left, right)
if branded_powder_line_conflict:
reasons.append("branded_powder_line_conflict")
@@ -2046,6 +2104,9 @@ def score_marketplace_match(
selection1990_wax_lamp_design_conflict = _has_selection1990_wax_lamp_design_conflict(left, right)
if selection1990_wax_lamp_design_conflict:
reasons.append("selection1990_wax_lamp_design_conflict")
hooome_wax_lamp_design_gap = _has_hooome_wax_lamp_design_gap(left, right)
if hooome_wax_lamp_design_gap:
reasons.append("hooome_wax_lamp_design_gap")
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")
@@ -2085,6 +2146,7 @@ def score_marketplace_match(
or relove_private_cleanser_variant_gap
or candle_catalog_selection_gap
or bath_additive_variant_gap
or hooome_wax_lamp_design_gap
or makeup_catalog_selection_gap
or loreal_serum_variant_gap
or sebamed_shampoo_variant_catalog_gap
@@ -2101,6 +2163,8 @@ def score_marketplace_match(
hard_veto = True
if multi_component_count_conflict:
hard_veto = True
if named_component_quantity_conflict:
hard_veto = True
if _has_refill_pack(left) != _has_refill_pack(right):
hard_veto = True
if accessory_case_conflict:
@@ -2149,6 +2213,8 @@ def score_marketplace_match(
hard_veto = True
if ingredient_line_conflict:
hard_veto = True
if clarins_body_oil_line_conflict:
hard_veto = True
if branded_powder_line_conflict:
hard_veto = True
if cleanser_lotion_line_conflict:
@@ -3282,6 +3348,29 @@ def _has_core_ingredient_line_conflict(left: ProductIdentity, right: ProductIden
return bool(left_groups and right_groups and not (left_groups & right_groups))
def _has_clarins_body_oil_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
if not ({"clarins", "克蘭詩"} & (left.brand_tokens & right.brand_tokens)):
return False
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not any(term in pair_text for term in ("護理油", "身體油", "美體油", "調和護理油")):
return False
line_groups = {
"contour_lightweight": ("輕盈美體", "美體護理油", "contour"),
"tonic_body": ("身體調和", "調和護理油", "孕期身體調和", "tonic"),
}
left_groups = {
group
for group, terms in line_groups.items()
if any(term in left.searchable_name for term in terms)
}
right_groups = {
group
for group, terms in line_groups.items()
if any(term in right.searchable_name for term in terms)
}
return bool(left_groups and right_groups and left_groups.isdisjoint(right_groups))
def _has_branded_powder_line_conflict(left: ProductIdentity, right: ProductIdentity) -> bool:
if not ({"港香蘭"} & (left.brand_tokens & right.brand_tokens)):
return False
@@ -3332,6 +3421,18 @@ def _has_selection1990_wax_lamp_design_conflict(left: ProductIdentity, right: Pr
return bool(left_groups and right_groups and left_groups.isdisjoint(right_groups))
def _has_hooome_wax_lamp_design_gap(left: ProductIdentity, right: ProductIdentity) -> bool:
if "hooome" not in (left.brand_tokens & right.brand_tokens):
return False
pair_text = f"{left.searchable_name} {right.searchable_name}"
if not any(term in pair_text for term in ("香氛蠟燭暖燈", "蠟燭暖燈", "融蠟燈")):
return False
concrete_design_terms = ("大理石", "雲石", "原木", "半圓罩")
left_designs = {term for term in concrete_design_terms if term in left.searchable_name}
right_designs = {term for term in concrete_design_terms if term in right.searchable_name}
return bool(left_designs or right_designs) and left_designs != right_designs
def _standalone_size_letter_tokens(identity: ProductIdentity) -> set[str]:
text = identity.searchable_name
return {
@@ -3860,6 +3961,16 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
and _has_shared_weight(left, right, 40)
):
return "ts6_private_elastic_gel_40g"
if (
{"ts6", "護一生"} & (left.brand_tokens & right.brand_tokens)
and "超美" in left_text
and "超美" in right_text
and "香氛誘霜" in left_text
and "香氛誘霜" in right_text
and (120.0 in set(left.weights_g) or 120.0 in set(left.volumes_ml))
and (120.0 in set(right.weights_g) or 120.0 in set(right.volumes_ml))
):
return "ts6_private_white_fragrance_cream_120"
if (
{"ts6", "護一生"} & (left.brand_tokens & right.brand_tokens)
and "淨白植感慕斯" in left_text
@@ -4037,6 +4148,24 @@ def _has_focused_low_score_exact_identity_line(left: ProductIdentity, right: Pro
and _has_shared_volume(left, right, 150)
):
return "derma_eco_skin_oil"
if (
{"derma", "丹麥德瑪"} & (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_exact_count_alignment(left, right)
):
return "derma_eco_skin_oil_2pack_review"
if (
{"修護保養"} & (left.brand_tokens & right.brand_tokens)
and "蝸牛特潤修護面膜" in left_text
and "蝸牛特潤修護面膜" in right_text
and _has_shared_count(left, right, 6, "")
):
return "w_repair_snail_mask_6pcs_review"
if (
{"yuskin", "悠斯晶"} & (left.brand_tokens & right.brand_tokens)
and "乳霜" in left_text