수집한 객실·이용 정보가 발행 화면에 연결되지 않던 경로를 보완하고, 숙소 소개와 지역 맛집 표시를 개선한다. - NOL 브라우저 수집 어댑터와 수집·반영 스크립트 추가 - 크롤링 fact 즉시 노출 및 직접 입력·정정값 보호 - 이용안내 항목별 구조화와 기존 표 연결, 원문 UI 비표시 - 군산 한일옥 고정 등록과 지역 맛집 탐색·보강 경로 추가 - 숙소 소개 요약, 히어로 문구, 지역 콘텐츠·목업 표시 개선 검증: 작업 트리 기준 site 타입·린트·빌드 및 안내 렌더링 테스트 통과, PC·모바일 화면 확인. 스테이징 diff 공백 검사 통과. 사용자 요청에 따라 현재 스테이징된 55개 파일만 포함하며 미스테이징 문서·테스트 등은 제외.
78 lines
3.9 KiB
Python
78 lines
3.9 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()
|
|
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
|