[버그수정]

This commit is contained in:
LASTA_DEV01\lasta
2026-05-11 13:02:11 +09:00
parent 5935c6cbe8
commit 3663734781
13 changed files with 686 additions and 135 deletions

View File

@@ -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