diff --git a/naver_review/config.sample.py b/naver_review/config.sample.py deleted file mode 100644 index df2ea42..0000000 --- a/naver_review/config.sample.py +++ /dev/null @@ -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" # 필터링 종료일 diff --git a/naver_review/main.py b/naver_review/main.py deleted file mode 100644 index cf8ceec..0000000 --- a/naver_review/main.py +++ /dev/null @@ -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") diff --git a/naver_review/run.py b/naver_review/run.py deleted file mode 100644 index ed90828..0000000 --- a/naver_review/run.py +++ /dev/null @@ -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()