"""collect 단계 - HTML / siteLinks 에서 tracking pixels / SNS links / additional domains 추출. 모두 정규식·도메인 매칭 기반 deterministic 추출 (LLM 미경유).""" import re from urllib.parse import urlparse # ── tracking pixels ────────────────────────────────────────────────────────── # 픽셀별 시그니처: 이름 → 정규식 리스트. installed 판정은 OR (하나라도 매치되면 설치된 것). # ID 캡처는 모든 패턴 시도 후 첫 non-empty 그룹 사용 (signature-only 패턴과 ID-capture 패턴 혼재 OK). _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 추출. 환각 0 — 정규식 매치만 신뢰. 모든 패턴 시도 → 하나라도 매치되면 installed. ID 는 캡처된 첫 non-empty 그룹 사용.""" 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, ) _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() def extract_main_cta(html: str) -> str: """HTML 에서 primary CTA 텍스트 추출. 우선순위: