{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "md-intro",
   "metadata": {},
   "source": [
    "# 해운대구 한식 (영업연수 10년 이상) 카카오맵 리뷰 크롤링\n",
    "\n",
    "네이버 지도 크롤링(`해운대구_한식_네이버리뷰_크롤링.ipynb`)은 Selenium으로 브라우저를 직접 조작해야 하고, 반복 요청 시 캡차에 걸리는 문제가 있었다.\n",
    "카카오맵은 실제 브라우저 요청을 분석해본 결과, 아래 두 개의 **공식/내부 API**를 그대로 호출하는 것만으로 리뷰를 가져올 수 있어 브라우저(Selenium) 없이 `requests`만으로 훨씬 빠르고 안정적으로 크롤링할 수 있다.\n",
    "\n",
    "1. **장소 검색**: 카카오 로컬 REST API(`dapi.kakao.com`, 공식 문서화된 API, API 키 필요) → 매장의 카카오맵 `place_id`를 얻는다.\n",
    "2. **리뷰 조회**: `place-api.map.kakao.com`의 내부 API → `place_id`로 리뷰 목록을 페이지네이션하며 가져온다. (비공식 API이므로 카카오 쪽에서 언제든 바뀔 수 있음)\n",
    "\n",
    "> 카카오맵은 일반적으로 네이버 지도보다 리뷰 개수가 적은 매장이 많다(특히 오래된/작은 식당). 매장 매칭 실패나 리뷰 0건인 경우도 실패 목록에 기록해 둔다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "cell-install",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Requirement already satisfied: requests in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (2.32.5)\n",
      "Requirement already satisfied: pandas in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (2.3.3)\n",
      "Requirement already satisfied: tqdm in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (4.67.1)\n",
      "Requirement already satisfied: charset_normalizer<4,>=2 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from requests) (3.4.4)\n",
      "Requirement already satisfied: idna<4,>=2.5 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from requests) (3.11)\n",
      "Requirement already satisfied: urllib3<3,>=1.21.1 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from requests) (2.7.0)\n",
      "Requirement already satisfied: certifi>=2017.4.17 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from requests) (2026.6.17)\n",
      "Requirement already satisfied: numpy>=1.26.0 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from pandas) (2.3.5)\n",
      "Requirement already satisfied: python-dateutil>=2.8.2 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from pandas) (2.9.0.post0)\n",
      "Requirement already satisfied: pytz>=2020.1 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from pandas) (2025.2)\n",
      "Requirement already satisfied: tzdata>=2022.7 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from pandas) (2025.2)\n",
      "Requirement already satisfied: colorama in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from tqdm) (0.4.6)\n",
      "Requirement already satisfied: six>=1.5 in C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\n",
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "%pip install requests pandas tqdm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "cell-imports",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages\\numpy\\_core\\getlimits.py:552: UserWarning: Signature b'f\\xae' for <class 'numpy.float16'> does not match any known type: falling back to type probe function.\n",
      "This warnings indicates broken support for the dtype!\n",
      "  machar = _get_machar(dtype)\n",
      "C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages\\numpy\\_core\\getlimits.py:552: UserWarning: Signature b'\\xcd\\xcc\\xcc\\xbd' for <class 'numpy.float32'> does not match any known type: falling back to type probe function.\n",
      "This warnings indicates broken support for the dtype!\n",
      "  machar = _get_machar(dtype)\n",
      "C:\\Users\\ddr77\\anaconda3\\Lib\\site-packages\\numpy\\_core\\getlimits.py:552: UserWarning: Signature b'\\x9a\\x99\\x99\\x99\\x99\\x99\\xb9\\xbf' for <class 'numpy.float64'> does not match any known type: falling back to type probe function.\n",
      "This warnings indicates broken support for the dtype!\n",
      "  machar = _get_machar(dtype)\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "import requests\n",
    "import pandas as pd\n",
    "from tqdm.notebook import tqdm"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md-filter",
   "metadata": {},
   "source": [
    "## 1. CSV 로드 및 조건 필터링\n",
    "네이버 노트북과 동일한 조건(해운대구 + 한식 + 영업연수 10년 이상)으로 필터링한다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "cell-filter",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "필터링 결과: 791 개 매장\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>관리번호</th>\n",
       "      <th>사업장명</th>\n",
       "      <th>업태구분명</th>\n",
       "      <th>구</th>\n",
       "      <th>도로명주소</th>\n",
       "      <th>지번주소</th>\n",
       "      <th>좌표X</th>\n",
       "      <th>좌표Y</th>\n",
       "      <th>전화번호</th>\n",
       "      <th>인허가일자</th>\n",
       "      <th>영업연수</th>\n",
       "      <th>시설총규모</th>\n",
       "      <th>다중이용업소여부</th>\n",
       "      <th>등급구분명</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>3330000-101-2010-00230</td>\n",
       "      <td>부산미가돼지국밥</td>\n",
       "      <td>한식</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)</td>\n",
       "      <td>부산광역시 해운대구 우동 693-3 2층</td>\n",
       "      <td>396204.616661</td>\n",
       "      <td>186699.183519</td>\n",
       "      <td>051-731-0814</td>\n",
       "      <td>2010-08-12</td>\n",
       "      <td>16.0</td>\n",
       "      <td>113.89</td>\n",
       "      <td>Y</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>3330000-101-2008-00324</td>\n",
       "      <td>바탄(VATAN)</td>\n",
       "      <td>한식</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 아랫반송로59번길 9 (반송동)</td>\n",
       "      <td>부산광역시 해운대구 반송동 250-849 1층 전체</td>\n",
       "      <td>395786.749546</td>\n",
       "      <td>193632.138701</td>\n",
       "      <td>NaN</td>\n",
       "      <td>2008-09-18</td>\n",
       "      <td>17.9</td>\n",
       "      <td>41.50</td>\n",
       "      <td>N</td>\n",
       "      <td>자율</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3330000-101-2011-00269</td>\n",
       "      <td>바오밥나무</td>\n",
       "      <td>한식</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 우동1로20번가길 58 (우동, 지상1층 )</td>\n",
       "      <td>부산광역시 해운대구 우동 398-20</td>\n",
       "      <td>396553.710538</td>\n",
       "      <td>187483.189607</td>\n",
       "      <td>051-747-9099</td>\n",
       "      <td>2011-09-23</td>\n",
       "      <td>14.8</td>\n",
       "      <td>16.56</td>\n",
       "      <td>N</td>\n",
       "      <td>자율</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>3330000-101-2009-00167</td>\n",
       "      <td>주식회사 큰사람유통 24시부산국밥</td>\n",
       "      <td>한식</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 해운대로 765 (중동, 지상1층)</td>\n",
       "      <td>부산광역시 해운대구 중동 284-3 지상1층</td>\n",
       "      <td>397905.177284</td>\n",
       "      <td>187556.691082</td>\n",
       "      <td>051-746-2188</td>\n",
       "      <td>2009-05-20</td>\n",
       "      <td>17.2</td>\n",
       "      <td>291.83</td>\n",
       "      <td>N</td>\n",
       "      <td>자율</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3330000-101-2015-00388</td>\n",
       "      <td>제주옥탑</td>\n",
       "      <td>한식</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 구남로 15 (우동, 2층)</td>\n",
       "      <td>부산광역시 해운대구 우동 618-13 2층</td>\n",
       "      <td>396737.606485</td>\n",
       "      <td>186960.908335</td>\n",
       "      <td>NaN</td>\n",
       "      <td>2015-12-09</td>\n",
       "      <td>10.6</td>\n",
       "      <td>112.40</td>\n",
       "      <td>N</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                     관리번호                사업장명 업태구분명     구  \\\n",
       "0  3330000-101-2010-00230            부산미가돼지국밥    한식  해운대구   \n",
       "1  3330000-101-2008-00324           바탄(VATAN)    한식  해운대구   \n",
       "2  3330000-101-2011-00269               바오밥나무    한식  해운대구   \n",
       "3  3330000-101-2009-00167  주식회사 큰사람유통 24시부산국밥    한식  해운대구   \n",
       "4  3330000-101-2015-00388                제주옥탑    한식  해운대구   \n",
       "\n",
       "                                  도로명주소                           지번주소  \\\n",
       "0  부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)        부산광역시 해운대구 우동 693-3 2층    \n",
       "1          부산광역시 해운대구 아랫반송로59번길 9 (반송동)  부산광역시 해운대구 반송동 250-849 1층 전체    \n",
       "2   부산광역시 해운대구 우동1로20번가길 58 (우동, 지상1층 )          부산광역시 해운대구 우동 398-20    \n",
       "3        부산광역시 해운대구 해운대로 765 (중동, 지상1층)      부산광역시 해운대구 중동 284-3 지상1층    \n",
       "4            부산광역시 해운대구 구남로 15 (우동, 2층)       부산광역시 해운대구 우동 618-13 2층    \n",
       "\n",
       "             좌표X            좌표Y          전화번호       인허가일자  영업연수   시설총규모  \\\n",
       "0  396204.616661  186699.183519  051-731-0814  2010-08-12  16.0  113.89   \n",
       "1  395786.749546  193632.138701           NaN  2008-09-18  17.9   41.50   \n",
       "2  396553.710538  187483.189607  051-747-9099  2011-09-23  14.8   16.56   \n",
       "3  397905.177284  187556.691082  051-746-2188  2009-05-20  17.2  291.83   \n",
       "4  396737.606485  186960.908335           NaN  2015-12-09  10.6  112.40   \n",
       "\n",
       "  다중이용업소여부 등급구분명  \n",
       "0        Y   NaN  \n",
       "1        N    자율  \n",
       "2        N    자율  \n",
       "3        N    자율  \n",
       "4        N   NaN  "
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.read_csv(\"부산_일반음식점_전처리.csv\", encoding=\"utf-8-sig\")\n",
    "\n",
    "filtered = df[\n",
    "    (df[\"구\"] == \"해운대구\")\n",
    "    & (df[\"업태구분명\"] == \"한식\")\n",
    "    & (df[\"영업연수\"] >= 10)\n",
    "].reset_index(drop=True)\n",
    "\n",
    "print(\"필터링 결과:\", len(filtered), \"개 매장\")\n",
    "\n",
    "FILTERED_CSV = \"haeundae_hansik_10y.csv\"\n",
    "filtered.to_csv(FILTERED_CSV, index=False, encoding=\"utf-8-sig\")\n",
    "filtered.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md-config",
   "metadata": {},
   "source": [
    "## 2. API 설정\n",
    "`REST_API_KEY`는 [카카오 디벨로퍼스](https://developers.kakao.com)에서 발급받은 REST API 키를 사용한다.\n",
    "리뷰 조회용 헤더(`pf`, `appversion`)는 실제 브라우저가 보내는 요청을 분석해 확인한 값으로,\n",
    "이 값이 없으면 카카오 서버가 `406 Not Acceptable`을 반환한다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "cell-config",
   "metadata": {},
   "outputs": [],
   "source": [
    "REST_API_KEY = \"REMOVED_API_KEY\"  # 카카오 디벨로퍼스에서 발급받은 본인 REST API 키로 교체 가능\n",
    "\n",
    "SEARCH_URL = \"https://dapi.kakao.com/v2/local/search/keyword.json\"\n",
    "REVIEW_URL_TMPL = \"https://place-api.map.kakao.com/places/tab/reviews/kakaomap/{}\"\n",
    "\n",
    "SEARCH_HEADERS = {\"Authorization\": f\"KakaoAK {REST_API_KEY}\"}\n",
    "REVIEW_HEADERS = {\n",
    "    \"User-Agent\": (\n",
    "        \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \"\n",
    "        \"(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\"\n",
    "    ),\n",
    "    \"Referer\": \"https://place.map.kakao.com/\",\n",
    "    \"Accept\": \"application/json, text/plain, */*\",\n",
    "    \"pf\": \"PC\",\n",
    "    \"appversion\": \"6.6.0\",\n",
    "}\n",
    "\n",
    "MAX_STORES = None   # 예: 10 으로 두면 테스트용으로 10개 매장만 크롤링\n",
    "REQUEST_DELAY = 0.5  # 매장 간 대기시간(초). 공식/내부 API라 네이버보다 훨씬 짧게 잡아도 된다\n",
    "CHECKPOINT_EVERY = 50\n",
    "\n",
    "REVIEW_CSV = \"kakao_reviews.csv\"\n",
    "REVIEW_CSV_TMP = \"kakao_reviews_checkpoint.csv\"\n",
    "FAIL_LOG_CSV = \"kakao_reviews_실패목록.csv\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md-search",
   "metadata": {},
   "source": [
    "## 3. 매장 검색 (카카오 로컬 REST API)\n",
    "여러 검색어 조합을 순서대로 시도해 매칭 성공률을 높인다. 검색 결과 중 상호명이 정확히 일치하는 것을 우선하고,\n",
    "없으면 첫 번째 결과를 사용한다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "cell-search",
   "metadata": {},
   "outputs": [],
   "source": [
    "def find_place_id(gu, dong, name):\n",
    "    \"\"\"카카오 로컬 검색 API로 매장을 찾아 place_id(카카오맵 confirm_id)를 반환한다.\"\"\"\n",
    "    queries = [f\"{gu} {dong} {name}\".strip(), f\"{gu} {name}\".strip(), name]\n",
    "    for query in dict.fromkeys(queries):  # 중복 제거하되 순서 유지\n",
    "        params = {\"query\": query, \"size\": 5}\n",
    "        res = requests.get(SEARCH_URL, headers=SEARCH_HEADERS, params=params, timeout=10)\n",
    "        res.raise_for_status()\n",
    "        docs = res.json().get(\"documents\", [])\n",
    "        if not docs:\n",
    "            continue\n",
    "        for d in docs:\n",
    "            if d.get(\"place_name\") == name:\n",
    "                return d.get(\"id\")\n",
    "        return docs[0].get(\"id\")\n",
    "    return None"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md-reviews",
   "metadata": {},
   "source": [
    "## 4. 리뷰 조회 (카카오맵 내부 API)\n",
    "`previous_last_review_id`를 이전 페이지의 마지막 review_id로 넘겨 다음 페이지를 가져오는 커서 방식 페이지네이션이다.\n",
    "`has_next`가 False가 되거나 더 이상 리뷰가 없으면 종료한다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "cell-reviews",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_all_reviews(place_id, max_pages=30):\n",
    "    rows = []\n",
    "    last_id = None\n",
    "    for _ in range(max_pages):\n",
    "        params = {\"order\": \"RECOMMENDED\", \"only_photo_review\": \"false\"}\n",
    "        if last_id is not None:\n",
    "            params[\"previous_last_review_id\"] = last_id\n",
    "\n",
    "        res = requests.get(\n",
    "            REVIEW_URL_TMPL.format(place_id), headers=REVIEW_HEADERS, params=params, timeout=10\n",
    "        )\n",
    "        if res.status_code != 200:\n",
    "            break\n",
    "\n",
    "        data = res.json()\n",
    "        reviews = data.get(\"reviews\", [])\n",
    "        if not reviews:\n",
    "            break\n",
    "        rows.extend(reviews)\n",
    "\n",
    "        if not data.get(\"has_next\"):\n",
    "            break\n",
    "        last_id = reviews[-1].get(\"review_id\")\n",
    "        time.sleep(0.3)\n",
    "\n",
    "    return rows"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md-main",
   "metadata": {},
   "source": [
    "## 5. 전체 매장 순회 크롤링"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "cell-main",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e836dd70bba048fa9a913e1908532259",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/791 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "수집한 리뷰 수: 15309건 / 실패(매칭없음·리뷰없음) 매장 수: 199곳\n"
     ]
    }
   ],
   "source": [
    "def extract_dong(gu, address):\n",
    "    if not isinstance(address, str):\n",
    "        return \"\"\n",
    "    import re\n",
    "    m = re.search(rf\"{re.escape(gu)}\\s+(\\S+동)\", address)\n",
    "    return m.group(1) if m else \"\"\n",
    "\n",
    "\n",
    "targets = filtered if MAX_STORES is None else filtered.head(MAX_STORES)\n",
    "\n",
    "all_reviews = []\n",
    "fail_stores = []\n",
    "\n",
    "for idx, (_, row) in enumerate(tqdm(targets.iterrows(), total=len(targets))):\n",
    "    name = row[\"사업장명\"]\n",
    "    gu = row[\"구\"]\n",
    "    dong = extract_dong(gu, row[\"지번주소\"]) or extract_dong(gu, row[\"도로명주소\"])\n",
    "\n",
    "    try:\n",
    "        place_id = find_place_id(gu, dong, name)\n",
    "        if place_id is None:\n",
    "            fail_stores.append({\"사업장명\": name, \"사유\": \"검색결과 없음\"})\n",
    "            continue\n",
    "\n",
    "        reviews = get_all_reviews(place_id)\n",
    "        if not reviews:\n",
    "            fail_stores.append({\"사업장명\": name, \"사유\": \"리뷰 0건\"})\n",
    "            continue\n",
    "\n",
    "        for r in reviews:\n",
    "            all_reviews.append({\n",
    "                \"사업장명\": name,\n",
    "                \"구\": gu,\n",
    "                \"도로명주소\": row[\"도로명주소\"],\n",
    "                \"영업연수\": row[\"영업연수\"],\n",
    "                \"kakao_place_id\": place_id,\n",
    "                \"review_id\": r.get(\"review_id\"),\n",
    "                \"평점\": r.get(\"star_rating\"),\n",
    "                \"작성일\": r.get(\"registered_at\"),\n",
    "                \"리뷰내용\": r.get(\"contents\"),\n",
    "            })\n",
    "\n",
    "    except Exception as e:\n",
    "        fail_stores.append({\"사업장명\": name, \"사유\": str(e)})\n",
    "\n",
    "    time.sleep(REQUEST_DELAY)\n",
    "\n",
    "    if (idx + 1) % CHECKPOINT_EVERY == 0:\n",
    "        pd.DataFrame(all_reviews).to_csv(REVIEW_CSV_TMP, index=False, encoding=\"utf-8-sig\")\n",
    "\n",
    "print(f\"수집한 리뷰 수: {len(all_reviews)}건 / 실패(매칭없음·리뷰없음) 매장 수: {len(fail_stores)}곳\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "md-save",
   "metadata": {},
   "source": [
    "## 6. 결과 저장"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "cell-save",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'kakao_reviews.csv' 저장 완료: 15309행\n",
      "'kakao_reviews_실패목록.csv' 저장 완료: 199행\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>사업장명</th>\n",
       "      <th>구</th>\n",
       "      <th>도로명주소</th>\n",
       "      <th>영업연수</th>\n",
       "      <th>kakao_place_id</th>\n",
       "      <th>review_id</th>\n",
       "      <th>평점</th>\n",
       "      <th>작성일</th>\n",
       "      <th>리뷰내용</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>부산미가돼지국밥</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)</td>\n",
       "      <td>16.0</td>\n",
       "      <td>428343473</td>\n",
       "      <td>13301311</td>\n",
       "      <td>5</td>\n",
       "      <td>2025-07-26 05:46:43</td>\n",
       "      <td>돼지국밥, 순대, 수육 모두 다 맛있었어요. 직접 담그시는 깍두기가 음싣과 너무 잘...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>부산미가돼지국밥</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)</td>\n",
       "      <td>16.0</td>\n",
       "      <td>428343473</td>\n",
       "      <td>12275819</td>\n",
       "      <td>1</td>\n",
       "      <td>2025-03-07 16:37:19</td>\n",
       "      <td>부산 돼지국밥 명성을 다버리네...\\n해운대고 24시간이고 뭐고간에. 돼지국밥의 명...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>부산미가돼지국밥</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)</td>\n",
       "      <td>16.0</td>\n",
       "      <td>428343473</td>\n",
       "      <td>8909369</td>\n",
       "      <td>5</td>\n",
       "      <td>2023-11-06 12:00:11</td>\n",
       "      <td>소면 나오고 맛있어요 김치 특히 맛있음 국물은 진하지는 않음 취향 따라 가시길</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>부산미가돼지국밥</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)</td>\n",
       "      <td>16.0</td>\n",
       "      <td>428343473</td>\n",
       "      <td>7504856</td>\n",
       "      <td>5</td>\n",
       "      <td>2023-05-03 10:12:38</td>\n",
       "      <td>갈 곳이 없을때 자꾸 발이간다.</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>부산미가돼지국밥</td>\n",
       "      <td>해운대구</td>\n",
       "      <td>부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)</td>\n",
       "      <td>16.0</td>\n",
       "      <td>428343473</td>\n",
       "      <td>7272732</td>\n",
       "      <td>4</td>\n",
       "      <td>2023-04-01 16:33:45</td>\n",
       "      <td>혼술 하기 딱좋은집 수육백반 주문후\\n고기가 나오는데 조금 돼지냄새가 난다\\n난 느...</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "       사업장명     구                                 도로명주소  영업연수 kakao_place_id  \\\n",
       "0  부산미가돼지국밥  해운대구  부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)  16.0      428343473   \n",
       "1  부산미가돼지국밥  해운대구  부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)  16.0      428343473   \n",
       "2  부산미가돼지국밥  해운대구  부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)  16.0      428343473   \n",
       "3  부산미가돼지국밥  해운대구  부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)  16.0      428343473   \n",
       "4  부산미가돼지국밥  해운대구  부산광역시 해운대구 해운대해변로209번길 22-1, 2층 (우동)  16.0      428343473   \n",
       "\n",
       "   review_id  평점                  작성일  \\\n",
       "0   13301311   5  2025-07-26 05:46:43   \n",
       "1   12275819   1  2025-03-07 16:37:19   \n",
       "2    8909369   5  2023-11-06 12:00:11   \n",
       "3    7504856   5  2023-05-03 10:12:38   \n",
       "4    7272732   4  2023-04-01 16:33:45   \n",
       "\n",
       "                                                리뷰내용  \n",
       "0  돼지국밥, 순대, 수육 모두 다 맛있었어요. 직접 담그시는 깍두기가 음싣과 너무 잘...  \n",
       "1  부산 돼지국밥 명성을 다버리네...\\n해운대고 24시간이고 뭐고간에. 돼지국밥의 명...  \n",
       "2        소면 나오고 맛있어요 김치 특히 맛있음 국물은 진하지는 않음 취향 따라 가시길  \n",
       "3                                 갈 곳이 없을때 자꾸 발이간다.   \n",
       "4  혼술 하기 딱좋은집 수육백반 주문후\\n고기가 나오는데 조금 돼지냄새가 난다\\n난 느...  "
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "review_df = pd.DataFrame(all_reviews)\n",
    "review_df.to_csv(REVIEW_CSV, index=False, encoding=\"utf-8-sig\")\n",
    "\n",
    "fail_df = pd.DataFrame(fail_stores)\n",
    "fail_df.to_csv(FAIL_LOG_CSV, index=False, encoding=\"utf-8-sig\")\n",
    "\n",
    "print(f\"'{REVIEW_CSV}' 저장 완료: {len(review_df)}행\")\n",
    "print(f\"'{FAIL_LOG_CSV}' 저장 완료: {len(fail_df)}행\")\n",
    "review_df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "24a06f34-4c13-43a3-87a9-7aff1dd56bc5",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "np.int64(913)"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "review_df[\"리뷰내용\"].isnull().sum()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "2c1d75ff-8d85-495e-84bb-1fe19028eb0e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "np.int64(0)"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# \"리뷰내용\" 컬럼의 결측치가 있는 행 삭제\n",
    "review_df.dropna(\n",
    "    subset=[\"리뷰내용\"],\n",
    "    inplace=True\n",
    ")\n",
    "review_df[\"리뷰내용\"].isnull().sum()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
