...
This commit is contained in:
parent
c67e2b3564
commit
f4d75d5f87
6
main.py
6
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: # 로그인 실패 시
|
||||
|
|
|
|||
|
|
@ -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_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