...
This commit is contained in:
parent
c67e2b3564
commit
f4d75d5f87
6
main.py
6
main.py
|
|
@ -1,7 +1,7 @@
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
from src.gui import TaobaoScraperApp
|
from src.mainWindow import MainWindow
|
||||||
from src.databaseManager import DatabaseManager
|
from src.databaseManager import DatabaseManager
|
||||||
from src.loggerModule import Logger
|
from src.loggerModule import Logger
|
||||||
from src.user_info_dialog import UserInfoDialog
|
from src.user_info_dialog import UserInfoDialog
|
||||||
|
|
@ -26,12 +26,12 @@ if __name__ == "__main__":
|
||||||
logger = Logger(log_file="Scrapper2.log", logger_name="Scrapper_Logger", level=logging.INFO)
|
logger = Logger(log_file="Scrapper2.log", logger_name="Scrapper_Logger", level=logging.INFO)
|
||||||
|
|
||||||
db_manager = DatabaseManager(logger) # 데이터베이스 매니저 인스턴스 생성
|
db_manager = DatabaseManager(logger) # 데이터베이스 매니저 인스턴스 생성
|
||||||
window = TaobaoScraperApp(logger, db_manager)
|
window = MainWindow(logger, db_manager)
|
||||||
|
|
||||||
# 로그인 다이얼로그 실행
|
# 로그인 다이얼로그 실행
|
||||||
login_dialog = UserInfoDialog()
|
login_dialog = UserInfoDialog()
|
||||||
if login_dialog.exec(): # 로그인 성공 시
|
if login_dialog.exec(): # 로그인 성공 시
|
||||||
window = TaobaoScraperApp(logger, db_manager)
|
window = MainWindow(logger, db_manager)
|
||||||
window.show()
|
window.show()
|
||||||
sys.exit(app.exec()) # 메인 UI 실행
|
sys.exit(app.exec()) # 메인 UI 실행
|
||||||
else: # 로그인 실패 시
|
else: # 로그인 실패 시
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
from src.gui import TaobaoScraperApp
|
||||||
|
from src.databaseManager import DatabaseManager
|
||||||
|
from src.loggerModule import Logger
|
||||||
|
from src.user_info_dialog import UserInfoDialog
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
|
||||||
|
# COM 초기화 (멀티스레드 모드)
|
||||||
|
def initialize_com():
|
||||||
|
COINIT_MULTITHREADED = 0x0
|
||||||
|
ctypes.windll.ole32.CoInitializeEx(None, COINIT_MULTITHREADED)
|
||||||
|
|
||||||
|
# COM 해제
|
||||||
|
def uninitialize_com():
|
||||||
|
ctypes.windll.ole32.CoUninitialize()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
initialize_com() # COM 초기화
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
logger = Logger(log_file="Scrapper2.log", logger_name="Scrapper_Logger", level=logging.INFO)
|
||||||
|
|
||||||
|
db_manager = DatabaseManager(logger) # 데이터베이스 매니저 인스턴스 생성
|
||||||
|
window = TaobaoScraperApp(logger, db_manager)
|
||||||
|
|
||||||
|
# 로그인 다이얼로그 실행
|
||||||
|
login_dialog = UserInfoDialog()
|
||||||
|
if login_dialog.exec(): # 로그인 성공 시
|
||||||
|
window = TaobaoScraperApp(logger, db_manager)
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec()) # 메인 UI 실행
|
||||||
|
else: # 로그인 실패 시
|
||||||
|
sys.exit(0) # 프로그램 종료
|
||||||
|
|
||||||
|
uninitialize_com() # COM 해제
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from PySide6.QtCore import QThread, Signal
|
||||||
|
from playwright.async_api import async_playwright, Page
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Scrapper1(QThread):
|
||||||
|
data_collected = Signal(bool, str)
|
||||||
|
progress_signal = Signal(int) # 진행률을 전달하는 시그널
|
||||||
|
login_complete = Signal() # 로그인 완료 시 MainWindow에 알림
|
||||||
|
|
||||||
|
def __init__(self, logger, db_manager):
|
||||||
|
super().__init__()
|
||||||
|
self.logger = logger
|
||||||
|
self.db_manager = db_manager
|
||||||
|
self.search_query = ""
|
||||||
|
self.login_detected = False
|
||||||
|
|
||||||
|
async def collect_data(self):
|
||||||
|
try:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
# 브라우저 경로 설정
|
||||||
|
browser_path = None
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
browser_path = os.path.join(os.path.dirname(sys.executable), 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
|
||||||
|
else:
|
||||||
|
browser_path = os.path.join(os.path.dirname(__file__), 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
|
||||||
|
|
||||||
|
self.logger.log(f"브라우저 경로: {browser_path}", level=logging.DEBUG)
|
||||||
|
|
||||||
|
# 사용자 에이전트 설정
|
||||||
|
user_agent = random.choice([
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.0.0",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/85.0.0.0",
|
||||||
|
])
|
||||||
|
self.logger.log(f"user_agent: {user_agent}", level=logging.DEBUG)
|
||||||
|
|
||||||
|
# 브라우저 시작 (headless 모드)
|
||||||
|
browser = await p.chromium.launch(
|
||||||
|
headless=True, # headless 모드로 설정
|
||||||
|
executable_path=browser_path,
|
||||||
|
args=[
|
||||||
|
'--disable-popup-blocking',
|
||||||
|
'--start-maximized',
|
||||||
|
'--window-size=1920,1080'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 시크릿 브라우저 컨텍스트 생성
|
||||||
|
context = await browser.new_context(
|
||||||
|
user_agent=user_agent,
|
||||||
|
geolocation={"latitude": 37.5665, "longitude": 126.9780},
|
||||||
|
locale="ko-KR",
|
||||||
|
permissions=["geolocation", "notifications"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 페이지 열기
|
||||||
|
page = await context.new_page()
|
||||||
|
# await page.goto("https://world.taobao.com")
|
||||||
|
await page.goto("https://s.taobao.com/search?commend=all&ie=utf8page=1&q=%E5%A5%B3%E5%A3%AB%E8%A5%BF%E6%9C%8D&search_type=item")
|
||||||
|
|
||||||
|
self.logger.log(f"접속 완료.", level=logging.INFO)
|
||||||
|
|
||||||
|
# 페이지 로딩 확인 및 pagedown
|
||||||
|
# await page.wait_for_selector(".tb-pick-content-item") # 상품 카드 로딩 대기
|
||||||
|
await page.wait_for_selector("doubleCard--gO3Bz6bu") # 상품 카드 로딩 대기
|
||||||
|
|
||||||
|
self.logger.log(f"페이지 로딩 완료", level=logging.INFO)
|
||||||
|
await page.keyboard.press("PageDown")
|
||||||
|
await page.wait_for_timeout(1000) # 1초 대기
|
||||||
|
await page.keyboard.press("PageDown")
|
||||||
|
await page.wait_for_timeout(2000) # 추가 2초 대기 (상품 로딩)
|
||||||
|
|
||||||
|
# 상품 수집
|
||||||
|
items_data = await self.scrape_items(page)
|
||||||
|
if items_data:
|
||||||
|
|
||||||
|
# 중복 필터링 후 새 상품만 추가
|
||||||
|
self.db_manager.insert_items(items_data)
|
||||||
|
|
||||||
|
self.data_collected.emit(True, "데이터 수집 완료")
|
||||||
|
else:
|
||||||
|
self.data_collected.emit(False, "데이터 수집 실패")
|
||||||
|
|
||||||
|
await context.close()
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.log(f"브라우저 작업 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||||
|
self.data_collected.emit(False, f"오류 발생: {e}", exec=True)
|
||||||
|
|
||||||
|
async def perform_qr_login(self):
|
||||||
|
try:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
# 1. 로그인 페이지 접속
|
||||||
|
if getattr(os, 'frozen', False):
|
||||||
|
browser_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
|
||||||
|
else:
|
||||||
|
browser_path = os.path.join(os.path.dirname(__file__), 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
|
||||||
|
|
||||||
|
user_agent = random.choice([
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
|
||||||
|
# ... (기타 user agent)
|
||||||
|
])
|
||||||
|
|
||||||
|
browser = await p.chromium.launch(
|
||||||
|
headless=False, # QR 로그인은 창이 보여야 함
|
||||||
|
executable_path=browser_path,
|
||||||
|
args=[
|
||||||
|
'--disable-popup-blocking',
|
||||||
|
'--start-maximized',
|
||||||
|
'--window-size=1920,1080'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
context = await browser.new_context(
|
||||||
|
user_agent=user_agent,
|
||||||
|
geolocation={"latitude": 37.5665, "longitude": 126.9780},
|
||||||
|
locale="ko-KR",
|
||||||
|
permissions=["geolocation", "notifications"]
|
||||||
|
)
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
login_url = "https://login.taobao.com/member/login.jhtml"
|
||||||
|
await page.goto(login_url)
|
||||||
|
|
||||||
|
# 2. QR 페이지 스크린샷 캡쳐 후 저장
|
||||||
|
await page.wait_for_selector("div#qrcode-img canvas", timeout=10000)
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
qr_canvas = await page.query_selector("div#qrcode-img canvas")
|
||||||
|
if not qr_canvas:
|
||||||
|
self.logger.log("QR 코드 캔버스 찾지 못함", level=30)
|
||||||
|
await browser.close()
|
||||||
|
return None, None, None
|
||||||
|
img_bytes = await qr_canvas.screenshot()
|
||||||
|
qr_path = os.path.join(os.getcwd(), "temp_qr.png")
|
||||||
|
with open(qr_path, "wb") as f:
|
||||||
|
f.write(img_bytes)
|
||||||
|
self.logger.log("QR 코드 캡쳐 및 저장 완료", level=20)
|
||||||
|
|
||||||
|
return browser, page, login_url, qr_path
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.log(f"QR 로그인 수행 중 오류: {e}", level=40)
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
async def monitor_login(self, page, login_url, timeout=90):
|
||||||
|
"""
|
||||||
|
1초마다 현재 URL을 감시하여, QR 페이지 URL과 달라지면 로그인 완료로 판단.
|
||||||
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
current_url = page.url
|
||||||
|
if current_url != login_url:
|
||||||
|
self.logger.log(f"로그인 완료 감지: {current_url}", level=20)
|
||||||
|
self.login_detected = True
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return self.login_detected
|
||||||
|
|
||||||
|
async def scrape_items(self, page: Page):
|
||||||
|
try:
|
||||||
|
items = await page.query_selector_all("div#ice-container div.tb-pick-feeds-container > div")
|
||||||
|
total_items = len(items)
|
||||||
|
self.logger.log(f"총 {len(items)}개의 상품 카드가 발견되었습니다.", level=logging.DEBUG)
|
||||||
|
|
||||||
|
items_data = []
|
||||||
|
for idx, item in enumerate(items):
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 카드 처리 중 - XPath 확인", level=logging.DEBUG)
|
||||||
|
|
||||||
|
# a 태그를 명시적으로 선택하여 href 가져오기
|
||||||
|
link_element = await item.query_selector("a.item-link")
|
||||||
|
if link_element:
|
||||||
|
item_href = await link_element.get_attribute("href")
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 href: {item_href}", level=logging.DEBUG)
|
||||||
|
|
||||||
|
if item_href:
|
||||||
|
# 숫자만 추출하고 9~12자리 필터링
|
||||||
|
item_id_match = re.search(r'(\d{9,12})', item_href)
|
||||||
|
if item_id_match:
|
||||||
|
item_id = item_id_match.group(1)
|
||||||
|
pc_url = f"https://item.taobao.com/item.htm?id={item_id}"
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 ID: {item_id}, 상품 URL: {pc_url}", level=logging.DEBUG)
|
||||||
|
else:
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 ID를 올바르게 추출할 수 없습니다.", level=logging.WARNING)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 ID를 가져올 수 없습니다.", level=logging.WARNING)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품의 a 태그를 찾을 수 없습니다.", level=logging.WARNING)
|
||||||
|
continue
|
||||||
|
|
||||||
|
name_element = await item.query_selector(".info-wrapper-title-text")
|
||||||
|
name = await name_element.inner_text() if name_element else "N/A"
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품명: {name}", level=logging.DEBUG)
|
||||||
|
|
||||||
|
price_element = await item.query_selector(".price-value")
|
||||||
|
price = await price_element.inner_text() if price_element else "0"
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 가격: {price}", level=logging.DEBUG)
|
||||||
|
|
||||||
|
image_element = await item.query_selector(".img-wrapper")
|
||||||
|
image_style = await image_element.get_attribute("style") if image_element else ""
|
||||||
|
# image_url = image_style.split("url(")[-1].strip('")') if image_style else "N/A"
|
||||||
|
image_url = (image_style.split("url(")[-1].strip('");').strip("'").replace("//", "https://", 1) if image_style else "N/A")
|
||||||
|
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 이미지 URL: {image_url}", level=logging.DEBUG)
|
||||||
|
|
||||||
|
sales_element = await item.query_selector(".month-sale")
|
||||||
|
sales = await sales_element.inner_text() if sales_element else "0"
|
||||||
|
self.logger.log(f"{idx + 1}번째 상품 판매량: {sales}", level=logging.DEBUG)
|
||||||
|
|
||||||
|
items_data.append((item_id, pc_url, name, float(price), image_url, sales))
|
||||||
|
|
||||||
|
# 프로그레스바 업데이트
|
||||||
|
progress = int(((idx + 1) / total_items) * 100)
|
||||||
|
self.progress_signal.emit(progress)
|
||||||
|
|
||||||
|
|
||||||
|
self.logger.log(f"수집된 상품 수 : {len(items_data)}", level=logging.DEBUG)
|
||||||
|
return items_data
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.log(f"데이터 수집 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def wait_for_user(self):
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
asyncio.run(self.collect_data())
|
||||||
|
|
@ -8,3 +8,11 @@ API_KEY = X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TP
|
||||||
banned_tags = 오늘출발, 오늘발송
|
banned_tags = 오늘출발, 오늘발송
|
||||||
banned_words = 금지어1, 금지어2, 금지어3
|
banned_words = 금지어1, 금지어2, 금지어3
|
||||||
disallowed_words = 비허용단어1, 비허용단어2, 비허용단어3
|
disallowed_words = 비허용단어1, 비허용단어2, 비허용단어3
|
||||||
|
|
||||||
|
|
||||||
|
[SearchCategory]
|
||||||
|
패션 = 여성의류:女装, 남성의류:男装, 아동의류:童装
|
||||||
|
가전 = 스마트폰:手机, 노트북:笔记本电脑, 태블릿:平板电脑
|
||||||
|
생활 = 주방용품:厨房用品, 청소기:吸尘器, 가구:家具
|
||||||
|
도서 = 소설:小说, 자기계발:自我发展, 역사:历史
|
||||||
|
음식 = 한식:韩国料理, 중식:中国料理, 일식:日本料理
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,396 @@
|
||||||
|
import sys, os, configparser, logging
|
||||||
|
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox,
|
||||||
|
QTextBrowser, QDialog, QProgressBar, QTextEdit, QHBoxLayout, QMenuBar, QMenu, QComboBox)
|
||||||
|
from PySide6.QtGui import QAction, QPixmap
|
||||||
|
from PySide6.QtCore import Qt, Slot, Signal, QThread
|
||||||
|
from src.Scrapper1 import Scrapper1
|
||||||
|
from src.excel_export import ExcelExporter
|
||||||
|
from src.post_processor import PostProcessor
|
||||||
|
from src.xlsProcessingThread import XLSProcessingThread
|
||||||
|
from src.ProcessingThread_for_DB import ProcessingThread
|
||||||
|
from src.keyword.keyword_manager import KeywordManager
|
||||||
|
from src.categoryManager import CategoryManager
|
||||||
|
from src.user_info_dialog import UserInfoDialog
|
||||||
|
import os, asyncio, qasync
|
||||||
|
|
||||||
|
class MainWindow(QWidget):
|
||||||
|
def __init__(self, logger, db_manager):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.logger = logger
|
||||||
|
self.logger.set_gui_logger(self.update_log_window)
|
||||||
|
|
||||||
|
self.is_logged_in = False # 로그인 상태 플래그
|
||||||
|
|
||||||
|
self.db_manager = db_manager
|
||||||
|
self.setWindowTitle("내차는 언제타냐 - 역소싱기")
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# 메뉴바 생성
|
||||||
|
self.menu_bar = QMenuBar(self)
|
||||||
|
|
||||||
|
# 설정 메뉴
|
||||||
|
self.settings_menu = QMenu("설정", self)
|
||||||
|
self.menu_bar.addMenu(self.settings_menu)
|
||||||
|
|
||||||
|
# 설정 메뉴에 금지어 관리 추가
|
||||||
|
self.manage_keywords_action = QAction("금지어 관리", self)
|
||||||
|
self.manage_keywords_action.triggered.connect(self.manage_keywords)
|
||||||
|
self.settings_menu.addAction(self.manage_keywords_action)
|
||||||
|
|
||||||
|
# 설정 메뉴에 금지 카테고리 관리 추가
|
||||||
|
self.manage_category_action = QAction("금지 카테고리 관리", self)
|
||||||
|
self.manage_category_action.triggered.connect(self.manage_cat)
|
||||||
|
self.settings_menu.addAction(self.manage_category_action)
|
||||||
|
|
||||||
|
# 도움말 메뉴
|
||||||
|
self.help_menu = QMenu("도움말", self)
|
||||||
|
self.menu_bar.addMenu(self.help_menu)
|
||||||
|
|
||||||
|
# 도움말 메뉴에 항목 추가
|
||||||
|
self.help_action = QAction("도움말 보기", self)
|
||||||
|
self.help_action.triggered.connect(self.show_help_dialog)
|
||||||
|
self.help_menu.addAction(self.help_action)
|
||||||
|
|
||||||
|
# 사용자 정보 메뉴
|
||||||
|
self.user_menu = QMenu("사용자 정보", self)
|
||||||
|
self.menu_bar.addMenu(self.user_menu)
|
||||||
|
|
||||||
|
# 사용자 정보 메뉴에 항목 추가
|
||||||
|
self.user_info_action = QAction("사용자 정보 보기", self)
|
||||||
|
self.user_info_action.triggered.connect(self.show_user_info_dialog)
|
||||||
|
self.user_menu.addAction(self.user_info_action)
|
||||||
|
|
||||||
|
# 로그창 추가
|
||||||
|
self.log_text_edit = QTextEdit()
|
||||||
|
self.log_text_edit.setReadOnly(True)
|
||||||
|
self.log_text_edit.setMinimumHeight(200)
|
||||||
|
|
||||||
|
# 프로그레스바 추가
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setAlignment(Qt.AlignCenter)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
|
||||||
|
# 상품분류 콤보박스 (대분류와 소분류)
|
||||||
|
self.category_combo = QComboBox()
|
||||||
|
self.subcategory_combo = QComboBox()
|
||||||
|
category_layout = QHBoxLayout()
|
||||||
|
category_layout.addWidget(QLabel("상품 대분류:"))
|
||||||
|
category_layout.addWidget(self.category_combo)
|
||||||
|
category_layout.addWidget(QLabel("상품 소분류:"))
|
||||||
|
category_layout.addWidget(self.subcategory_combo)
|
||||||
|
|
||||||
|
# 시작, 엑셀출력, 후처리, 닫기 버튼
|
||||||
|
# 로그인 버튼, 시작 버튼 등
|
||||||
|
self.login_button = QPushButton("로그인")
|
||||||
|
self.login_button.clicked.connect(self.on_login_button_clicked)
|
||||||
|
|
||||||
|
self.start_button = QPushButton("시작")
|
||||||
|
self.start_button.setEnabled(False)
|
||||||
|
|
||||||
|
self.start_button.clicked.connect(self.start_scraping)
|
||||||
|
self.excel_button = QPushButton("엑셀출력")
|
||||||
|
self.excel_button.clicked.connect(self.save_to_excel)
|
||||||
|
self.post_db__button = QPushButton("후처리")
|
||||||
|
self.post_db__button.clicked.connect(self.post_process_by_DB)
|
||||||
|
self.close_button = QPushButton("닫기")
|
||||||
|
self.close_button.clicked.connect(self.close)
|
||||||
|
|
||||||
|
# 버튼 레이아웃
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.addWidget(self.login_button)
|
||||||
|
button_layout.addWidget(self.start_button)
|
||||||
|
button_layout.addWidget(self.post_db__button)
|
||||||
|
button_layout.addWidget(self.excel_button)
|
||||||
|
button_layout.addWidget(self.close_button)
|
||||||
|
|
||||||
|
# 전체 레이아웃 구성
|
||||||
|
self.layout.addWidget(self.menu_bar)
|
||||||
|
self.layout.addLayout(category_layout) # 상품분류 선택 영역 추가
|
||||||
|
self.layout.addLayout(button_layout)
|
||||||
|
self.layout.addWidget(QLabel("진행 상황"))
|
||||||
|
self.layout.addWidget(self.progress_bar)
|
||||||
|
self.layout.addWidget(QLabel("로그 출력"))
|
||||||
|
self.layout.addWidget(self.log_text_edit)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
# QR 이미지 표시를 위한 QLabel (초기에는 숨김)
|
||||||
|
self.qr_label = QLabel()
|
||||||
|
self.qr_label.setVisible(False)
|
||||||
|
self.layout.addWidget(self.qr_label)
|
||||||
|
|
||||||
|
# base_dir 경로 가져오기
|
||||||
|
base_dir = self.get_base_dir()
|
||||||
|
self.xls_file_path = os.path.join(base_dir, "baseXLS_Percenty.xlsx")
|
||||||
|
config_path = os.path.join(base_dir, "config.ini")
|
||||||
|
self.config = self.load_config(config_path)
|
||||||
|
|
||||||
|
# 콤보박스 초기화 (config.ini의 [Category] 섹션 사용)
|
||||||
|
self.category_dict = {} # 대분류 -> (소분류_표시, 검색어) 리스트
|
||||||
|
self.populate_category_combobox()
|
||||||
|
|
||||||
|
# PlaywrightThread에 전달할 검색어를 초기화하기 위해 빈 값 설정
|
||||||
|
self.search_query = ""
|
||||||
|
|
||||||
|
self.scrapper1 = Scrapper1(self.logger, self.db_manager)
|
||||||
|
self.scrapper1.data_collected.connect(self.on_data_collected)
|
||||||
|
self.scrapper1.progress_signal.connect(self.update_progress_bar)
|
||||||
|
self.scrapper1.login_complete.connect(self.on_login_complete)
|
||||||
|
|
||||||
|
self.categoryManager = CategoryManager(self.logger, self.xls_file_path)
|
||||||
|
|
||||||
|
self.excel_exporter = ExcelExporter(logger=self.logger, db_manager=self.db_manager)
|
||||||
|
self.excel_exporter.progress_signal.connect(self.update_progress_bar)
|
||||||
|
|
||||||
|
self.postProcessor = PostProcessor(self.logger, self.db_manager, self.config, self.categoryManager)
|
||||||
|
self.keyword_manager = KeywordManager(logger=self.logger, config=self.config, parent=self)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def on_login_button_clicked(self):
|
||||||
|
# qasync를 사용하여 비동기 로그인 수행
|
||||||
|
qasync.ensure_future(self.perform_login())
|
||||||
|
|
||||||
|
async def perform_login(self):
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
self.logger.log("QR 로그인 시작", level=20)
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser, page, login_url, qr_path = await self.scrapper1.perform_qr_login()
|
||||||
|
if not qr_path:
|
||||||
|
QMessageBox.warning(self, "오류", "QR 코드 캡쳐 실패")
|
||||||
|
return
|
||||||
|
# QR 이미지 파일 경로를 받아서 표시
|
||||||
|
pixmap = QPixmap(qr_path)
|
||||||
|
self.qr_label.setPixmap(pixmap)
|
||||||
|
self.qr_label.setVisible(True)
|
||||||
|
|
||||||
|
# 90초 동안 QR 표시, 이후 자동으로 닫힘
|
||||||
|
self.logger.log("QR 코드 표시 (최대 90초)", level=20)
|
||||||
|
# 동시에 로그인 모니터링 시작
|
||||||
|
login_task = asyncio.create_task(self.scrapper1.monitor_login(page, login_url, timeout=90))
|
||||||
|
# 90초 후 자동 QR 창 닫기
|
||||||
|
await asyncio.sleep(90)
|
||||||
|
self.qr_label.setVisible(False)
|
||||||
|
if await login_task:
|
||||||
|
self.logger.log("로그인 완료 감지됨", level=20)
|
||||||
|
self.start_button.setEnabled(True)
|
||||||
|
self.login_button.setEnabled(False)
|
||||||
|
else:
|
||||||
|
self.logger.log("로그인 시간 초과", level=30)
|
||||||
|
QMessageBox.warning(self, "로그인 실패", "로그인 시간 초과")
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def start_scraping(self):
|
||||||
|
# 사용자가 로그인 완료 후 시작 버튼을 누르면,
|
||||||
|
# 기존 로그인 세션이 유지된 상태에서 검색 페이지로 이동하여 데이터 수집 시작
|
||||||
|
self.logger.log("시작 버튼 클릭 - 데이터 수집 시작", level=20)
|
||||||
|
# 예를 들어, Scrapper1.collect_data() 실행
|
||||||
|
self.scrapper1.start()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def on_login_complete(self):
|
||||||
|
# Scrapper1에서 로그인 완료 시 발생시키는 시그널 처리 (필요 시)
|
||||||
|
self.logger.log("MainWindow: 로그인 완료 알림 받음", level=20)
|
||||||
|
|
||||||
|
def load_config(self, file_path: str) -> configparser.ConfigParser:
|
||||||
|
"""
|
||||||
|
config.ini 파일을 읽어서 ConfigParser 객체로 반환
|
||||||
|
"""
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
config.read(file_path, encoding="utf-8") # UTF-8 인코딩으로 설정 파일 읽기
|
||||||
|
self.logger.log(f"Config 파일 '{file_path}' 로드 성공", level=logging.INFO)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.log(f"Config 파일 로드 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||||
|
return config
|
||||||
|
|
||||||
|
def get_base_dir(self):
|
||||||
|
"""
|
||||||
|
실행 환경에 따라 base_dir을 설정하는 메서드.
|
||||||
|
"""
|
||||||
|
if getattr(sys, 'frozen', False): # 패키징된 경우
|
||||||
|
base_dir = os.path.dirname(sys.executable)
|
||||||
|
internal_dir = os.path.join(base_dir, '_internal') # _internal 디렉토리 포함
|
||||||
|
if os.path.exists(internal_dir):
|
||||||
|
return internal_dir
|
||||||
|
else:
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
return base_dir
|
||||||
|
|
||||||
|
def populate_category_combobox(self):
|
||||||
|
"""
|
||||||
|
config.ini의 [SearchCategory] 섹션을 읽어 대분류와 소분류 콤보박스 초기화
|
||||||
|
"""
|
||||||
|
if "SearchCategory" not in self.config:
|
||||||
|
self.logger.log("config.ini에 [SearchCategory] 섹션이 없습니다.", level=logging.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.category_combo.clear()
|
||||||
|
self.category_dict.clear()
|
||||||
|
# 대분류 키들을 추가
|
||||||
|
for major in self.config["SearchCategory"]:
|
||||||
|
self.category_combo.addItem(major)
|
||||||
|
# 값은 "소분류1:중국어, 소분류2:중국어, ..." 형태
|
||||||
|
items = self.config["SearchCategory"][major].split(",")
|
||||||
|
sub_list = []
|
||||||
|
for item in items:
|
||||||
|
# 좌우 공백 제거 후 ':' 기준 분리
|
||||||
|
parts = item.strip().split(":")
|
||||||
|
if len(parts) == 2:
|
||||||
|
display, search = parts
|
||||||
|
sub_list.append((display.strip(), search.strip()))
|
||||||
|
self.category_dict[major] = sub_list
|
||||||
|
|
||||||
|
# 초기 대분류 선택 시 소분류 콤보박스 업데이트
|
||||||
|
self.category_combo.currentIndexChanged.connect(self.populate_subcategory_combobox)
|
||||||
|
self.populate_subcategory_combobox(0)
|
||||||
|
|
||||||
|
def populate_subcategory_combobox(self, index):
|
||||||
|
"""
|
||||||
|
대분류 선택에 따라 소분류 콤보박스를 업데이트
|
||||||
|
"""
|
||||||
|
self.subcategory_combo.clear()
|
||||||
|
if index < 0:
|
||||||
|
return
|
||||||
|
major = self.category_combo.itemText(index)
|
||||||
|
sub_list = self.category_dict.get(major, [])
|
||||||
|
for display, _ in sub_list:
|
||||||
|
self.subcategory_combo.addItem(display)
|
||||||
|
# 자동 선택 첫 소분류의 검색어 저장
|
||||||
|
if sub_list:
|
||||||
|
self.search_query = sub_list[0][1]
|
||||||
|
else:
|
||||||
|
self.search_query = ""
|
||||||
|
|
||||||
|
# 연결: 소분류 선택 변경 시 검색어 업데이트
|
||||||
|
self.subcategory_combo.currentIndexChanged.connect(self.update_search_query)
|
||||||
|
|
||||||
|
def update_search_query(self, index):
|
||||||
|
"""
|
||||||
|
소분류 선택이 변경되면 해당 중국어 검색어를 업데이트
|
||||||
|
"""
|
||||||
|
major = self.category_combo.currentText()
|
||||||
|
sub_list = self.category_dict.get(major, [])
|
||||||
|
if 0 <= index < len(sub_list):
|
||||||
|
self.search_query = sub_list[index][1]
|
||||||
|
self.logger.log(f"선택된 검색어: {self.search_query}", level=logging.DEBUG)
|
||||||
|
else:
|
||||||
|
self.search_query = ""
|
||||||
|
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def start_scraping(self):
|
||||||
|
"""
|
||||||
|
시작 버튼 클릭 시, 선택된 검색어를 playwright_thread에 전달 후 크롤링 시작
|
||||||
|
"""
|
||||||
|
if not self.search_query:
|
||||||
|
QMessageBox.warning(self, "오류", "상품분류를 올바르게 선택해주세요.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.log(f"선택된 검색어 '{self.search_query}'로 Playwright 스레드 시작.", level=logging.INFO)
|
||||||
|
# playwright_thread에 검색어 전달
|
||||||
|
self.scrapper1.search_query = self.search_query
|
||||||
|
|
||||||
|
self.progress_bar.setValue(1)
|
||||||
|
self.scrapper1.start()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def post_process_by_DB(self):
|
||||||
|
self.logger.log(f"DB로 후처리 작업을 시작합니다.", level=logging.INFO)
|
||||||
|
self.progress_bar.setValue(1)
|
||||||
|
self.db_thread = ProcessingThread(self.postProcessor, self.keyword_manager)
|
||||||
|
self.db_thread.progress.connect(self.on_DB_progress)
|
||||||
|
self.db_thread.start()
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def on_DB_progress(self, message):
|
||||||
|
self.logger.log(message, level=logging.INFO)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def manage_keywords(self):
|
||||||
|
self.logger.log("금지어 관리 버튼 클릭됨", level=logging.DEBUG)
|
||||||
|
self.keyword_manager.exec()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def manage_cat(self):
|
||||||
|
try:
|
||||||
|
if not os.path.exists(self.xls_file_path):
|
||||||
|
QMessageBox.warning(self, "파일 없음", f"'{self.xls_file_path}' 파일이 존재하지 않습니다.")
|
||||||
|
self.logger.log(f"'{self.xls_file_path}' 파일이 존재하지 않습니다.", level=logging.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
os.startfile(self.xls_file_path)
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"카테고리 관리",
|
||||||
|
"금지할 카테고리 옆에 False를 입력하면 금지, 비어있거나 True면 허용"
|
||||||
|
)
|
||||||
|
self.logger.log(f"'{self.xls_file_path}' 파일이 실행되었습니다.", level=logging.INFO)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "오류", f"엑셀 파일 열기 중 오류 발생: {e}")
|
||||||
|
self.logger.log(f"엑셀 파일 열기 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def on_folder_selected(self, selected_folder):
|
||||||
|
self.logger.log(f"선택된 폴더: {selected_folder}", level=logging.INFO)
|
||||||
|
self.xls_thread = XLSProcessingThread(self.postProcessor, selected_folder)
|
||||||
|
self.xls_thread.progress.connect(self.on_xls_progress)
|
||||||
|
self.xls_thread.start()
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def on_xls_progress(self, message):
|
||||||
|
self.logger.log(message, level=logging.INFO)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def save_to_excel(self):
|
||||||
|
self.progress_bar.setValue(1)
|
||||||
|
success = self.excel_exporter.save_to_excel()
|
||||||
|
if success:
|
||||||
|
QMessageBox.information(self, "엑셀 출력", "엑셀 파일로 저장 완료")
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "오류", "엑셀 저장 오류")
|
||||||
|
|
||||||
|
@Slot(bool, str)
|
||||||
|
def on_data_collected(self, success, message):
|
||||||
|
if success:
|
||||||
|
QMessageBox.information(self, "수집 완료", message)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "수집 실패", message)
|
||||||
|
|
||||||
|
@Slot(int)
|
||||||
|
def update_progress_bar(self, value):
|
||||||
|
self.progress_bar.setValue(value)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def update_log_window(self, message):
|
||||||
|
self.log_text_edit.append(message)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def show_help_dialog(self):
|
||||||
|
self.logger.log("도움말 메뉴 클릭됨", level=logging.INFO)
|
||||||
|
dialog = QDialog(self)
|
||||||
|
dialog.setWindowTitle("도움말")
|
||||||
|
dialog.setMinimumSize(400, 300)
|
||||||
|
dialog_layout = QVBoxLayout()
|
||||||
|
help_text_browser = QTextBrowser()
|
||||||
|
help_text_browser.setText("""
|
||||||
|
<h3>Taobao Scraper 도움말</h3>
|
||||||
|
<p>본 프로그램은 Taobao 데이터를 스크래핑하고 엑셀 파일로 출력하거나, DB를 후처리하는 기능을 제공합니다.</p>
|
||||||
|
<ul>
|
||||||
|
<li><b>시작:</b> 선택한 상품분류에 따라 데이터를 스크래핑 시작</li>
|
||||||
|
<li><b>엑셀 출력:</b> 데이터를 엑셀 파일로 저장</li>
|
||||||
|
<li><b>후처리:</b> DB의 데이터를 후처리</li>
|
||||||
|
<li><b>설정 > 금지어 관리:</b> 금지어 목록을 관리</li>
|
||||||
|
<li><b>설정 > 금지 카테고리 관리:</b> 금지된 카테고리를 설정</li>
|
||||||
|
</ul>
|
||||||
|
<p>추가적인 문의 사항이 있으면 개발자에게 문의하세요.</p>
|
||||||
|
""")
|
||||||
|
dialog_layout.addWidget(help_text_browser)
|
||||||
|
dialog.setLayout(dialog_layout)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def show_user_info_dialog(self):
|
||||||
|
self.logger.log("사용자 정보 메뉴 클릭됨", level=logging.INFO)
|
||||||
|
dialog = UserInfoDialog(self)
|
||||||
|
dialog.exec()
|
||||||
Loading…
Reference in New Issue