Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72a12ac29f | |||
| e396c08f6b | |||
| e58fa0bd57 | |||
| 3b73517cfa | |||
| f760723067 | |||
| bbb17ef362 | |||
| c3488e7bc9 | |||
| 698e0736fc | |||
| 66bf05e11d | |||
| 77d209d6fc | |||
| 27fcea070a |
@ -16,7 +16,7 @@ NAVER_PW=Login_Password
|
|||||||
|
|
||||||
# 메시지 전송 플랫폼 선택
|
# 메시지 전송 플랫폼 선택
|
||||||
# mattermost, synology_chat, telegram 중 선택(콤마로 구분) 또는 빈 값(발송 안함)
|
# mattermost, synology_chat, telegram 중 선택(콤마로 구분) 또는 빈 값(발송 안함)
|
||||||
MESSAGE_PLATFORM=mattermost,telegram
|
MESSAGE_PLATFORMS=mattermost,telegram
|
||||||
|
|
||||||
# Mattermost 설정
|
# Mattermost 설정
|
||||||
# MATTERMOST_URL은 마지막에 '/' 없이 입력
|
# MATTERMOST_URL은 마지막에 '/' 없이 입력
|
||||||
@ -27,7 +27,7 @@ MATTERMOST_CHANNEL_ID=CHANNEL_ID
|
|||||||
MATTERMOST_BOT_TOKEN=BOT_TOKEN
|
MATTERMOST_BOT_TOKEN=BOT_TOKEN
|
||||||
|
|
||||||
# Synology Chat Webhook URL (사용 시 설정)
|
# Synology Chat Webhook URL (사용 시 설정)
|
||||||
SYNology_CHAT_WEBHOOK_URL=https://synology.chat/webhook/your_webhook_url
|
Synology_CHAT_WEBHOOK_URL=https://synology.chat/webhook/your_webhook_url
|
||||||
|
|
||||||
# Telegram 설정 (사용 시 설정)
|
# Telegram 설정 (사용 시 설정)
|
||||||
TELEGRAM_BOT_TOKEN=your_bot_token
|
TELEGRAM_BOT_TOKEN=your_bot_token
|
||||||
|
|||||||
14
README.md
14
README.md
@ -1,7 +1,7 @@
|
|||||||
# 네이버 리뷰 크롤러
|
# 네이버 리뷰 크롤러
|
||||||
- 네이버 비즈니스, 네이버 map 기준 리뷰를 크롤링해 메시지를 보내줌
|
- 네이버 비즈니스, 네이버 map 기준 리뷰를 크롤링해 메시지를 보내줌
|
||||||
# 폴더 구조
|
# 폴더 구조
|
||||||
``` bash
|
```bash
|
||||||
/
|
/
|
||||||
├── .env_sample # 환경변수 샘플 파일 (.env_sample)
|
├── .env_sample # 환경변수 샘플 파일 (.env_sample)
|
||||||
├── .gitignore # Git 무시 파일 목록
|
├── .gitignore # Git 무시 파일 목록
|
||||||
@ -19,3 +19,15 @@
|
|||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# 빌드(Windows 11 기준)
|
||||||
|
```bash
|
||||||
|
pyinstaller --onefile `
|
||||||
|
--add-data ".env;.env" `
|
||||||
|
--add-data "conf;conf" `
|
||||||
|
--add-data "lib;lib" `
|
||||||
|
--add-data "data;data" `
|
||||||
|
run.py
|
||||||
|
```
|
||||||
|
- 환경 변수를 포함하여 빌드하므로 `.env` 파일을 사전에 생성해 두어야 함
|
||||||
|
- 동작 확인을 위해 `--noconsloe` 옵션 제거
|
||||||
|
|||||||
@ -1,4 +1,25 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 프로젝트 루트 기준으로 .env 위치 지정
|
||||||
|
BASE_DIR = getattr(sys, '_MEIPASS', os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
ENV_PATH = os.path.join(BASE_DIR, '.env')
|
||||||
|
load_dotenv(dotenv_path=ENV_PATH)
|
||||||
|
|
||||||
|
# ✅ 타임존 설정
|
||||||
|
TZ = os.getenv("TZ", "Asia/Seoul")
|
||||||
|
os.environ["TZ"] = TZ
|
||||||
|
try:
|
||||||
|
time.tzset()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
time.tzset()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
def parse_bool(value):
|
def parse_bool(value):
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
@ -17,14 +38,14 @@ MAX_REVIEWS = int(os.getenv("MAX_REVIEWS", "100"))
|
|||||||
NAVER_ID = os.getenv("NAVER_ID", "")
|
NAVER_ID = os.getenv("NAVER_ID", "")
|
||||||
NAVER_PW = os.getenv("NAVER_PW", "")
|
NAVER_PW = os.getenv("NAVER_PW", "")
|
||||||
|
|
||||||
MESSAGE_PLATFORMS = parse_list(os.getenv("MESSAGE_PLATFORM", ""))
|
MESSAGE_PLATFORMS = parse_list(os.getenv("MESSAGE_PLATFORMS", ""))
|
||||||
|
|
||||||
MATTERMOST_URL = os.getenv("MATTERMOST_URL", "")
|
MATTERMOST_URL = os.getenv("MATTERMOST_URL", "")
|
||||||
MATTERMOST_WEBHOOK_URL = os.getenv("MATTERMOST_WEBHOOK_URL", "")
|
MATTERMOST_WEBHOOK_URL = os.getenv("MATTERMOST_WEBHOOK_URL", "")
|
||||||
MATTERMOST_CHANNEL_ID = os.getenv("MATTERMOST_CHANNEL_ID", "")
|
MATTERMOST_CHANNEL_ID = os.getenv("MATTERMOST_CHANNEL_ID", "")
|
||||||
MATTERMOST_BOT_TOKEN = os.getenv("MATTERMOST_BOT_TOKEN", "")
|
MATTERMOST_BOT_TOKEN = os.getenv("MATTERMOST_BOT_TOKEN", "")
|
||||||
|
|
||||||
SYNology_CHAT_WEBHOOK_URL = os.getenv("SYNology_CHAT_WEBHOOK_URL", "")
|
Synology_CHAT_WEBHOOK_URL = os.getenv("Synology_CHAT_WEBHOOK_URL", "")
|
||||||
|
|
||||||
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
||||||
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# biz_crawler.py
|
||||||
import os, sys
|
import os, sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -13,7 +14,7 @@ from conf.config import (
|
|||||||
MESSAGE_PLATFORMS, MATTERMOST_URL, MATTERMOST_BOT_TOKEN, MATTERMOST_CHANNEL_ID
|
MESSAGE_PLATFORMS, MATTERMOST_URL, MATTERMOST_BOT_TOKEN, MATTERMOST_CHANNEL_ID
|
||||||
)
|
)
|
||||||
from lib.send_message import MessageSender
|
from lib.send_message import MessageSender
|
||||||
from lib.lib import (
|
from lib.common import (
|
||||||
create_mobile_driver,
|
create_mobile_driver,
|
||||||
save_cookies,
|
save_cookies,
|
||||||
load_cookies,
|
load_cookies,
|
||||||
@ -22,6 +23,10 @@ from lib.lib import (
|
|||||||
clean_html_text
|
clean_html_text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def debug(msg):
|
||||||
|
if DEBUG:
|
||||||
|
print(f"[DEBUG] {msg}")
|
||||||
|
|
||||||
class NaverReviewCollector:
|
class NaverReviewCollector:
|
||||||
def __init__(self, headless=HEADLESS):
|
def __init__(self, headless=HEADLESS):
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
@ -41,7 +46,7 @@ class NaverReviewCollector:
|
|||||||
try:
|
try:
|
||||||
modal = wait.until(EC.presence_of_element_located((By.ID, "modal-root")))
|
modal = wait.until(EC.presence_of_element_located((By.ID, "modal-root")))
|
||||||
modal.find_element(By.XPATH, './/button').click()
|
modal.find_element(By.XPATH, './/button').click()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -49,7 +54,6 @@ class NaverReviewCollector:
|
|||||||
self.driver.find_element(By.ID, 'pw').send_keys(NAVER_PW)
|
self.driver.find_element(By.ID, 'pw').send_keys(NAVER_PW)
|
||||||
self.driver.find_element(By.XPATH, '//button[@type="submit"]').click()
|
self.driver.find_element(By.XPATH, '//button[@type="submit"]').click()
|
||||||
except Exception:
|
except Exception:
|
||||||
self.driver.quit()
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
@ -70,9 +74,32 @@ class NaverReviewCollector:
|
|||||||
EC.presence_of_element_located((By.XPATH, '//*[starts-with(@class, "Header_btn_select_")]'))
|
EC.presence_of_element_located((By.XPATH, '//*[starts-with(@class, "Header_btn_select_")]'))
|
||||||
)
|
)
|
||||||
return el.text.strip()
|
return el.text.strip()
|
||||||
except:
|
except Exception:
|
||||||
return "알수없음"
|
return "알수없음"
|
||||||
|
|
||||||
|
def extract_written_date(self, spans, li):
|
||||||
|
labels = [s.text.strip() for s in spans]
|
||||||
|
try:
|
||||||
|
if "작성일" in labels:
|
||||||
|
idx = labels.index("작성일")
|
||||||
|
return spans[idx + 1].find_element(By.TAG_NAME, "time").text.strip()
|
||||||
|
elif "예약자" in labels:
|
||||||
|
return li.find_element(By.XPATH, ".//div[3]/div[1]/span[2]/time").text.strip()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def extract_review_text(self, li):
|
||||||
|
for i in range(4, 7):
|
||||||
|
try:
|
||||||
|
el = li.find_element(By.XPATH, f"./div[{i}]/a")
|
||||||
|
if el:
|
||||||
|
text = el.text.strip()
|
||||||
|
return clean_html_text(el.get_attribute("innerHTML")) if text else "내용 없음"
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return "내용 없음"
|
||||||
|
|
||||||
|
|
||||||
def extract_reviews(self):
|
def extract_reviews(self):
|
||||||
reviews = []
|
reviews = []
|
||||||
try:
|
try:
|
||||||
@ -81,48 +108,34 @@ class NaverReviewCollector:
|
|||||||
)
|
)
|
||||||
lis = self.driver.find_elements(By.XPATH, "//ul[starts-with(@class, 'Review_columns_list')]/li")
|
lis = self.driver.find_elements(By.XPATH, "//ul[starts-with(@class, 'Review_columns_list')]/li")
|
||||||
for li in lis:
|
for li in lis:
|
||||||
if "Review_banner__" in li.get_attribute("class"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if "Review_banner__" in li.get_attribute("class"):
|
||||||
|
continue
|
||||||
|
|
||||||
author = li.find_element(By.XPATH, ".//div[1]/a[2]/div/span/span").text.strip()
|
author = li.find_element(By.XPATH, ".//div[1]/a[2]/div/span/span").text.strip()
|
||||||
visit_text = li.find_element(By.XPATH, ".//div[2]/div[1]/span[2]/time").text.strip()
|
visit_text = li.find_element(By.XPATH, ".//div[2]/div[1]/span[2]/time").text.strip()
|
||||||
visit_date = datetime.strptime(visit_text.split("(")[0].replace(". ", "-").replace(".", ""), "%Y-%m-%d").strftime("%Y-%m-%d")
|
visit_date = datetime.strptime(
|
||||||
|
visit_text.split("(")[0].replace(". ", "-").replace(".", ""), "%Y-%m-%d"
|
||||||
|
).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
spans = li.find_elements(By.XPATH, ".//div[2]/div[2]/span")
|
spans = li.find_elements(By.XPATH, ".//div[2]/div[2]/span")
|
||||||
labels = [s.text.strip() for s in spans]
|
written_text = self.extract_written_date(spans, li)
|
||||||
written_text = None
|
|
||||||
|
|
||||||
if "작성일" in labels:
|
|
||||||
idx = labels.index("작성일")
|
|
||||||
written_text = spans[idx + 1].find_element(By.TAG_NAME, "time").text.strip()
|
|
||||||
elif "예약자" in labels:
|
|
||||||
try:
|
|
||||||
written_text = li.find_element(By.XPATH, ".//div[3]/div[1]/span[2]/time").text.strip()
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not written_text:
|
if not written_text:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
written_date = datetime.strptime(written_text.split("(")[0].replace(". ", "-").replace(".", ""), "%Y-%m-%d").date()
|
try:
|
||||||
|
written_date = datetime.strptime(
|
||||||
|
written_text.split("(")[0].replace(". ", "-").replace(".", ""), "%Y-%m-%d"
|
||||||
|
).date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
if not (self.start_date <= written_date <= self.end_date):
|
if not (self.start_date <= written_date <= self.end_date):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
content_el = None
|
text = self.extract_review_text(li)
|
||||||
for i in range(4, 7):
|
#if not text:
|
||||||
try:
|
# continue
|
||||||
el = li.find_element(By.XPATH, f"./div[{i}]/a")
|
|
||||||
if el and el.text.strip():
|
|
||||||
content_el = el
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
if content_el is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
html = content_el.get_attribute("innerHTML")
|
|
||||||
text = clean_html_text(html)
|
|
||||||
|
|
||||||
reviews.append({
|
reviews.append({
|
||||||
"작성자": author,
|
"작성자": author,
|
||||||
@ -130,10 +143,9 @@ class NaverReviewCollector:
|
|||||||
"작성일": written_date,
|
"작성일": written_date,
|
||||||
"내용": text
|
"내용": text
|
||||||
})
|
})
|
||||||
|
except Exception:
|
||||||
except:
|
|
||||||
continue
|
continue
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return reviews
|
return reviews
|
||||||
|
|
||||||
@ -163,9 +175,10 @@ class NaverReviewCollector:
|
|||||||
lines.append("---")
|
lines.append("---")
|
||||||
|
|
||||||
message = "\n".join(lines)
|
message = "\n".join(lines)
|
||||||
|
|
||||||
if not MESSAGE_PLATFORMS:
|
if not MESSAGE_PLATFORMS:
|
||||||
print("[WARN] 메시지 전송 플랫폼이 지정되지 않음. 미전송")
|
print("[WARN] 메시지 전송 플랫폼이 지정되지 않음. 미전송")
|
||||||
print(f"[DEBUG] {message}")
|
debug(message)
|
||||||
return
|
return
|
||||||
|
|
||||||
sender = MessageSender(
|
sender = MessageSender(
|
||||||
@ -175,38 +188,40 @@ class NaverReviewCollector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
print(f"[DEBUG] message platform : {MESSAGE_PLATFORMS}")
|
debug(f"메시지 플랫폼: {MESSAGE_PLATFORMS}")
|
||||||
print("[DEBUG] 디버그 모드 메시지 미전송")
|
debug("디버그 모드: 메시지 전송 생략")
|
||||||
print(f"[DEBUG] {message}")
|
debug(message)
|
||||||
else:
|
else:
|
||||||
sender.send(message, platforms=MESSAGE_PLATFORMS, use_webhook=False)
|
sender.send(message, platforms=MESSAGE_PLATFORMS, use_webhook=False)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.create_driver()
|
while True:
|
||||||
self.driver.get("https://naver.com")
|
self.create_driver()
|
||||||
time.sleep(1)
|
self.driver.get("https://naver.com")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
if os.path.exists(COOKIE_FILE):
|
if os.path.exists(COOKIE_FILE):
|
||||||
try:
|
try:
|
||||||
load_cookies(self.driver, COOKIE_FILE)
|
load_cookies(self.driver, COOKIE_FILE)
|
||||||
self.driver.get("https://naver.com")
|
self.driver.get("https://naver.com")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except:
|
except Exception:
|
||||||
os.remove(COOKIE_FILE)
|
os.remove(COOKIE_FILE)
|
||||||
|
self.driver.quit()
|
||||||
|
self.headless = False
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if self.headless:
|
||||||
|
self.driver.quit()
|
||||||
|
self.headless = False
|
||||||
|
continue
|
||||||
|
if not self.perform_login():
|
||||||
|
self.driver.quit()
|
||||||
|
return
|
||||||
self.driver.quit()
|
self.driver.quit()
|
||||||
NaverReviewCollector(headless=False).run()
|
continue
|
||||||
return
|
|
||||||
else:
|
break # 쿠키 로딩 또는 로그인 성공 시 루프 종료
|
||||||
if self.headless:
|
|
||||||
self.driver.quit()
|
|
||||||
NaverReviewCollector(headless=False).run()
|
|
||||||
return
|
|
||||||
if not self.perform_login():
|
|
||||||
self.driver.quit()
|
|
||||||
return
|
|
||||||
self.driver.quit()
|
|
||||||
NaverReviewCollector(headless=self.headless).run()
|
|
||||||
return
|
|
||||||
|
|
||||||
for biz_id in BIZ_ID:
|
for biz_id in BIZ_ID:
|
||||||
place_name = self.access_review_page(biz_id)
|
place_name = self.access_review_page(biz_id)
|
||||||
@ -215,8 +230,8 @@ class NaverReviewCollector:
|
|||||||
print("[WARN] 세션 만료 또는 쿠키 무효. 로그인 재진행")
|
print("[WARN] 세션 만료 또는 쿠키 무효. 로그인 재진행")
|
||||||
os.remove(COOKIE_FILE)
|
os.remove(COOKIE_FILE)
|
||||||
self.driver.quit()
|
self.driver.quit()
|
||||||
NaverReviewCollector(headless=False).run()
|
self.headless = False
|
||||||
return
|
return self.run()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reviews = self.extract_reviews()
|
reviews = self.extract_reviews()
|
||||||
@ -228,13 +243,14 @@ class NaverReviewCollector:
|
|||||||
self.reviews_by_place[place_name] = []
|
self.reviews_by_place[place_name] = []
|
||||||
|
|
||||||
self.driver.quit()
|
self.driver.quit()
|
||||||
|
|
||||||
if not self.reviews_by_place:
|
if not self.reviews_by_place:
|
||||||
sender = MessageSender(
|
sender = MessageSender(
|
||||||
mattermost_url=MATTERMOST_URL,
|
mattermost_url=MATTERMOST_URL,
|
||||||
mattermost_bot_token=MATTERMOST_BOT_TOKEN,
|
mattermost_token=MATTERMOST_BOT_TOKEN,
|
||||||
mattermost_channel_id=MATTERMOST_CHANNEL_ID,
|
mattermost_channel_id=MATTERMOST_CHANNEL_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
send_failure_message(sender, MESSAGE_PLATFORMS)
|
send_failure_message(sender, MESSAGE_PLATFORMS)
|
||||||
else:
|
else:
|
||||||
self.send_to_message()
|
self.send_to_message()
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
# lib/lib.py
|
# lib/lib.py
|
||||||
|
|
||||||
import os
|
import os, sys
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import undetected_chromedriver as uc
|
import undetected_chromedriver as uc
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
# 공통 설정 경로 추가 (필요 시)
|
# 공통 설정 경로 추가 (필요 시)
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
# naver_review_crawler.py
|
||||||
import os, sys
|
import os, sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
@ -7,11 +8,11 @@ from selenium.webdriver.support import expected_conditions as EC
|
|||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
|
|
||||||
from conf.config import (
|
from conf.config import (
|
||||||
PLACE_IDS, START_DATE, END_DATE, DEBUG,
|
PLACE_IDS, START_DATE, END_DATE, DEBUG,
|
||||||
MESSAGE_PLATFORMS, MATTERMOST_URL, MATTERMOST_BOT_TOKEN, MATTERMOST_CHANNEL_ID
|
MESSAGE_PLATFORMS, MATTERMOST_URL, MATTERMOST_BOT_TOKEN, MATTERMOST_CHANNEL_ID
|
||||||
)
|
)
|
||||||
from lib.send_message import MessageSender
|
from lib.send_message import MessageSender
|
||||||
from lib.lib import (
|
from lib.common import (
|
||||||
create_mobile_driver,
|
create_mobile_driver,
|
||||||
get_start_end_dates,
|
get_start_end_dates,
|
||||||
parse_korean_date,
|
parse_korean_date,
|
||||||
@ -21,7 +22,13 @@ from lib.lib import (
|
|||||||
send_failure_message
|
send_failure_message
|
||||||
)
|
)
|
||||||
|
|
||||||
class NaverReviewMapCollector:
|
|
||||||
|
def debug(msg):
|
||||||
|
if DEBUG:
|
||||||
|
print(f"[DEBUG] {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
class NaverMapReviewCollector:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.driver = None
|
self.driver = None
|
||||||
self.total_reviews = 0
|
self.total_reviews = 0
|
||||||
@ -30,40 +37,46 @@ class NaverReviewMapCollector:
|
|||||||
|
|
||||||
def extract_reviews(self):
|
def extract_reviews(self):
|
||||||
reviews = []
|
reviews = []
|
||||||
WebDriverWait(self.driver, 10).until(
|
try:
|
||||||
EC.presence_of_element_located((By.ID, "_review_list"))
|
WebDriverWait(self.driver, 10).until(
|
||||||
)
|
EC.presence_of_element_located((By.ID, "_review_list"))
|
||||||
ul = self.driver.find_element(By.ID, "_review_list")
|
)
|
||||||
items = ul.find_elements(By.XPATH, './/li[contains(@class, "place_apply_pui")]')
|
ul = self.driver.find_element(By.ID, "_review_list")
|
||||||
for item in items:
|
items = ul.find_elements(By.XPATH, './/li[contains(@class, "place_apply_pui")]')
|
||||||
try:
|
|
||||||
writer = "익명"
|
|
||||||
try:
|
|
||||||
writer = item.find_element(By.XPATH, "./div[1]/a[2]/div/span/span").text.strip()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
date_obj = None
|
for item in items:
|
||||||
try:
|
try:
|
||||||
date_text = item.find_element(By.XPATH, "./div[7]/div[2]/div/span[1]/span[2]").text.strip()
|
writer = "익명"
|
||||||
date_obj = parse_korean_date(date_text)
|
try:
|
||||||
except:
|
writer = item.find_element(By.XPATH, "./div[1]/a[2]/div/span/span").text.strip()
|
||||||
continue
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
text = ""
|
try:
|
||||||
try:
|
date_text = item.find_element(By.XPATH, "./div[7]/div[2]/div/span[1]/span[2]").text.strip()
|
||||||
text = item.find_element(By.XPATH, "./div[5]/a").get_attribute("innerHTML")
|
date_obj = parse_korean_date(date_text)
|
||||||
except:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not (self.start_date <= date_obj <= self.end_date):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
text_html = item.find_element(By.XPATH, "./div[5]/a").get_attribute("innerHTML")
|
||||||
|
content = clean_html_text(text_html)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
if date_obj and (self.start_date <= date_obj <= self.end_date):
|
|
||||||
reviews.append({
|
reviews.append({
|
||||||
"작성자": writer,
|
"작성자": writer,
|
||||||
"작성일": date_obj,
|
"작성일": date_obj,
|
||||||
"내용": clean_html_text(text)
|
"내용": content
|
||||||
})
|
})
|
||||||
except Exception as e:
|
|
||||||
print(f"[WARN] 리뷰 추출 실패: {e}")
|
except Exception as e:
|
||||||
|
debug(f"[WARN] 리뷰 항목 처리 중 오류: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
debug(f"[ERROR] 리뷰 리스트 접근 실패: {e}")
|
||||||
return reviews
|
return reviews
|
||||||
|
|
||||||
def send_to_message(self):
|
def send_to_message(self):
|
||||||
@ -91,9 +104,10 @@ class NaverReviewMapCollector:
|
|||||||
lines.append("---")
|
lines.append("---")
|
||||||
|
|
||||||
message = "\n".join(lines)
|
message = "\n".join(lines)
|
||||||
|
|
||||||
if not MESSAGE_PLATFORMS:
|
if not MESSAGE_PLATFORMS:
|
||||||
print("[WARN] 메시지 전송 플랫폼 없음")
|
print("[WARN] 메시지 전송 플랫폼이 지정되지 않음")
|
||||||
print(f"[DEBUG] {message}")
|
debug(message)
|
||||||
return
|
return
|
||||||
|
|
||||||
sender = MessageSender(
|
sender = MessageSender(
|
||||||
@ -103,8 +117,8 @@ class NaverReviewMapCollector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
print("[DEBUG] 디버그 모드로 메시지 미전송")
|
debug("디버그 모드로 메시지 전송 생략")
|
||||||
print(message)
|
debug(message)
|
||||||
else:
|
else:
|
||||||
sender.send(message, platforms=MESSAGE_PLATFORMS, use_webhook=False)
|
sender.send(message, platforms=MESSAGE_PLATFORMS, use_webhook=False)
|
||||||
|
|
||||||
@ -114,10 +128,15 @@ class NaverReviewMapCollector:
|
|||||||
for place_id in PLACE_IDS:
|
for place_id in PLACE_IDS:
|
||||||
url = f"https://m.place.naver.com/place/{place_id}/review/visitor?reviewSort=recent"
|
url = f"https://m.place.naver.com/place/{place_id}/review/visitor?reviewSort=recent"
|
||||||
print(f"[INFO] 접근: {url}")
|
print(f"[INFO] 접근: {url}")
|
||||||
self.driver.get(url)
|
try:
|
||||||
shop_name = extract_shop_name(self.driver)
|
self.driver.get(url)
|
||||||
|
shop_name = extract_shop_name(self.driver)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] {place_id} 매장 접근 오류: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
all_reviews = []
|
all_reviews = []
|
||||||
seen = set()
|
seen = set() # (작성자, 작성일, 내용) 기준으로 중복 제거
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
new_reviews = self.extract_reviews()
|
new_reviews = self.extract_reviews()
|
||||||
@ -135,6 +154,7 @@ class NaverReviewMapCollector:
|
|||||||
break
|
break
|
||||||
|
|
||||||
all_reviews.extend(filtered)
|
all_reviews.extend(filtered)
|
||||||
|
|
||||||
if not click_more(self.driver):
|
if not click_more(self.driver):
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -154,6 +174,7 @@ class NaverReviewMapCollector:
|
|||||||
else:
|
else:
|
||||||
self.send_to_message()
|
self.send_to_message()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
collector = NaverReviewMapCollector()
|
collector = NaverMapReviewCollector()
|
||||||
collector.run()
|
collector.run()
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# send_message.py
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
class MessageSender:
|
class MessageSender:
|
||||||
|
|||||||
2
naver_review_bot.bat
Normal file
2
naver_review_bot.bat
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
python C:\DEV\python\fg-auto\naver_review\run.py
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
selenium
|
||||||
|
requests
|
||||||
|
undetected_chromedriver
|
||||||
|
dotenv
|
||||||
43
run.py
43
run.py
@ -1,28 +1,27 @@
|
|||||||
import os
|
# run.py
|
||||||
import sys
|
import os, sys
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
|
|
||||||
# 환경 변수 로드
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# 프로젝트 루트 기준 경로 추가
|
# 프로젝트 루트 기준 경로 추가
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
|
||||||
|
|
||||||
# 실행 모드 확인
|
from conf import config
|
||||||
mode = os.getenv("MODE", "").strip().lower()
|
|
||||||
|
|
||||||
if mode == "biz":
|
def main():
|
||||||
from lib.biz_crawler import NaverReviewCollector
|
mode = os.getenv("MODE", "").strip().lower()
|
||||||
print("[INFO] 비즈니스 리뷰 수집기 실행")
|
|
||||||
collector = NaverReviewCollector()
|
|
||||||
collector.run()
|
|
||||||
|
|
||||||
elif mode == "map":
|
if mode == "biz":
|
||||||
from lib.naver_review_crawler import NaverMapReviewCollector
|
from lib.biz_crawler import NaverReviewCollector
|
||||||
print("[INFO] 지도 리뷰 수집기 실행")
|
print("[INFO] 비즈니스 리뷰 수집기 실행")
|
||||||
collector = NaverMapReviewCollector()
|
collector = NaverReviewCollector()
|
||||||
collector.run()
|
collector.run()
|
||||||
|
|
||||||
else:
|
elif mode == "map":
|
||||||
print("[ERROR] .env 파일에서 MODE 값을 설정해주세요. (biz 또는 map)")
|
from lib.naver_review_crawler import NaverMapReviewCollector
|
||||||
|
print("[INFO] 지도 리뷰 수집기 실행")
|
||||||
|
collector = NaverMapReviewCollector()
|
||||||
|
collector.run()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] .env 파일에서 MODE 값을 설정해주세요. (biz 또는 map) 현재값: '{mode}'")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user