This commit is contained in:
Envy_PC 2025-02-14 16:41:59 +09:00
parent 3fce9b2018
commit a922dc2ba4
2 changed files with 308 additions and 158 deletions

View File

@ -9,147 +9,160 @@ 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에 알림
# QR 코드가 준비되면 QR 이미지 바이트를 전달 (메인윈도우에서 이미지 표시)
login_ready = Signal(bytes)
# 로그인 완료 시(현재 URL이 로그인 페이지와 달라지면) 신호 전달
login_complete = Signal()
# 스크래핑 완료 후 결과 전달
scraping_complete = Signal(bool, str)
# 진행률 업데이트용 시그널 (필요 시)
progress_signal = Signal(int)
def __init__(self, logger, db_manager):
super().__init__()
self.logger = logger
self.db_manager = db_manager
self.search_query = ""
self.login_detected = False
self.search_query = "" # 메인윈도우에서 할당(중국어 검색어)
self.browser = None
self.context = None
self.page = None
self.playwright = None
self.loop = None
self._initialized = 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')
# 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)
# 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)
# # 사용자 에이전트 설정
# 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'
]
)
# # 브라우저 시작 (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"]
)
# # 시크릿 브라우저 컨텍스트 생성
# 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")
# # 페이지 열기
# 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)
# self.logger.log(f"접속 완료.", level=logging.INFO)
# 페이지 로딩 확인 및 pagedown
# await page.wait_for_selector(".tb-pick-content-item") # 상품 카드 로딩 대기
await page.wait_for_selector("doubleCard--gO3Bz6bu") # 상품 카드 로딩 대기
# # 페이지 로딩 확인 및 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초 대기 (상품 로딩)
# 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:
# # 상품 수집
# items_data = await self.scrape_items(page)
# if items_data:
# 중복 필터링 후 새 상품만 추가
self.db_manager.insert_items(items_data)
# # 중복 필터링 후 새 상품만 추가
# self.db_manager.insert_items(items_data)
self.data_collected.emit(True, "데이터 수집 완료")
else:
self.data_collected.emit(False, "데이터 수집 실패")
# self.data_collected.emit(True, "데이터 수집 완료")
# else:
# self.data_collected.emit(False, "데이터 수집 실패")
await context.close()
await browser.close()
# 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)
# 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')
# 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)
])
# 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'
]
)
# 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"]
)
# 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)
# 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)
# # 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
# return browser, page, login_url, qr_path
except Exception as e:
self.logger.log(f"QR 로그인 수행 중 오류: {e}", level=40)
return None, None, None, None
# 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):
"""
@ -234,4 +247,117 @@ class Scrapper1(QThread):
await asyncio.sleep(2)
def run(self):
asyncio.run(self.collect_data())
"""
QThread의 run()에서 별도의 asyncio 이벤트 루프를 생성하여
브라우저( playwright 객체) 초기화한 대기상태로 둡니다.
"""
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self.start_browser())
# 이벤트 루프를 계속 돌려서 앞으로 호출되는 코루틴들을 처리합니다.
self.loop.run_forever()
self.loop.close()
async def start_browser(self):
"""
async_playwright를 이용해 브라우저를 실행하고,
초기 페이지( 페이지) 준비합니다.
"""
try:
self.playwright = await async_playwright().start()
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')
# 로그인 시에는 headless=False (로그인 창이 보여야 함)
self.browser = await self.playwright.chromium.launch(
headless=False,
executable_path=browser_path,
args=[
'--disable-popup-blocking',
'--start-maximized',
'--window-size=1920,1080'
]
)
self.context = await self.browser.new_context()
self.page = await self.context.new_page()
self._initialized = True
self.logger.log("브라우저 초기화 완료", level=20)
except Exception as e:
self.logger.log(f"브라우저 초기화 오류: {e}", level=40)
def start_login(self):
"""
메인윈도우에서 로그인 버튼 클릭 호출.
QThread의 이벤트 루프에서 _start_login() 코루틴을 실행합니다.
"""
if not self._initialized:
self.logger.log("브라우저가 초기화되지 않음", level=40)
return
asyncio.run_coroutine_threadsafe(self._start_login(), self.loop)
async def _start_login(self):
"""
로그인 페이지로 이동하여 QR 코드를 캡쳐하고, 메인윈도우에 전달한
1초마다 현재 URL을 감시하여 로그인 완료 여부를 판단합니다.
"""
login_url = "https://login.taobao.com/member/login.jhtml"
await self.page.goto(login_url)
self.logger.log("로그인 페이지 접속", level=20)
try:
await self.page.wait_for_selector("div#qrcode-img canvas", timeout=10000)
except Exception as e:
self.logger.log(f"QR 코드 로드 오류: {e}", level=40)
return
await asyncio.sleep(2) # QR 코드 렌더링 대기
qr_canvas = await self.page.query_selector("div#qrcode-img canvas")
if not qr_canvas:
self.logger.log("QR 코드 캔버스 없음", level=40)
return
qr_bytes = await qr_canvas.screenshot()
# 메인윈도우에 QR 이미지(바이트)를 전달
self.login_ready.emit(qr_bytes)
# 로그인 감시 시작 (90초 제한)
asyncio.create_task(self._monitor_login(login_url, timeout=90))
async def _monitor_login(self, login_url, timeout=90):
"""
1초마다 현재 URL을 확인하여, 로그인 페이지 URL과 달라지면
로그인 완료로 판단하고 login_complete 시그널을 emit합니다.
"""
start_time = time.time()
while time.time() - start_time < timeout:
current_url = self.page.url
if current_url != login_url:
self.logger.log(f"로그인 완료 감지: {current_url}", level=20)
self.login_complete.emit()
return
await asyncio.sleep(1)
self.logger.log("로그인 시간 초과", level=30)
# 시간 초과 시 별도 처리 가능 (예: 실패 시그널 emit 등)
def start_scraping(self, search_query):
"""
메인윈도우에서 시작 버튼 클릭 호출.
동일한 playwright 객체를 재활용하여 검색 페이지에 접속하고 데이터를 수집합니다.
"""
self.search_query = search_query
if not self._initialized:
self.logger.log("브라우저 초기화 안됨", level=40)
return
asyncio.run_coroutine_threadsafe(self._start_scraping(), self.loop)
async def _start_scraping(self):
"""
검색어를 기반으로 검색 페이지에 접속하여 데이터를 수집합니다.
실제 스크래핑 로직은 필요에 따라 구현하세요.
"""
import urllib.parse
encoded_query = urllib.parse.quote(self.search_query)
url = f"https://s.taobao.com/search?commend=all&ie=utf8&page=1&q={encoded_query}&search_type=item"
await self.page.goto(url)
self.logger.log("검색 페이지 접속 완료", level=20)
# 예시: 간단한 대기 후 데이터 수집 완료 신호 emit (실제 스크래핑 로직 추가 필요)
await asyncio.sleep(2)
self.scraping_complete.emit(True, "데이터 수집 완료")

View File

@ -2,7 +2,7 @@ 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 PySide6.QtCore import Qt, Slot, Signal, QThread, QTimer
from src.Scrapper1 import Scrapper1
from src.excel_export import ExcelExporter
from src.post_processor import PostProcessor
@ -117,7 +117,6 @@ class MainWindow(QWidget):
# 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()
@ -132,10 +131,13 @@ class MainWindow(QWidget):
# PlaywrightThread에 전달할 검색어를 초기화하기 위해 빈 값 설정
self.search_query = ""
# Scrapper1 QThread 인스턴스 생성 및 시작 (브라우저 초기화)
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_ready.connect(self.on_qr_ready)
self.scrapper1.login_complete.connect(self.on_login_complete)
self.scrapper1.scraping_complete.connect(self.on_scraping_complete)
self.scrapper1.progress_signal.connect(self.update_progress_bar)
self.scrapper1.start() # run()에서 start_browser() 실행
self.categoryManager = CategoryManager(self.logger, self.xls_file_path)
@ -145,52 +147,74 @@ class MainWindow(QWidget):
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())
self.logger.log("로그인 버튼 클릭 - QR 로그인 요청", level=logging.INFO)
# Scrapper1에 내장된 브라우저를 이용해 로그인 플로우 시작
self.scrapper1.start_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(bytes)
def on_qr_ready(self, qr_bytes):
# Scrapper1에서 QR 이미지(바이트)를 전달받으면 화면에 표시
pixmap = QPixmap()
pixmap.loadFromData(qr_bytes)
self.qr_label.setPixmap(pixmap)
self.qr_label.setVisible(True)
# (추가: 타이머를 이용해 90초 후 자동 숨김 처리 가능)
QTimer.singleShot(90000, self.qr_label.hide)
@Slot()
def on_login_complete(self):
# Scrapper1에서 로그인 완료 시 발생시키는 시그널 처리 (필요 시)
self.logger.log("MainWindow: 로그인 완료 알림 받음", level=20)
self.logger.log("로그인 완료 신호 수신", level=logging.INFO)
self.start_button.setEnabled(True)
self.qr_label.setVisible(False)
@Slot()
def on_start_button_clicked(self):
if not self.selected_search_query:
QMessageBox.warning(self, "오류", "상품분류를 올바르게 선택하세요.")
return
self.logger.log(f"시작 버튼 클릭 - 검색어 '{self.selected_search_query}'", level=logging.INFO)
# Scrapper1의 기존 브라우저(로그인 세션 유지)를 재활용하여 스크래핑 시작
self.scrapper1.start_scraping(self.selected_search_query)
@Slot(bool, str)
def on_scraping_complete(self, success, message):
if success:
QMessageBox.information(self, "수집 완료", message)
else:
QMessageBox.warning(self, "수집 실패", message)
# 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()
def load_config(self, file_path: str) -> configparser.ConfigParser:
"""