diff --git a/main.py b/main.py index f4f36f7..add72fa 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ import sys import logging from PySide6.QtWidgets import QApplication -from src.gui import TaobaoScraperApp +from src.mainWindow import MainWindow from src.databaseManager import DatabaseManager from src.loggerModule import Logger 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) db_manager = DatabaseManager(logger) # 데이터베이스 매니저 인스턴스 생성 - window = TaobaoScraperApp(logger, db_manager) + window = MainWindow(logger, db_manager) # 로그인 다이얼로그 실행 login_dialog = UserInfoDialog() if login_dialog.exec(): # 로그인 성공 시 - window = TaobaoScraperApp(logger, db_manager) + window = MainWindow(logger, db_manager) window.show() sys.exit(app.exec()) # 메인 UI 실행 else: # 로그인 실패 시 diff --git a/main.py.bak b/main.py.bak new file mode 100644 index 0000000..f4f36f7 --- /dev/null +++ b/main.py.bak @@ -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 해제 diff --git a/src/Scrapper1.py b/src/Scrapper1.py new file mode 100644 index 0000000..b06029c --- /dev/null +++ b/src/Scrapper1.py @@ -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()) diff --git a/src/config.ini b/src/config.ini index 86e8025..5701f78 100644 --- a/src/config.ini +++ b/src/config.ini @@ -8,3 +8,11 @@ API_KEY = X9Tz3JqC/JcCwxnNewA6qdloIN6QFIitVBgS1a2KVDYk1AmddaDTvzr6+t3dyLZV3gh2TP banned_tags = 오늘출발, 오늘발송 banned_words = 금지어1, 금지어2, 금지어3 disallowed_words = 비허용단어1, 비허용단어2, 비허용단어3 + + +[SearchCategory] +패션 = 여성의류:女装, 남성의류:男装, 아동의류:童装 +가전 = 스마트폰:手机, 노트북:笔记本电脑, 태블릿:平板电脑 +생활 = 주방용품:厨房用品, 청소기:吸尘器, 가구:家具 +도서 = 소설:小说, 자기계발:自我发展, 역사:历史 +음식 = 한식:韩国料理, 중식:中国料理, 일식:日本料理 diff --git a/src/mainWindow.py b/src/mainWindow.py new file mode 100644 index 0000000..bdb0f0b --- /dev/null +++ b/src/mainWindow.py @@ -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(""" +

Taobao Scraper 도움말

+

본 프로그램은 Taobao 데이터를 스크래핑하고 엑셀 파일로 출력하거나, DB를 후처리하는 기능을 제공합니다.

+ +

추가적인 문의 사항이 있으면 개발자에게 문의하세요.

+ """) + 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() diff --git a/src/playwright_thread.py b/src/playwright_thread.py.bak similarity index 100% rename from src/playwright_thread.py rename to src/playwright_thread.py.bak