Created
February 10, 2026 06:58
-
-
Save ninejuan/951ff8282c43c252db4137945422cd21 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Crawl Starbucks Korea stores and print name/address/business hours.""" | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| import time | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from dataclasses import dataclass | |
| from datetime import datetime | |
| STORE_LIST_URL = "https://www.starbucks.co.kr/store/getStore.do" | |
| STORE_TIME_URL = "https://www.starbucks.co.kr/store/getStoreTime.do" | |
| STORE_SIDO_URL = "https://www.starbucks.co.kr/store/getSidoList.do" | |
| HEADERS = { | |
| "User-Agent": "Mozilla/5.0", | |
| "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", | |
| "Referer": "https://www.starbucks.co.kr/store/store_map.do", | |
| } | |
| WEEKDAY_LABELS = { | |
| "1": "일", | |
| "2": "월", | |
| "3": "화", | |
| "4": "수", | |
| "5": "목", | |
| "6": "금", | |
| "7": "토", | |
| } | |
| @dataclass(frozen=True) | |
| class Store: | |
| biz_code: str | |
| name: str | |
| address: str | |
| def post_json( | |
| url: str, payload: dict[str, str], retries: int = 3, timeout: int = 20 | |
| ) -> dict: | |
| encoded = urllib.parse.urlencode(payload).encode("utf-8") | |
| for attempt in range(1, retries + 1): | |
| request = urllib.request.Request( | |
| url, data=encoded, method="POST", headers=HEADERS | |
| ) | |
| try: | |
| with urllib.request.urlopen(request, timeout=timeout) as response: | |
| return json.loads(response.read().decode("utf-8")) | |
| except (urllib.error.URLError, TimeoutError, json.JSONDecodeError): | |
| if attempt == retries: | |
| raise | |
| time.sleep(0.4 * attempt) | |
| raise RuntimeError("unreachable") | |
| def format_time_range(raw: str | None, holiday_tag: str | None) -> str: | |
| if holiday_tag == "4": | |
| return "휴점" | |
| if not raw or "-" not in raw: | |
| return "정보없음" | |
| start, end = raw.split("-", maxsplit=1) | |
| start = start.strip() | |
| end = end.strip() | |
| if len(start) < 4 or len(end) < 4: | |
| return "정보없음" | |
| return f"{start[:2]}:{start[2:4]}-{end[:2]}:{end[2:4]}" | |
| def get_sido_map() -> dict[str, str]: | |
| response = post_json(STORE_SIDO_URL, {}) | |
| return { | |
| str(row.get("sido_nm", "")).strip(): str(row.get("sido_cd", "")).strip() | |
| for row in response.get("list", []) | |
| if str(row.get("sido_nm", "")).strip() and str(row.get("sido_cd", "")).strip() | |
| } | |
| def ask_region_code(sido_map: dict[str, str]) -> tuple[str, str]: | |
| regions = sorted(sido_map.keys()) | |
| print("조회할 지역(시/도)을 입력하세요.") | |
| print("가능 지역: " + ", ".join(regions)) | |
| normalized_map = {"".join(name.split()): name for name in regions} | |
| while True: | |
| user_input = input("지역명: ").strip() | |
| normalized = "".join(user_input.split()) | |
| region_name = normalized_map.get(normalized) | |
| if region_name: | |
| return region_name, sido_map[region_name] | |
| print("유효하지 않은 지역입니다. 위 목록 중 하나를 다시 입력해 주세요.") | |
| def get_all_stores(sido_code: str) -> list[Store]: | |
| payload = { | |
| "ins_lat": "37.56682", | |
| "ins_lng": "126.97865", | |
| "search_text": "", | |
| "p_sido_cd": sido_code, | |
| "p_gugun_cd": "", | |
| "in_distance": "0", | |
| "in_biz_cd": "", | |
| "iend": "1000", | |
| "searchType": "A", | |
| "in_biz_cds": "0", | |
| "in_scodes": "0", | |
| "set_date": "", | |
| "T03": "0", | |
| "T01": "0", | |
| "T12": "0", | |
| "T09": "0", | |
| "T06": "0", | |
| "T10": "0", | |
| "P10": "0", | |
| "P50": "0", | |
| "P20": "0", | |
| "P60": "0", | |
| "P30": "0", | |
| "P70": "0", | |
| "P40": "0", | |
| "P80": "0", | |
| "T20": "0", | |
| "T22": "0", | |
| "T29": "0", | |
| "T30": "0", | |
| "T36": "0", | |
| "T43": "0", | |
| "T48": "0", | |
| "new_bool": "0", | |
| "T64": "0", | |
| "T66": "0", | |
| } | |
| response = post_json(STORE_LIST_URL, payload) | |
| stores: list[Store] = [] | |
| seen: set[str] = set() | |
| for row in response.get("list", []): | |
| biz_code = str(row.get("s_biz_code", "")).strip() | |
| if not biz_code or biz_code in seen: | |
| continue | |
| seen.add(biz_code) | |
| doro_address = str(row.get("doro_address", "")).strip() | |
| address = doro_address or str(row.get("addr", "")).strip() | |
| stores.append( | |
| Store( | |
| biz_code=biz_code, | |
| name=str(row.get("s_name", "")).strip(), | |
| address=address, | |
| ) | |
| ) | |
| return stores | |
| def get_today_week_code() -> str: | |
| weekday = datetime.now().weekday() | |
| return str(((weekday + 1) % 7) + 1) | |
| def get_today_store_hours(biz_code: str) -> str: | |
| payload = { | |
| "in_biz_cd": biz_code, | |
| "in_store_type": "C", | |
| } | |
| response = post_json(STORE_TIME_URL, payload) | |
| rows = response.get("list", []) | |
| if not rows: | |
| return "정보없음" | |
| today_code = get_today_week_code() | |
| today_row = next( | |
| ( | |
| row | |
| for row in rows | |
| if str(row.get("store_time_week", "")).strip() == today_code | |
| ), | |
| rows[0], | |
| ) | |
| return format_time_range( | |
| str(today_row.get("store_opentime", "")).strip() | |
| if today_row.get("store_opentime") is not None | |
| else None, | |
| str(today_row.get("store_time_hlytag", "")).strip() | |
| if today_row.get("store_time_hlytag") is not None | |
| else None, | |
| ) | |
| def main() -> int: | |
| try: | |
| sido_map = get_sido_map() | |
| region_name, region_code = ask_region_code(sido_map) | |
| stores = get_all_stores(region_code) | |
| except Exception as error: # noqa: BLE001 | |
| print(f"매장 목록 조회 실패: {error}", file=sys.stderr) | |
| return 1 | |
| if not stores: | |
| print(f"{region_name} 지역 매장을 찾지 못했습니다.", file=sys.stderr) | |
| return 1 | |
| rows: list[tuple[str, str, str]] = [] | |
| with ThreadPoolExecutor(max_workers=24) as executor: | |
| future_map = { | |
| executor.submit(get_today_store_hours, store.biz_code): store | |
| for store in stores | |
| } | |
| for future in as_completed(future_map): | |
| store = future_map[future] | |
| try: | |
| hours = future.result() | |
| except Exception: | |
| hours = "정보없음" | |
| rows.append((store.name, store.address, hours)) | |
| rows.sort(key=lambda row: row[0]) | |
| today_label = WEEKDAY_LABELS.get(get_today_week_code(), "오늘") | |
| print(f"조회 지역: {region_name}") | |
| print(f"매장명\t주소\t{today_label} 영업시간") | |
| for name, address, hours in rows: | |
| print(f"{name}\t{address}\t{hours}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment