[버그수정]
This commit is contained in:
@@ -86,6 +86,30 @@ class PerfumeRuleBasedExtractor(GenericRuleBasedExtractor):
|
||||
name = "perfume_rule_based"
|
||||
|
||||
def extract_entities(self, page_text: str, project_config: ProjectConfig) -> list[ExtractedEntity]:
|
||||
product_cards = extract_product_cards(page_text)
|
||||
if product_cards:
|
||||
brand = valid_brand(extract_brand(page_text, "")) or infer_site_brand(page_text)
|
||||
entities: list[ExtractedEntity] = []
|
||||
if brand:
|
||||
entities.append(ExtractedEntity("Brand", brand, confidence=0.62))
|
||||
for card in product_cards:
|
||||
attrs: dict[str, object] = {"name": card["name"]}
|
||||
if brand:
|
||||
attrs["brand"] = brand
|
||||
if card.get("price"):
|
||||
attrs["price"] = card["price"]
|
||||
entities.append(
|
||||
ExtractedEntity(
|
||||
"Perfume",
|
||||
str(card["name"]),
|
||||
attrs,
|
||||
evidence_text=str(card.get("evidence") or card["name"]),
|
||||
confidence=0.74,
|
||||
metadata={"page_pattern": "product_listing"},
|
||||
)
|
||||
)
|
||||
return dedupe_entities(entities)
|
||||
|
||||
product_name = extract_product_name(page_text)
|
||||
attrs = {"name": product_name}
|
||||
brand = extract_brand(page_text, product_name)
|
||||
@@ -137,6 +161,38 @@ class PerfumeRuleBasedExtractor(GenericRuleBasedExtractor):
|
||||
page_text: str,
|
||||
project_config: ProjectConfig,
|
||||
) -> list[ExtractedClaim]:
|
||||
product_cards = extract_product_cards(page_text)
|
||||
if product_cards:
|
||||
claims: list[ExtractedClaim] = []
|
||||
brand = next((entity for entity in entities if entity.entity_type == "Brand"), None)
|
||||
for card in product_cards:
|
||||
if brand:
|
||||
claims.append(
|
||||
ExtractedClaim(
|
||||
str(card["name"]),
|
||||
"Perfume",
|
||||
"hasBrand",
|
||||
brand.name,
|
||||
"Brand",
|
||||
evidence_text=brand.evidence_text or brand.name,
|
||||
confidence=0.72,
|
||||
confidence_reason="site brand inferred from listing page",
|
||||
)
|
||||
)
|
||||
if card.get("price"):
|
||||
claims.append(
|
||||
ExtractedClaim(
|
||||
str(card["name"]),
|
||||
"Perfume",
|
||||
"hasPrice",
|
||||
object_value=card["price"],
|
||||
evidence_text=str(card.get("evidence") or card["name"]),
|
||||
confidence=0.76,
|
||||
confidence_reason="Korean product listing price pattern matched",
|
||||
)
|
||||
)
|
||||
return claims
|
||||
|
||||
perfume = next((entity for entity in entities if entity.entity_type == "Perfume"), None)
|
||||
if perfume is None:
|
||||
return []
|
||||
@@ -195,7 +251,7 @@ class PerfumeRuleBasedExtractor(GenericRuleBasedExtractor):
|
||||
def extract_product_name(page_text: str) -> str:
|
||||
for line in page_text.splitlines()[:8]:
|
||||
clean = line.strip()
|
||||
if clean and not looks_like_navigation(clean):
|
||||
if clean and not looks_like_navigation(clean) and not is_template_placeholder(clean):
|
||||
return clean[:240]
|
||||
return first_non_empty_line(page_text) or "Unknown Perfume"
|
||||
|
||||
@@ -217,6 +273,95 @@ def extract_brand(page_text: str, product_name: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def extract_product_cards(page_text: str) -> list[dict[str, object]]:
|
||||
lines = [line.strip() for line in page_text.splitlines() if line.strip()]
|
||||
cards: list[dict[str, object]] = []
|
||||
idx = 0
|
||||
while idx < len(lines):
|
||||
if lines[idx] != "상품명":
|
||||
idx += 1
|
||||
continue
|
||||
name, name_idx = next_value_after_label(lines, idx)
|
||||
if not name or is_template_placeholder(name) or name in {":", "상품명"}:
|
||||
idx += 1
|
||||
continue
|
||||
card: dict[str, object] = {"name": cleanup_value(name), "evidence": f"상품명: {name}"}
|
||||
scan_end = next_label_index(lines, "상품명", name_idx + 1) or min(len(lines), name_idx + 12)
|
||||
for price_label in ("할인판매가", "판매가", "price", "Price"):
|
||||
label_idx = find_label_index(lines, price_label, name_idx + 1, scan_end)
|
||||
if label_idx is None:
|
||||
continue
|
||||
raw_price, _price_idx = next_value_after_label(lines, label_idx)
|
||||
parsed = parse_price_value(raw_price)
|
||||
if parsed:
|
||||
card["price"] = parsed
|
||||
card["evidence"] = f"{card['evidence']} / {price_label}: {raw_price}"
|
||||
break
|
||||
cards.append(card)
|
||||
idx = scan_end
|
||||
return dedupe_product_cards(cards)
|
||||
|
||||
|
||||
def next_value_after_label(lines: list[str], label_idx: int) -> tuple[str | None, int]:
|
||||
for idx in range(label_idx + 1, min(len(lines), label_idx + 5)):
|
||||
value = cleanup_value(lines[idx])
|
||||
if not value or value == ":":
|
||||
continue
|
||||
return value, idx
|
||||
return None, label_idx
|
||||
|
||||
|
||||
def next_label_index(lines: list[str], label: str, start: int) -> int | None:
|
||||
for idx in range(start, len(lines)):
|
||||
if lines[idx] == label:
|
||||
return idx
|
||||
return None
|
||||
|
||||
|
||||
def find_label_index(lines: list[str], label: str, start: int, end: int) -> int | None:
|
||||
lower_label = label.lower()
|
||||
for idx in range(start, min(end, len(lines))):
|
||||
if lines[idx].lower() == lower_label:
|
||||
return idx
|
||||
return None
|
||||
|
||||
|
||||
def parse_price_value(raw_price: str | None) -> dict[str, object] | None:
|
||||
if not raw_price:
|
||||
return None
|
||||
match = re.search(r"(?P<amount>\d{1,3}(?:,\d{3})*|\d+)\s*(?P<currency>원|KRW|₩|USD|\$)?", raw_price)
|
||||
if not match:
|
||||
return None
|
||||
currency = match.group("currency") or "KRW"
|
||||
if currency in {"원", "₩"}:
|
||||
currency = "KRW"
|
||||
return {"amount": float(match.group("amount").replace(",", "")), "currency": currency}
|
||||
|
||||
|
||||
def dedupe_product_cards(cards: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||
seen: set[str] = set()
|
||||
result: list[dict[str, object]] = []
|
||||
for card in cards:
|
||||
key = str(card["name"]).strip().lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
result.append(card)
|
||||
return result
|
||||
|
||||
|
||||
def infer_site_brand(page_text: str) -> str | None:
|
||||
if "912 공식 홈페이지" in page_text or "912" in page_text[:500]:
|
||||
return "912"
|
||||
return None
|
||||
|
||||
|
||||
def valid_brand(value: str | None) -> str | None:
|
||||
if not value or is_template_placeholder(value):
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def extract_field_values(field: str, page_text: str) -> list[tuple[str, str]]:
|
||||
if field in NOTE_LABELS:
|
||||
return extract_labeled_values(page_text, NOTE_LABELS[field])
|
||||
@@ -292,6 +437,15 @@ def looks_like_navigation(value: str) -> bool:
|
||||
return value.lower() in {"home", "shop", "menu", "cart", "login", "검색", "장바구니", "홈"}
|
||||
|
||||
|
||||
def is_template_placeholder(value: str) -> bool:
|
||||
clean = value.strip()
|
||||
return clean.startswith("{#") or clean.endswith("}") or clean in {
|
||||
"CLONE FRAGRANCE",
|
||||
"NICHE FRAGRANCE",
|
||||
"HOME FRAGRANCE",
|
||||
}
|
||||
|
||||
|
||||
def field_entity_type(field: str) -> str:
|
||||
return {
|
||||
"top_notes": "Note",
|
||||
@@ -347,4 +501,3 @@ def dedupe_entities(entities: list[ExtractedEntity]) -> list[ExtractedEntity]:
|
||||
seen.add(key)
|
||||
result.append(entity)
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user