"""HTML 정규식 기반 deterministic 추출 — tracking pixels / SNS / additional domains / main CTA.""" import re from urllib.parse import urlparse # ── tracking pixels ────────────────────────────────────────────────────────── # 픽셀별 시그니처: 하나라도 매치되면 installed, ID 는 첫 non-empty 그룹. _TRACKING_PIXEL_PATTERNS: dict[str, list[re.Pattern]] = { "Google Analytics": [ re.compile(r"googletagmanager\.com/gtag/js\?id=(G-[A-Z0-9]+)", re.IGNORECASE), re.compile(r"google-analytics\.com/analytics\.js", re.IGNORECASE), re.compile(r"\bgtag\(\s*['\"]config['\"]\s*,\s*['\"](G-[A-Z0-9]+|UA-\d+-\d+)['\"]", re.IGNORECASE), re.compile(r"\b(UA-\d+-\d+)\b"), ], "Google Tag Manager": [ re.compile(r"googletagmanager\.com/gtm\.js\?id=(GTM-[A-Z0-9]+)", re.IGNORECASE), re.compile(r"\b(GTM-[A-Z0-9]+)\b"), ], "Facebook Pixel": [ re.compile(r"connect\.facebook\.net/[^/]+/fbevents\.js", re.IGNORECASE), re.compile(r"fbq\(\s*['\"]init['\"]\s*,\s*['\"](\d{10,20})['\"]", re.IGNORECASE), re.compile(r"facebook\.com/tr/?\?id=(\d{10,20})", re.IGNORECASE), # noscript image pixel fallback re.compile(r"_fbq\.push\(\s*\[\s*['\"]init['\"]\s*,\s*['\"](\d{10,20})['\"]", re.IGNORECASE), # legacy ], "Naver Analytics": [ re.compile(r"wcs\.naver\.net/wcslog\.js", re.IGNORECASE), re.compile(r"\bwcs_add\b", re.IGNORECASE), re.compile(r"\bwcs\.inflow\(", re.IGNORECASE), re.compile(r"wcs_add\s*\[\s*['\"]wa['\"]\s*\]\s*=\s*['\"]([a-zA-Z0-9_]+)['\"]", re.IGNORECASE), # wa ID ], "Kakao Pixel": [ re.compile(r"t1\.daumcdn\.net/kas/static/kp\.js", re.IGNORECASE), re.compile(r"kakaoPixel\s*\(\s*['\"]?(\d+)['\"]?", re.IGNORECASE), ], } def extract_tracking_pixels(html: str) -> list[dict]: """HTML 에서 트래킹 픽셀 설치 여부 + ID 추출.""" if not html: return [] pixels: list[dict] = [] for name, patterns in _TRACKING_PIXEL_PATTERNS.items(): any_match = False captured_id: str | None = None for pat in patterns: m = pat.search(html) if not m: continue any_match = True if m.groups() and m.group(1) and not captured_id: captured_id = m.group(1) if any_match: pixels.append({ "name": name, "installed": True, "details": captured_id, }) return pixels # ── main CTA ───────────────────────────────────────────────────────────────── _CTA_KEYWORDS = re.compile( r"(상담|예약|문의|신청|등록|Book\s*Now|Consult|Reservation|Contact|Apply)", re.IGNORECASE, ) # CTA 카테고리 우선순위 (전화>카톡>예약). 매치 없으면 결과에 포함 X. _CTA_CATEGORIES: list[tuple[str, re.Pattern]] = [ ("전화 상담", re.compile(r"전화|\bCall\b|\bPhone\b", re.IGNORECASE)), ("카카오톡 상담", re.compile(r"카(카오)?톡|Kakao", re.IGNORECASE)), ("온라인 예약", re.compile(r"예약|Reserv|Book", re.IGNORECASE)), ] _BUTTON_TAG = re.compile(r"]*>(.*?)", re.IGNORECASE | re.DOTALL) _ANCHOR_TAG = re.compile(r"]*>(.*?)", re.IGNORECASE | re.DOTALL) def _clean_text(inner: str) -> str: text = re.sub(r"<[^>]+>", "", inner) return re.sub(r"\s+", " ", text).strip() _MAIN_CTA_LIMIT = 3 def _classify_cta(text: str) -> str | None: """CTA 텍스트 → 카테고리 (전화/카톡/예약). 매치 없으면 None.""" for label, pat in _CTA_CATEGORIES: if pat.search(text): return label return None def extract_main_cta(html: str) -> str: """