"""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:
"""