Merge branch 'naver_review'
This commit is contained in:
@ -1,5 +0,0 @@
|
||||
# config.py
|
||||
PLACE_IDS = ["플레이스 ID 1", "플레이스 ID 2"] # 여러 플레이스 ID 가능
|
||||
MAX_REVIEWS = 100 # 각 플레이스당 최대 수집 수
|
||||
START_DATE = "2025-07-01" # 필터링 시작일
|
||||
END_DATE = "2025-07-03" # 필터링 종료일
|
||||
@ -1,170 +0,0 @@
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from datetime import datetime
|
||||
import time
|
||||
import config # 사용자 설정 파일
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# ✅ WebDriver 설정 (모바일 User-Agent 포함, 헤드리스 옵션 가능)
|
||||
# ───────────────────────────────────────────────────────
|
||||
def setup_driver():
|
||||
chrome_options = Options()
|
||||
chrome_options.add_argument("--headless=new") # 필요 시 주석 해제
|
||||
chrome_options.add_argument("--no-sandbox")
|
||||
chrome_options.add_argument("--disable-dev-shm-usage")
|
||||
chrome_options.add_argument(
|
||||
"--user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 13_5 like Mac OS X) "
|
||||
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1"
|
||||
)
|
||||
print("[INFO] Chrome WebDriver 실행 중...")
|
||||
return webdriver.Chrome(options=chrome_options)
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# ✅ 날짜 문자열 파싱 함수 (예: '2025년 6월 8일 일요일')
|
||||
# ───────────────────────────────────────────────────────
|
||||
def parse_korean_date(date_str):
|
||||
try:
|
||||
# '2025년 6월 8일 일요일' → '2025년 6월 8일'
|
||||
date_clean = " ".join(date_str.strip().split(" ")[:3])
|
||||
return datetime.strptime(date_clean, "%Y년 %m월 %d일").date()
|
||||
except Exception as e:
|
||||
print(f"[WARN] 날짜 파싱 실패: {date_str} ({e})")
|
||||
return None
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# ✅ "더보기" 버튼 클릭 함수 (한 번만 클릭)
|
||||
# ───────────────────────────────────────────────────────
|
||||
def click_more(driver):
|
||||
try:
|
||||
container = driver.find_element(By.CLASS_NAME, "place_section_content")
|
||||
more_div = container.find_element(By.XPATH, "./following-sibling::div[1]")
|
||||
more_btn = more_div.find_element(By.TAG_NAME, "a")
|
||||
driver.execute_script("arguments[0].click();", more_btn)
|
||||
print("[INFO] 더보기 클릭")
|
||||
time.sleep(2)
|
||||
return True
|
||||
except:
|
||||
print("[INFO] 더보기 없음")
|
||||
return False
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# ✅ 업체명 추출 함수 (페이지 상단의 "_title" ID 활용)
|
||||
# ───────────────────────────────────────────────────────
|
||||
def extract_shop_name(driver):
|
||||
try:
|
||||
main = driver.find_element(By.CSS_SELECTOR, 'div[role="main"]')
|
||||
title = main.find_element(By.ID, "_title")
|
||||
name = title.find_element(By.TAG_NAME, "span").text.strip()
|
||||
return name
|
||||
except Exception as e:
|
||||
print(f"[WARN] 업체명 추출 실패: {e}")
|
||||
return "업체명 없음"
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# ✅ 리뷰 추출 함수: 작성자 / 날짜 / 본문
|
||||
# ───────────────────────────────────────────────────────
|
||||
def extract_reviews(driver):
|
||||
wait = WebDriverWait(driver, 10)
|
||||
wait.until(EC.presence_of_element_located((By.ID, "_review_list")))
|
||||
|
||||
ul = driver.find_element(By.ID, "_review_list")
|
||||
items = ul.find_elements(By.XPATH, './/li[contains(@class, "place_apply_pui")]')
|
||||
reviews = []
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# ① 작성자: ./div[1]/a[2]/div/span/span
|
||||
writer = "익명"
|
||||
try:
|
||||
writer = item.find_element(By.XPATH, "./div[1]/a[2]/div/span/span").text.strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
# ② 날짜: ./div[7]/div[2]/div/span[1]/span[2]
|
||||
date = "날짜 없음"
|
||||
date_obj = None
|
||||
try:
|
||||
date_text = item.find_element(By.XPATH, "./div[7]/div[2]/div/span[1]/span[2]").text.strip()
|
||||
date = date_text
|
||||
date_obj = parse_korean_date(date_text)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ③ 본문: ./div[5]/a
|
||||
text = ""
|
||||
try:
|
||||
text = item.find_element(By.XPATH, "./div[5]/a").text.strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
if text:
|
||||
reviews.append({
|
||||
"writer": writer,
|
||||
"date": date,
|
||||
"date_obj": date_obj,
|
||||
"text": text
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[WARN] 리뷰 추출 실패: {e}")
|
||||
|
||||
return reviews
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# ✅ 특정 기간 내 리뷰 수집 함수 (날짜 필터 + 더보기 반복)
|
||||
# ───────────────────────────────────────────────────────
|
||||
def crawl_reviews_within_range(place_id, start_date, end_date):
|
||||
url = f"https://m.place.naver.com/place/{place_id}/review/visitor?reviewSort=recent"
|
||||
driver = setup_driver()
|
||||
print(f"[INFO] 리뷰 페이지 접속: {url}")
|
||||
driver.get(url)
|
||||
|
||||
shop_name = extract_shop_name(driver)
|
||||
all_reviews = []
|
||||
seen = set()
|
||||
|
||||
while True:
|
||||
new_reviews = extract_reviews(driver)
|
||||
if not new_reviews:
|
||||
break
|
||||
|
||||
filtered = []
|
||||
for r in new_reviews:
|
||||
if r["date_obj"] is None:
|
||||
continue
|
||||
if start_date <= r["date_obj"] <= end_date:
|
||||
key = (r["writer"], r["date_obj"], r["text"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
filtered.append(r)
|
||||
|
||||
if not filtered:
|
||||
print("[INFO] 범위 내 리뷰 없음 → 수집 종료")
|
||||
break
|
||||
|
||||
all_reviews.extend(filtered)
|
||||
|
||||
# 더 클릭할 필요가 없으면 종료
|
||||
if not click_more(driver):
|
||||
break
|
||||
|
||||
driver.quit()
|
||||
print(f"[DONE] [{shop_name}] {len(all_reviews)}개 리뷰 수집 완료")
|
||||
return shop_name, all_reviews
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# ✅ 메인 실행부
|
||||
# ───────────────────────────────────────────────────────
|
||||
if __name__ == "__main__":
|
||||
start_date = datetime.strptime(config.START_DATE, "%Y-%m-%d").date()
|
||||
end_date = datetime.strptime(config.END_DATE, "%Y-%m-%d").date()
|
||||
|
||||
for place_id in config.PLACE_IDS:
|
||||
shop, reviews = crawl_reviews_within_range(place_id, start_date, end_date)
|
||||
|
||||
print(f"\n==== {shop} ({place_id}) 리뷰 목록 ====")
|
||||
for i, r in enumerate(reviews, 1):
|
||||
print(f"{i}. 작성자: {r['writer']}, 날짜: {r['date']}")
|
||||
print(f" 내용: {r['text']}\n")
|
||||
@ -1,89 +0,0 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
from datetime import datetime, timedelta
|
||||
import threading
|
||||
import main # main.py가 같은 폴더에 있어야 함
|
||||
|
||||
def get_yesterday_str():
|
||||
return (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
|
||||
def run_crawler_thread(place_ids, start_date, end_date, output_widget, run_button):
|
||||
def task():
|
||||
try:
|
||||
output_widget.config(state='normal')
|
||||
output_widget.delete("1.0", tk.END)
|
||||
output_widget.insert(tk.END, f"크롤링 시작: {start_date} ~ {end_date}\n\n")
|
||||
|
||||
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||
|
||||
for place_id in place_ids:
|
||||
output_widget.insert(tk.END, f"[업체 ID: {place_id}] 리뷰 수집 중...\n")
|
||||
shop_name, reviews = main.crawl_reviews_within_range(place_id, start_date_obj, end_date_obj)
|
||||
|
||||
output_widget.insert(tk.END, f"업체명: {shop_name}\n")
|
||||
output_widget.insert(tk.END, f"수집 리뷰 수: {len(reviews)}\n")
|
||||
|
||||
for i, r in enumerate(reviews, 1):
|
||||
output_widget.insert(tk.END, f"{i}. 작성자: {r['writer']}, 날짜: {r['date']}\n")
|
||||
output_widget.insert(tk.END, f" 내용: {r['text']}\n")
|
||||
|
||||
output_widget.insert(tk.END, "\n")
|
||||
|
||||
output_widget.insert(tk.END, "크롤링 완료!\n")
|
||||
output_widget.config(state='disabled')
|
||||
except Exception as e:
|
||||
messagebox.showerror("오류", f"크롤링 중 오류 발생:\n{e}")
|
||||
output_widget.config(state='disabled')
|
||||
finally:
|
||||
run_button.config(state='normal')
|
||||
|
||||
run_button.config(state='disabled')
|
||||
threading.Thread(target=task, daemon=True).start()
|
||||
|
||||
def create_gui():
|
||||
root = tk.Tk()
|
||||
root.title("네이버 플레이스 리뷰 크롤러")
|
||||
|
||||
frm = ttk.Frame(root, padding=10)
|
||||
frm.grid()
|
||||
|
||||
ttk.Label(frm, text="시작일 (YYYY-MM-DD):").grid(column=0, row=0, sticky='w')
|
||||
start_entry = ttk.Entry(frm, width=15)
|
||||
start_entry.grid(column=1, row=0)
|
||||
start_entry.insert(0, get_yesterday_str())
|
||||
|
||||
ttk.Label(frm, text="종료일 (YYYY-MM-DD):").grid(column=0, row=1, sticky='w')
|
||||
end_entry = ttk.Entry(frm, width=15)
|
||||
end_entry.grid(column=1, row=1)
|
||||
end_entry.insert(0, get_yesterday_str())
|
||||
|
||||
output = scrolledtext.ScrolledText(frm, width=80, height=25, state='disabled')
|
||||
output.grid(column=0, row=3, columnspan=3, pady=10)
|
||||
|
||||
def on_run():
|
||||
start_date = start_entry.get()
|
||||
end_date = end_entry.get()
|
||||
|
||||
# 날짜 형식 검증
|
||||
try:
|
||||
datetime.strptime(start_date, "%Y-%m-%d")
|
||||
datetime.strptime(end_date, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
messagebox.showerror("입력 오류", "날짜 형식을 YYYY-MM-DD 로 입력하세요.")
|
||||
return
|
||||
|
||||
place_ids = getattr(main.config, "PLACE_IDS", [])
|
||||
if not place_ids:
|
||||
messagebox.showerror("설정 오류", "config.py에 PLACE_IDS 리스트가 비어있습니다.")
|
||||
return
|
||||
|
||||
run_crawler_thread(place_ids, start_date, end_date, output, run_button)
|
||||
|
||||
run_button = ttk.Button(frm, text="실행", command=on_run)
|
||||
run_button.grid(column=0, row=2, pady=5)
|
||||
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_gui()
|
||||
Reference in New Issue
Block a user