⚠️ 본 코드는 학습 및 개인 연구 목적으로 작성되었으며, 네이버 지도 서비스의 구조가 변경될 경우 작동하지 않을 수 있습니다. 상업적 목적이나 대량 데이터 수집에는 사용하지 마세요. ⚠️
크롤링? 스크래핑?
웹사이트를 자동으로 탐색하며 정보를 수집하는 것
데이터를 추출해 검색 엔진의 색인 생성, 데이터 분석, 시장 조사 등 다양한 용도로 활용됨.
| 웹 크롤링 | 웹 스크래핑 |
| 여러 웹 페이지를 자동으로 탐색하며 데이터 수집 검색 엔진이 웹사이트를 인덱싱할 때 사용 크롤링 봇(크롤러)이 사이트의 링크를 따라가며 데이터 수집 |
특정 웹페이지에서 원하는 데이터만 추출 가격 비교 사이트, 뉴스 기사 수집에 활용 일반적으로 특정 HTML 요소에서 텍스트, 이미지 등 필요한 데이터만 수집 |
도구
| Python 라이브러리 | GUI 기반 크롤링, 스프래핑 도구 |
| BeautifulSoup : HTML 파싱 및 데이터 추출 Scrapy : 대규모 크롤링 프레임워크 Selenium : 동적 웹사이트 크롤링 (->Playwright) Requests : 웹페이지 요청 보낼 때 |
1. Screaming Frog: SEO 분석 및 사이트 크롤러 2. Octoparce: 코드 없이 데이터 추출 3. ParseHub: 비개발자를 위한 크롤러 |
프로젝트 목표
- 사용자 경험 기반 데이터 분석 : 혼자 즐길 수 있는 장소 ( 혼밥, 혼카페, 혼술)를 네이버지도의 실제 후기를 통해 추출해낸다.
- 동적 웹 스크래핑 + AI 리뷰 분석 + 키워드 분류
전체 설계
| 데이터 수집 | 네이버지도 리뷰 크롤링 | Playwright | 가게명, 주소, 후기 텍스트 |
| 전처리 | 불필요한 텍스트 제거 | Python, pandas | 정제된 리뷰 문장 |
| AI 분석 | 혼밥, 혼카페, 혼술 등 문맥분류 | OpenAI GPT, KoBERT | 각 리뷰별 라벨 |
| 결과 저장 | 분석 결과 엑셀파일로 저장 | pandas (to_exel) | 엑셀 데이터셋 |
1. 시작할 URL 설정 : 크롤링할 웹사이트 주소 정하기
2. 웹페이지 요청 : HTTP 요청을 보내 해당 페이지의 HTML 가져옴
3. 데이터 파싱 : HTML을 분석해 원하는 정보 추출
4. 버튼 클릭과 프레임 전환으로 페이지 전환 제어 (동적 탐색)
5. 반복 실행
키워드
네이버 지도 구조 및 통신 파악
GraphQL로 통신
식당 리스트 - 무한 스크롤 + 페이지네이션
1. 구조 탐색 (Elements 탭)
리뷰 내용은 초기 HTML에는 없고,
JS 실행 후 비동기로 로드되어 DOM에 추가됨 (즉, 브라우저 렌더링 후 확인 가능)
| 페이지 소스 (Ctrl+U) | 서버에서 받은 초기 HTML | 없음 |
| Elements 탭 (F12) | JS가 실행된 후의 DOM | 있음 |
2. 데이터 요청 흐름 보기 (Network 탭)
filter - xhr, fetch (JS가 백엔드로부터 데이터를 받아오는 요청만 받음)
grahql 형태로 되어있음.
즉, 브라우저가 HTMl로 렌더링하기 전에 AJAX로 받아온다.
3. 동적 프레임 구조 확인 (Sources 탭)
네이버 지도는 iframe 구조 (main page -> searchIframe -> entryIframe)
각각의 iframe은 별도의 HTML 문서라 개발자도구 상단의 드롭다운으로 전환해 해당 프레임 내부 구조를 다시 탐색
리뷰는 거의 항상 entryIframe 내부에서 비동기로 로드
* iframe 이란? : 웹페이지 안에 또 다른 웹페이지를 삽입하는 HTML 요소. 브라우저 상에서는 한 페이지처럼 보이지만, 실제로는 여러 HTML 문서가 겹쳐있음.
4. API 형태 확인하기 (참고용)
Network 탭에서 리뷰 로드 요청을 찾으면
Headers : https://pcmap-api.place.naver.com/graphql
Response : JSON 형태
필드명 파악하면 나중에 Python으로 데이터 추출할 때 정확히 매칭
data.visitorReviews.items[i].author.nickname : 사용자 이름
data.visitorReviews.items[i].body : 후기 본문
data.visitorReviews.items[i].businessName : 가게명
data.visitorReviews.items[i].votedKeywords[].name : 후기 키워드
=> 네이버지도는 GraphQL 기반 비동기 요청으로 데이터를 로드하지만, 이번 프로젝트에선 직접 API 요청을 보내지 않고, Playwright로 실제 사용자 동작 (클릭, 스크롤)을 시뮬레이션하여 리뷰데이터를 추출
검색했을 때 리스트
iframe
id = searchIframe
title = Naver Place search
해당 가게
li class = UEzoS rTjJo
가게 눌렀을 때
iframe
id = entryIframe
title = Naver Place Entry
리뷰 버튼
class = veBoZ
리뷰 더보기
data-pui-click-code="rvshowmore"
class="place_section_content" -> 전체 리뷰 영역
class="place_apply_pui EjjAW" -> 리뷰 하나 영역
class="pui__vn15t2" -> 리뷰내용
class="pui__wFzIYl" -> 버튼
검색했을 때 리스트
스크롤 끝까지 + 페이지네이션
가게 눌렀을 때 리뷰 목록
스크롤 끝까지 + 리뷰 더보기 클릭
data.visitorReviews.items[i].author.nickname : 사용자 이름
data.visitorReviews.items[i].body : 후기 본문
data.visitorReviews.items[i].businessName : 가게명
data.visitorReviews.items[i].votedKeywords[].name : 후기 키워드
화이트리스트 : 혼밥, 혼술, 혼카페, 혼자, 혼커피, 혼자, 1인, 1인석,
카운터석, 칸막이, 프라이버시, 자리간격, 바테이블,
바석, 혼자와도, 부담없음, 혼자오기, 혼자먹기, 혼자가기
블랙리스트 : 시끄럽, 자리없, 불친절, 별로, 단체
리뷰 없는 경우는 X
id="entryIframe"
class="place_section no_margin OP4V8"
class="zD5Nm undefined"
id="_title" class="LylZZ v8v5j"
class="GHAhO"
data-nclicks-area-code="rrv"
class="NSTUp"
class="lfH3O"
class="fvwqf"
class="TeItc"
+ 추가로 :: 리스트에서 가게를 클릭하면 그 가게에 대한 후기만 나온다고. 근데 요청시 받는 데이터에 그 클릭한 가게이름도 있으니까 차라리 리스트가 아니라 가게 클릭시 받는 데이터만 추적하는게 나을 것 같음.
+ 근데 이건 어케 해야하지 : 근데 그럼 리스트는 페이지네이션으로 되어있는데, 첫번째 페이지에 있는 가게들을 클릭해서 각각 해당하는 리뷰만 가져올 순 없고, 다른페이지에 있는 리스트들도 클릭했을 때 해당하는 후기를 보게 해야되잖아.
==> ==> 밑에 방법으로 해결하래요 <==<==
- 리스트 페이지는 페이지네이션 구조이므로, Playwright로 '다음 페이지' 버튼을 클릭하며 탐색
- 각 페이지의 가게를 하나씩 클릭하여 해당 entryIframe 진입
- 이때 브라우저 내부에서 자동으로 GraphQL 요청이 발생하고,
- 응답 데이터가 DOM에 렌더링되므로, HTML 요소에서 리뷰 텍스트를 추출
< 작업 과정 >
환경 설정 + 준비 과정
1. 파이썬 설치 3.12.8
2. 작업용 폴더 생성 C:\naver_review
3. 가상환경 만들기 python -m venv venv
4. 가상환경 활성화 venv\Scripts\activate
5. 패키지 설치 pip install playwright pandas openpyxl
- playwright: 브라우저 자동 제어 (네이버 지도 조작용)
- pandas: 표 데이터 다루기
- openpyxl: 엑셀 파일 저장용
6. 브라우저 바이너리 설치 playwright install - 실제 사용할 브라우저(Chromium, Firefox, WebKit)의 드라이버를 자동으로 설치하여 Playwright가 제어할 수 있게 함.
7. 설치 확인 python --version / pip list (playwright, pandas, openyxl 이 보이면 OK)
실제 크롤링 코드 작성 및 데이터 수집 단계
8.VSCode에서 프로젝트 열기 - Python 3.12.7(venv)가 떠있는지 확인
8-1. 안뜨면, Ctrl+Shift+P -> Python: Select Interpreter -> C:\naver_review\venv\Scripts\python.exe 선택
9. 테스트 코드 작성 후 실행 -> python review_crawler.py
브라우저 열기 테스트용 코드

from playwright.sync_api import sync_playwright
import time
import pandas as pd
import json, re
def contains_any(text, keywords):
return any(k in text for k in keywords)
WHITE = ["혼밥", "혼술", "혼카페", "혼자", "1인", "1인석", "카운터석", "편하게", "눈치 안보",
"칸막이", "프라이버시", "자리간격", "바테이블", "바석", "혼자 있어도", "혼자 하기 좋", "적당히 조용",
"혼자와도", "부담없음", "혼자오기", "혼자먹기", "혼자가기"]
BLACK = ["시끄럽", "자리없", "불친절", "별로", "단체"]
with sync_playwright() as p:
browser = p.chromium.launch(headless=False, slow_mo=60)
page = browser.new_page()
page.goto("https://map.naver.com/p")
page.set_viewport_size({"width": 1280, "height": 900})
print("⏳ 네이버 지도 로딩 중...")
page.wait_for_timeout(6000)
query = "구로5동 술집"
print(f"🔍 '{query}' 검색 중...")
# [1] 검색창 입력
search_box = page.locator("div.input_box input, div[role='textbox']")
search_box.first.click()
search_box.first.fill(query)
page.keyboard.press("Enter")
page.wait_for_timeout(4000)
# [2] searchIframe 접근
search_frame = None
for _ in range(40):
for f in page.frames:
if f.name == "searchIframe":
search_frame = f
break
if search_frame:
break
time.sleep(0.5)
if not search_frame:
raise Exception("❌ searchIframe 감지 실패")
print("✅ searchIframe 접근 성공!")
# [3] 리스트 로딩 및 스크롤
list_sel = "li.UEzoS.rTjJo"
search_frame.wait_for_selector(list_sel, timeout=20000)
print("📜 리스트 전체 스크롤 탐색 중...")
seen_texts = set()
scroll_stable_count = 0
last_scroll_height = 0
while True:
try:
elements = search_frame.locator(list_sel)
count_now = elements.count()
for idx in range(count_now):
try:
txt = elements.nth(idx).inner_text().strip()
if txt not in seen_texts:
seen_texts.add(txt)
except:
pass
search_frame.evaluate("""
() => {
const scrollable =
document.querySelector('#_pcmap_list_scroll_container') ||
document.querySelector('#pcmap_list_scroll_container') ||
document.querySelector('[class*="scroll"]') ||
document.scrollingElement;
if (scrollable) scrollable.scrollBy(0, scrollable.scrollHeight);
}
""")
time.sleep(1.5)
current_scroll_height = search_frame.evaluate(
"() => document.body.scrollHeight"
)
if current_scroll_height == last_scroll_height:
scroll_stable_count += 1
else:
scroll_stable_count = 0
if scroll_stable_count >= 3:
print("✅ 스크롤 끝 감지됨 (더 이상 새로운 가게 없음)")
# ✅ 브라우저에서 스크롤 완료 상태 확인용 대기
print("⏸️ 3초간 대기 중... (스크롤 완료 화면 확인)")
time.sleep(3)
break
last_scroll_height = current_scroll_height
except Exception as e:
print(f"⚠️ 스크롤 중 오류 발생: {e}")
break
print(f"✅ 총 {len(seen_texts)}개 가게 탐지 완료 (가상 스크롤 포함)")
items = search_frame.locator(list_sel)
total_places = items.count()
print(f"📋 실제 감지된 항목 수: {total_places}")
if total_places == 0:
print("❌ 검색 결과 없음. 종료.")
browser.close()
exit()
results = []
seen_reviews = set()
last_saved_count = 0
# [4] 각 가게 반복
for i in range(total_places):
try:
print(f"\n🏠 [{i+1}/{total_places}] 가게 클릭 중...")
# ✅ [수정1] searchIframe 재탐색 안정화
time.sleep(1.0) # iframe 로드 안정화용 대기
for _ in range(15): # 최대 7.5초 동안 시도
search_frame = None
for f in page.frames:
if f.name == "searchIframe":
search_frame = f
break
if search_frame:
break
print(f"⏳ searchIframe 재로딩 대기 중... ({i+1}번째 가게)")
time.sleep(0.5)
if not search_frame:
print(f"❌ searchIframe 감지 실패 — {i+1}번째 가게 스킵")
continue
# ✅ 프레임 로드 후 기본 요소 확인
search_frame = page.frame(name="searchIframe")
search_frame.wait_for_selector("#_pcmap_list_scroll_container, #pcmap_list_scroll_container", timeout=30000)
search_frame.wait_for_selector(list_sel, state="attached", timeout=30000)
if not page.frame(name="searchIframe"):
page.wait_for_function("document.querySelector('iframe[name=searchIframe]') !== null", timeout=10000)
search_frame = page.frame(name="searchIframe")
items = search_frame.locator(list_sel)
search_frame.wait_for_selector(list_sel, timeout=10000)
search_frame.wait_for_function(
f"() => document.querySelectorAll('{list_sel}').length > {i}",
timeout=15000
)
# i번째 항목이 렌더링될 때까지 스크롤 반복
max_scroll_attempts = 8
scroll_attempt = 0
while True:
try:
count_visible = search_frame.evaluate(f"() => document.querySelectorAll('{list_sel}').length")
if count_visible > i:
break # i번째 항목이 DOM에 렌더링됨
search_frame.evaluate("""
() => {
const sc =
document.querySelector('#_pcmap_list_scroll_container') ||
document.querySelector('#pcmap_list_scroll_container') ||
document.querySelector('[class*="scroll"]') ||
document.scrollingElement;
if (sc) sc.scrollBy(0, 500);
}
""")
time.sleep(0.8)
scroll_attempt += 1
if scroll_attempt > max_scroll_attempts:
print(f"⚠️ {i+1}번째 항목이 끝내 렌더링되지 않음 — 건너뜀.")
raise Exception("가상 스크롤 초과")
except Exception as e:
print(f"⚠️ 스크롤 시도 중 오류: {e}")
break
# 스크롤 컨테이너 직접 스크롤
search_frame.evaluate(
"""({ idx, sel }) => {
const sc =
document.querySelector('#_pcmap_list_scroll_container') ||
document.querySelector('#pcmap_list_scroll_container');
const els = Array.from(document.querySelectorAll(sel));
const el = els[idx];
if (sc && el) sc.scrollTo({ top: el.offsetTop - 100 });
}""",
{"idx": i, "sel": list_sel}
)
time.sleep(0.5)
# ✅ 클릭 전 lazyload-wrapper가 아닌 UEzoS.rTjJo로 완전히 로드될 때까지 대기
for _ in range(10):
class_name = search_frame.evaluate(
f"""(idx) => {{
const el = document.querySelectorAll('{list_sel}')[idx];
return el ? el.className : '';
}}""",
i
)
if class_name and "lazyload-wrapper" not in class_name:
break
print(f"⏳ {i+1}번째 항목 로딩 중... (현재 상태: {class_name})")
time.sleep(0.5)
else:
print(f"⚠️ {i+1}번째 항목이 끝내 로드되지 않음 (lazyload-wrapper 상태 유지)")
continue # 다음 가게로 넘어감
# 실제 클릭 시도
# ✅ li 안의 a.place_bluelink 클릭 (가게명 텍스트 기준)
place_link_sel = f"{list_sel} a.place_bluelink"
try:
place_links = search_frame.locator(place_link_sel)
# i번째 가게의 링크 클릭
place_links.nth(i).scroll_into_view_if_needed(timeout=5000)
name = place_links.nth(i).inner_text().strip()
print(f"🏠 클릭 대상: {name}")
place_links.nth(i).click(timeout=7000)
print(f"🖱️ {i+1}번째 가게명 클릭 완료! entryIframe 생성 대기 중...")
time.sleep(3)
except Exception as e:
print(f"⚠️ {i+1}번째 가게명 클릭 실패 → 재시도 중...", e)
time.sleep(1)
try:
place_links.nth(i).scroll_into_view_if_needed(timeout=5000)
place_links.nth(i).click(timeout=10000)
print(f"🖱️ {i+1}번째 가게명 재클릭 성공!")
time.sleep(3)
except Exception as e:
print(f"❌ {i+1}번째 가게 클릭 완전 실패:", e)
continue
# ✅ entryIframe 대기 (최대 40초로 확장)
try:
page.wait_for_function(
"() => document.querySelector('iframe#entryIframe') !== null",
timeout=40000
)
print("✅ entryIframe 생성 감지 완료!")
except Exception as e:
print(f"⚠️ entryIframe 생성 지연: {e} → 1회 새로고침 후 재시도")
page.reload()
time.sleep(5)
try:
page.wait_for_function(
"() => document.querySelector('iframe#entryIframe') !== null",
timeout=30000
)
print("✅ entryIframe 생성 성공 (재시도 후)!")
except:
print(f"❌ entryIframe 로드 실패 — {i+1}번째 가게 스킵")
page.go_back()
print("🔄 목록 페이지로 복귀 중... searchIframe 로딩 대기")
for _ in range(20): # 최대 10초 동안 시도
search_frame = None
for f in page.frames:
if f.name == "searchIframe":
search_frame = f
break
if search_frame:
print("✅ searchIframe 복구 감지됨!")
break
time.sleep(0.5)
else:
print("⚠️ searchIframe 복구 실패 — 스킵 또는 reload 시도")
try:
page.reload()
time.sleep(5)
except:
pass
time.sleep(1)
continue
page.wait_for_function(
"() => document.querySelector('iframe#entryIframe') !== null",
timeout=15000
)
time.sleep(1.5)
entry_frame = None
for attempt in range(3):
try:
page.wait_for_function(
"() => document.querySelector('iframe#entryIframe') !== null", timeout=20000
)
entry_frame = page.frame(name="entryIframe")
if entry_frame:
break
except Exception as e:
print(f"⚠️ entryIframe 탐색 중 예외 (시도 {attempt+1}/3):", e)
time.sleep(3)
if not entry_frame:
print("❌ entryIframe 로드 실패 — 다음 가게로 넘어감.")
page.go_back()
print("🔄 목록 페이지로 복귀 중... searchIframe 로딩 대기")
for _ in range(20): # 최대 10초 동안 시도
search_frame = None
for f in page.frames:
if f.name == "searchIframe":
search_frame = f
break
if search_frame:
print("✅ searchIframe 복구 감지됨!")
break
time.sleep(0.5)
else:
print("⚠️ searchIframe 복구 실패 — 스킵 또는 reload 시도")
try:
page.reload()
time.sleep(5)
except:
pass
time.sleep(1)
continue
try:
entry_frame.wait_for_selector('span.PXMot', timeout=10000)
review_btns = entry_frame.locator('span.PXMot')
target = None
for btn in review_btns.all():
try:
if "방문자 리뷰" in btn.inner_text().strip():
target = btn
break
except:
continue
if not target:
raise Exception("방문자 리뷰 버튼 없음")
target.click()
print("✅ 방문자 리뷰 버튼 클릭 완료!")
time.sleep(4)
except Exception as e:
print("⚠️ 방문자 리뷰 클릭 실패:", e)
page.go_back()
print("🔄 목록 페이지로 복귀 중... searchIframe 로딩 대기")
for _ in range(20): # 최대 10초 동안 시도
search_frame = None
for f in page.frames:
if f.name == "searchIframe":
search_frame = f
break
if search_frame:
print("✅ searchIframe 복구 감지됨!")
break
time.sleep(0.5)
else:
print("⚠️ searchIframe 복구 실패 — 스킵 또는 reload 시도")
try:
page.reload()
time.sleep(5)
except:
pass
time.sleep(1)
continue
store_name = ""
try:
entry_frame.wait_for_selector("div#_title span.GHAhO", timeout=5000)
store_name = entry_frame.locator("div#_title span.GHAhO").inner_text().strip()
except:
store_name = "가게명_확인불가"
if store_name == "가게명_확인불가":
page.go_back()
print("🔄 목록 페이지로 복귀 중... searchIframe 로딩 대기")
for _ in range(20): # 최대 10초 동안 시도
search_frame = None
for f in page.frames:
if f.name == "searchIframe":
search_frame = f
break
if search_frame:
print("✅ searchIframe 복구 감지됨!")
break
time.sleep(0.5)
else:
print("⚠️ searchIframe 복구 실패 — 스킵 또는 reload 시도")
try:
page.reload()
time.sleep(5)
except:
pass
time.sleep(1)
continue
entry_frame.wait_for_selector("div.place_section.k1QQ5 > div.place_section_content", timeout=8000)
review_area = entry_frame.locator("div.place_section.k1QQ5 > div.place_section_content").first
print("📜 리뷰탭 스크롤 및 리뷰 수집 시작...")
for page_round in range(7):
print(f"--- 리뷰 페이지 {page_round+1} ---")
last_scroll = 0
same_scroll_count = 0
while True:
try:
entry_frame.evaluate("""
() => {
const scrollable = document.querySelector('div.place_section.k1QQ5 > div.place_section_content');
if (scrollable) scrollable.scrollBy(0, scrollable.scrollHeight);
}
""")
time.sleep(1.0)
curr_scroll = entry_frame.evaluate("""
() => {
const el = document.querySelector('div.place_section.k1QQ5 > div.place_section_content');
return el ? el.scrollTop : 0;
}
""")
if curr_scroll == last_scroll:
same_scroll_count += 1
else:
same_scroll_count = 0
last_scroll = curr_scroll
more_btns = entry_frame.locator("a.pui__wFzIYl[data-pui-click-code='rvshowmore']")
for btn in more_btns.all():
try:
btn.scroll_into_view_if_needed()
btn.click()
time.sleep(0.2)
except:
pass
if same_scroll_count >= 2:
break
except:
break
review_blocks = entry_frame.locator("li.place_apply_pui.EjjAW").all()
if not review_blocks:
print("⚠️ 리뷰 없음, 종료")
break
count_before = len(results)
for block in review_blocks:
try:
text = block.locator("div.pui__vn15t2").inner_text().strip()
if text not in seen_reviews:
if contains_any(text, WHITE) and not contains_any(text, BLACK):
seen_reviews.add(text)
results.append({
"가게명": store_name,
"리뷰내용": text
})
except:
continue
if len(results) - last_saved_count >= 20:
try:
pd.DataFrame(results).to_excel("naver_reviews_autosave.xlsx", index=False)
last_saved_count = len(results)
print(f"💾 자동 저장 완료 ({len(results)}개 누적)")
except Exception as e:
print("⚠️ 자동저장 실패:", e)
count_added = len(results) - count_before
print(f"💬 이번 페이지에서 리뷰 {count_added}개 수집 완료 (누적 {len(results)})")
load_more_btn = entry_frame.locator("a.fvwqf:has(span.TeItc:has-text('더보기'))")
if load_more_btn.count() > 0:
try:
load_more_btn.first.scroll_into_view_if_needed()
load_more_btn.first.click()
print(f"➡️ 리뷰 더보기 클릭 ({page_round+1}/7)")
# ✅ 리뷰 더보기 클릭 후 명시적으로 3초 대기 (리뷰 로드 확인용)
print("⏸️ 리뷰 더보기 클릭 후 3초 대기 중...")
time.sleep(3)
except:
print("⚠️ 리뷰 더보기 클릭 실패")
break
page.go_back()
print("🔄 목록 페이지로 복귀 중... searchIframe 로딩 대기")
for _ in range(20): # 최대 10초 동안 시도
search_frame = None
for f in page.frames:
if f.name == "searchIframe":
search_frame = f
break
if search_frame:
print("✅ searchIframe 복구 감지됨!")
break
time.sleep(0.5)
else:
print("⚠️ searchIframe 복구 실패 — 스킵 또는 reload 시도")
try:
page.reload()
time.sleep(5)
except:
pass
time.sleep(1)
except Exception as e:
print(f"⚠️ 오류 발생 ({i+1}번째):", e)
continue
try:
if results:
df = pd.DataFrame(results)
df.to_excel("naver_reviews_filtered.xlsx", index=False)
print(f"\n✅ 완료! 총 {len(df)}개의 리뷰를 naver_reviews_filtered.xlsx에 저장했습니다.")
else:
print("❌ 조건에 맞는 리뷰 없음")
except Exception as e:
print("⚠️ 최종 저장 중 오류:", e)
browser.close()'공간정보아카데미' 카테고리의 다른 글
| [Spring·Vue·MySQL] 개발 기술 개념도 정리 (0) | 2025.09.18 |
|---|---|
| [Git] Github 사용법 총정 (0) | 2025.09.10 |
| [dbmodeling-Normalization] 데이터모델링 정규화 실습 (0) | 2025.09.09 |
| 개발 필수 개념 총정리 (JSON부터 Spring Boot, Node.js 까지) (0) | 2025.09.09 |
| Spring - Bean 주입 / XML vs JavaConfig (0) | 2025.09.06 |