공식채널 단일화, 한일옥 거리 반영 날씨 조건을 7종으로 세분화, 축제 종료 여부와 무관하게 상시 노출, '지역 읽기'갈래 축소, 야놀자(NOL) 브랜드명 제거.
81 lines
4.2 KiB
Python
81 lines
4.2 KiB
Python
"""확정된 NOL 링크의 수집 원문에서 공개 안내 섹션만 전달한다."""
|
|
import re
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
def nol_stay_guide(url: str, raw) -> dict:
|
|
try:
|
|
parsed = urlsplit(url)
|
|
except ValueError:
|
|
return {}
|
|
if parsed.scheme != "https" or parsed.hostname != "nol.yanolja.com":
|
|
return {}
|
|
if not re.fullmatch(r"/stay/domestic/\d+/?", parsed.path):
|
|
return {}
|
|
text = raw.get("text") if isinstance(raw, dict) else None
|
|
if not isinstance(text, str):
|
|
return {}
|
|
|
|
labels = {"시설/서비스": "service", "이용 안내": "policy", "예약 공지": "reservation"}
|
|
# 수집기가 붙인 경계로만 나눈다. 규정 안의 괄호나 문장은 분리하지 않는다.
|
|
sections = re.split(r"(?m)^\[(숙소 소개|시설/서비스|이용 안내|예약 공지)\]\s*\n", text)
|
|
guide = {}
|
|
for label, body in zip(sections[1::2], sections[2::2]):
|
|
if label not in labels:
|
|
continue
|
|
lines = body.strip().splitlines()
|
|
if lines and lines[0].strip() == label:
|
|
lines.pop(0)
|
|
# 제목 중복과 UI 버튼만 제외한다. 요약·추론 없이 수집 문장을 보존한다.
|
|
body = "\n".join(line for line in lines if line.strip() != "전체보기").strip()
|
|
# NOL 은 원 플랫폼 브랜드다 — 사장님 사이트에 그대로 실으면 남의 고객센터·정책을
|
|
# 우리 것처럼 안내하게 된다. 문장 자체는 그대로 두고 그 단어만 지운다.
|
|
body = re.sub(r"NOL\s*", "", body)
|
|
if body:
|
|
guide[labels[label]] = body
|
|
if guide:
|
|
guide["fields"] = structured_fields(guide)
|
|
return guide
|
|
|
|
|
|
def structured_fields(guide: dict) -> list[dict]:
|
|
"""확실한 표기만 구조화한다. 매칭되지 않은 내용은 안내 원문에 남는다."""
|
|
policy = guide.get("policy", "")
|
|
service = guide.get("service", "")
|
|
reservation = guide.get("reservation", "")
|
|
lines = {line.strip().lstrip("- ") for line in (policy + "\n" + reservation).splitlines()}
|
|
facilities = {line.strip() for line in service.splitlines()}
|
|
fields = []
|
|
|
|
def add(key, label, value, group="rules", note=None):
|
|
fields.append(dict(key=key, label=label, value=value, group=group,
|
|
**({"note": note} if note else {})))
|
|
|
|
for token, key, label in (("체크인", "check_in_time", "체크인 시간"),
|
|
("체크아웃", "check_out_time", "체크아웃 시간")):
|
|
matches = set(re.findall(rf"{token}\s+([0-2]\d:[0-5]\d)(?!\d)", policy))
|
|
if len(matches) == 1:
|
|
value = matches.pop()
|
|
if int(value[:2]) < 24:
|
|
add(key, label, value)
|
|
fees = [re.fullmatch(r"전 연령 동일 1인당\s*([\d,]+)\s*(만)?원", line) for line in lines]
|
|
amounts = {int(m[1].replace(",", "")) * (10000 if m[2] else 1) for m in fees if m}
|
|
if len(amounts) == 1:
|
|
add("extra_person_fee", "인원 추가 요금", f"{amounts.pop():,}원", note="1인당 · 전 연령 동일")
|
|
if "반려동물 입실금지" in lines and not any("반려동물 입실가능" == line for line in lines):
|
|
add("pet_allowed", "반려동물 동반", "불가")
|
|
if "전 구역 금연" in lines:
|
|
add("smoking", "흡연", "불가", "facilities", "전 구역 금연")
|
|
for token, key, label in (("주차가능", "parking", "주차"), ("와이파이", "wifi", "와이파이"),
|
|
("취사가능", "cooking_allowed", "취사")):
|
|
if token in facilities:
|
|
restrictions = [line.strip().lstrip("- ") for line in reservation.splitlines()
|
|
if "조리금지" in line or "조리 금지" in line]
|
|
add(key, label, "가능", "facilities",
|
|
"\n".join(restrictions) if key == "cooking_allowed" and restrictions else None)
|
|
known = [name for name in ("욕조", "개별 화장실", "주방", "테라스/발코니", "OTT (스트리밍 서비스)",
|
|
"다이닝룸", "벽난로", "어메니티", "카페형룸") if name in facilities]
|
|
if known:
|
|
add("facilities", "부대시설", ", ".join(known), "facilities")
|
|
return fields
|