This commit is contained in:
Envy_PC 2024-12-09 16:42:37 +09:00
parent 623671f586
commit 017958d2a8
9 changed files with 286 additions and 52 deletions

Binary file not shown.

View File

@ -35,14 +35,21 @@ class DatabaseManager():
except sqlite3.Error as e: except sqlite3.Error as e:
self.logger.log(f"테이블 생성 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) self.logger.log(f"테이블 생성 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
def insert_items(self, items): def insert_items(self, items, db_name=None):
"""
데이터를 데이터베이스에 삽입하는 메서드.
:param items: 삽입할 데이터 리스트 [(id, pc_url, name, price, image_url, sales), ...]
:param db_name: 사용할 데이터베이스 경로 (기본값은 self.db_path)
"""
db_path = db_name or self.db_path # db_name이 전달되면 해당 값을 사용, 없으면 self.db_path 사용
try: try:
with sqlite3.connect(self.db_path, check_same_thread=False) as conn: with sqlite3.connect(db_path, check_same_thread=False) as conn:
conn.executemany('''INSERT OR REPLACE INTO items (id, pc_url, name, price, image_url, sales) conn.executemany('''INSERT OR REPLACE INTO items
(id, pc_url, name, price, image_url, sales)
VALUES (?, ?, ?, ?, ?, ?)''', items) VALUES (?, ?, ?, ?, ?, ?)''', items)
self.logger.log(f"{len(items)}개의 항목이 데이터베이스에 저장되었습니다.", level=logging.DEBUG) self.logger.log(f"{len(items)}개의 항목이 데이터베이스({db_path})에 저장되었습니다.", level=logging.DEBUG)
except sqlite3.Error as e: except sqlite3.Error as e:
self.logger.log(f"데이터베이스 저장 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) self.logger.log(f"데이터베이스({db_path}) 저장 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
# def fetch_all(self): # def fetch_all(self):
# try: # try:
@ -96,6 +103,30 @@ class DatabaseManager():
except sqlite3.Error as e: except sqlite3.Error as e:
self.logger.log(f"상품 {product['id']} 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) self.logger.log(f"상품 {product['id']} 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
def update_item_by_field(self, product: Dict, db_name: str = None):
"""
처리된 상품 정보를 데이터베이스에 업데이트.
:param product: 업데이트할 상품 정보 딕셔너리 (필드: )
"""
try:
# 데이터베이스 경로 설정
db_path = db_name or self.db_path
self.logger.log(f"update_item_by_field - db_path : {db_path}", level=logging.DEBUG)
# 업데이트할 필드와 값 생성
update_fields = ", ".join([f"{key} = ?" for key in product.keys() if key != "id"])
update_values = tuple(product[key] for key in product.keys() if key != "id")
update_query = f"UPDATE items SET {update_fields} WHERE id = ?"
with sqlite3.connect(db_path, check_same_thread=False) as conn:
conn.execute(update_query, update_values + (product["id"],))
conn.commit()
self.logger.log(f"상품 {product['id']}이(가) 성공적으로 업데이트되었습니다.", level=logging.DEBUG)
except sqlite3.Error as e:
self.logger.log(f"상품 {product.get('id', 'Unknown')} 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
def close(self): def close(self):
# 별도의 연결을 사용하는 구조에서는 close 메서드가 필요하지 않을 수 있습니다. # 별도의 연결을 사용하는 구조에서는 close 메서드가 필요하지 않을 수 있습니다.
self.logger.log(f"DBManager 종료", level=logging.DEBUG) self.logger.log(f"DBManager 종료", level=logging.DEBUG)

View File

@ -6,10 +6,10 @@ import xlwings as xw
import logging import logging
from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QMessageBox
logger = logging.getLogger(__name__)
class ExcelExporter: class ExcelExporter:
def __init__(self, db_manager, base_excel_path="src\\baseXLS_Percenty.xlsx"): def __init__(self, logger, db_manager, base_excel_path="src\\baseXLS_Percenty.xlsx"):
self.logger =logger
self.db_manager = db_manager # DBManager 인스턴스 self.db_manager = db_manager # DBManager 인스턴스
self.base_excel_path = base_excel_path self.base_excel_path = base_excel_path
self.saved_files = [] self.saved_files = []
@ -17,44 +17,44 @@ class ExcelExporter:
def fetch_data_from_db(self): def fetch_data_from_db(self):
try: try:
df = self.db_manager.fetch_all() # DBManager 인스턴스를 사용하여 데이터 가져오기 df = self.db_manager.fetch_all() # DBManager 인스턴스를 사용하여 데이터 가져오기
logger.debug("DB에서 데이터 로드 완료") self.logger.log(f"DB에서 데이터 로드 완료", level=logging.DEBUG)
return df return df
except Exception as e: except Exception as e:
logger.error(f"DB에서 데이터 로드 중 오류 발생: {e}") self.logger.log(f"DB에서 데이터 로드 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return pd.DataFrame() return pd.DataFrame()
def save_to_excel(self, output_path="output.xlsx"): def save_to_excel(self, output_path="output.xlsx"):
df = self.fetch_data_from_db() df = self.fetch_data_from_db()
if df.empty: if df.empty:
logger.warning("DB에서 불러온 데이터가 없습니다.") self.logger.log(f"DB에서 불러온 데이터가 없습니다.", level=logging.WARNING)
return False # 성공 여부 반환 return False # 성공 여부 반환
# 조건에 맞는 데이터 필터링 # 조건에 맞는 데이터 필터링
filtered_df = df[(df['is_valid'] == 1) & (df['is_export'] == 0)] filtered_df = df[(df['is_valid'] == 1) & (df['is_export'] == 0)]
if filtered_df.empty: if filtered_df.empty:
logger.warning("조건에 맞는 데이터가 없습니다.") self.logger.log(f"조건에 맞는 데이터가 없습니다.", level=logging.WARNING)
return False # 성공 여부 반환 return False # 성공 여부 반환
app = xw.App(visible=False) app = xw.App(visible=False)
logger.debug("xlwings 시작") self.logger.log(f"xlwings 시작", level=logging.DEBUG)
try: try:
for i in range(0, len(df), 50): for i in range(0, len(df), 50):
df_subset = df.iloc[i:i+50] df_subset = df.iloc[i:i+50]
logger.debug(f"{i}번째 출력할 데이터:\n{df_subset}") # 데이터 검증 로그 추가 self.logger.log(f"{i}번째 출력할 데이터:\n{df_subset}", level=logging.DEBUG) # 데이터 검증 로그 추가
part_file_name = output_path.replace('.xlsx', f'_part{i//50 + 1}.xlsx') part_file_name = output_path.replace('.xlsx', f'_part{i//50 + 1}.xlsx')
shutil.copy(self.base_excel_path, part_file_name) shutil.copy(self.base_excel_path, part_file_name)
logger.debug(f"기본 엑셀 파일 '{self.base_excel_path}' 복사 완료") self.logger.log(f"기본 엑셀 파일 '{self.base_excel_path}' 복사 완료", level=logging.DEBUG)
wb = xw.Book(part_file_name) wb = xw.Book(part_file_name)
ws = wb.sheets['multi_ss'] ws = wb.sheets['multi_ss']
for index, row in df_subset.iterrows(): for index, row in df_subset.iterrows():
row_num = 4 + (index % 50) row_num = 4 + (index % 50)
logger.debug(f"{index + 1}번째 행 기록 시작: B{row_num}, C{row_num}, D{row_num}, F{row_num}, G{row_num}, H{row_num}") # 셀 위치 로그 추가 self.logger.log(f"{index + 1}번째 행 기록 시작: B{row_num}, C{row_num}, D{row_num}, F{row_num}, G{row_num}, H{row_num}", level=logging.DEBUG) # 셀 위치 로그 추가
ws.range(f'B{row_num}').value = row['pc_url'] ws.range(f'B{row_num}').value = row['pc_url']
ws.range(f'C{row_num}').value = row['name'] ws.range(f'C{row_num}').value = row['name']
ws.range(f'D{row_num}').value = row['price'] ws.range(f'D{row_num}').value = row['price']
@ -74,17 +74,17 @@ class ExcelExporter:
'is_export': 1 # is_export를 1로 설정 'is_export': 1 # is_export를 1로 설정
}) })
logger.debug(f"{index + 1}번째 행 기록 완료") self.logger.log(f"{index + 1}번째 행 기록 완료", level=logging.DEBUG)
wb.save(part_file_name) # SaveCopyAs 대신 save 사용 wb.save(part_file_name) # SaveCopyAs 대신 save 사용
wb.close() wb.close()
self.saved_files.append(part_file_name) self.saved_files.append(part_file_name)
logger.info(f"파일 '{part_file_name}'에 데이터가 저장되었습니다.") self.logger.log(f"파일 '{part_file_name}'에 데이터가 저장되었습니다.", level=logging.INFO)
return True # 성공 여부 반환 return True # 성공 여부 반환
except Exception as e: except Exception as e:
logger.error(f"엑셀 저장 중 예외 발생: {e}", exc_info=True) self.logger.log(f"엑셀 저장 중 예외 발생: {e}", level=logging.ERROR, exc_info=True)
return False # 실패 시 False 반환 return False # 실패 시 False 반환
finally: finally:
app.quit() app.quit()
logger.debug("xlwings 종료") self.logger.log(f"xlwings 종료", level=logging.DEBUG)

View File

@ -38,10 +38,10 @@ class TaobaoScraperApp(QWidget):
self.setLayout(self.layout) self.setLayout(self.layout)
self.playwright_thread = PlaywrightThread(self.db_manager) self.playwright_thread = PlaywrightThread(self.logger, self.db_manager)
self.playwright_thread.data_collected.connect(self.on_data_collected) self.playwright_thread.data_collected.connect(self.on_data_collected)
self.excel_exporter = ExcelExporter(self.db_manager) self.excel_exporter = ExcelExporter(self.logger, self.db_manager)
self.postProcessor = PostProcessor(self.logger, self.db_manager) self.postProcessor = PostProcessor(self.logger, self.db_manager)
@Slot() @Slot()

View File

@ -7,13 +7,12 @@ import re
from PySide6.QtCore import QThread, Signal from PySide6.QtCore import QThread, Signal
from playwright.async_api import async_playwright, Page from playwright.async_api import async_playwright, Page
logger = logging.getLogger(__name__)
class PlaywrightThread(QThread): class PlaywrightThread(QThread):
data_collected = Signal(bool, str) data_collected = Signal(bool, str)
def __init__(self, db_manager): def __init__(self, logger, db_manager):
super().__init__() super().__init__()
self.logger = logger
self.db_manager = db_manager self.db_manager = db_manager
async def collect_data(self): async def collect_data(self):
@ -26,7 +25,7 @@ class PlaywrightThread(QThread):
else: else:
browser_path = os.path.join(os.path.dirname(__file__), 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe') browser_path = os.path.join(os.path.dirname(__file__), 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
logger.debug(f"브라우저 경로: {browser_path}") self.logger.log(f"브라우저 경로: {browser_path}", level=logging.DEBUG)
# 사용자 에이전트 설정 # 사용자 에이전트 설정
user_agent = random.choice([ user_agent = random.choice([
@ -36,7 +35,7 @@ class PlaywrightThread(QThread):
"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 (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", "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",
]) ])
logger.debug(f"user_agent: {user_agent}") self.logger.log(f"user_agent: {user_agent}", level=logging.DEBUG)
# 브라우저 시작 (headless 모드) # 브라우저 시작 (headless 모드)
browser = await p.chromium.launch( browser = await p.chromium.launch(
@ -60,11 +59,11 @@ class PlaywrightThread(QThread):
# 페이지 열기 # 페이지 열기
page = await context.new_page() page = await context.new_page()
await page.goto("https://world.taobao.com") await page.goto("https://world.taobao.com")
logger.info("타오바오 사이트에 접속했습니다.") self.logger.log(f"타오바오 사이트에 접속했습니다.", level=logging.INFO)
# 페이지 로딩 확인 및 pagedown # 페이지 로딩 확인 및 pagedown
await page.wait_for_selector(".tb-pick-content-item") # 상품 카드 로딩 대기 await page.wait_for_selector(".tb-pick-content-item") # 상품 카드 로딩 대기
logger.info("페이지 로딩 완료 - Pagedown을 두 번 누릅니다.") self.logger.log(f"페이지 로딩 완료 - Pagedown을 두 번 누릅니다.", level=logging.INFO)
await page.keyboard.press("PageDown") await page.keyboard.press("PageDown")
await page.wait_for_timeout(1000) # 1초 대기 await page.wait_for_timeout(1000) # 1초 대기
await page.keyboard.press("PageDown") await page.keyboard.press("PageDown")
@ -82,23 +81,23 @@ class PlaywrightThread(QThread):
await browser.close() await browser.close()
except Exception as e: except Exception as e:
logger.error(f"브라우저 작업 중 오류 발생: {e}", exec=True) self.logger.log(f"브라우저 작업 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
self.data_collected.emit(False, f"오류 발생: {e}", exec=True) self.data_collected.emit(False, f"오류 발생: {e}", exec=True)
async def scrape_items(self, page: Page): async def scrape_items(self, page: Page):
try: try:
items = await page.query_selector_all("div#ice-container div.tb-pick-feeds-container > div") items = await page.query_selector_all("div#ice-container div.tb-pick-feeds-container > div")
logger.debug(f"{len(items)}개의 상품 카드가 발견되었습니다.") self.logger.log(f"{len(items)}개의 상품 카드가 발견되었습니다.", level=logging.DEBUG)
items_data = [] items_data = []
for idx, item in enumerate(items): for idx, item in enumerate(items):
logger.debug(f"{idx + 1}번째 상품 카드 처리 중 - XPath 확인") self.logger.log(f"{idx + 1}번째 상품 카드 처리 중 - XPath 확인", level=logging.DEBUG)
# a 태그를 명시적으로 선택하여 href 가져오기 # a 태그를 명시적으로 선택하여 href 가져오기
link_element = await item.query_selector("a.item-link") link_element = await item.query_selector("a.item-link")
if link_element: if link_element:
item_href = await link_element.get_attribute("href") item_href = await link_element.get_attribute("href")
logger.debug(f"{idx + 1}번째 상품 href: {item_href}") self.logger.log(f"{idx + 1}번째 상품 href: {item_href}", level=logging.DEBUG)
if item_href: if item_href:
# 숫자만 추출하고 9~12자리 필터링 # 숫자만 추출하고 9~12자리 필터링
@ -106,42 +105,42 @@ class PlaywrightThread(QThread):
if item_id_match: if item_id_match:
item_id = item_id_match.group(1) item_id = item_id_match.group(1)
pc_url = f"https://item.taobao.com/item.htm?id={item_id}" pc_url = f"https://item.taobao.com/item.htm?id={item_id}"
logger.debug(f"{idx + 1}번째 상품 ID: {item_id}, 상품 URL: {pc_url}") self.logger.log(f"{idx + 1}번째 상품 ID: {item_id}, 상품 URL: {pc_url}", level=logging.DEBUG)
else: else:
logger.warning(f"{idx + 1}번째 상품 ID를 올바르게 추출할 수 없습니다.") self.logger.log(f"{idx + 1}번째 상품 ID를 올바르게 추출할 수 없습니다.", level=logging.WARNING)
continue continue
else: else:
logger.warning(f"{idx + 1}번째 상품 ID를 가져올 수 없습니다.") self.logger.log(f"{idx + 1}번째 상품 ID를 가져올 수 없습니다.", level=logging.WARNING)
continue continue
else: else:
logger.warning(f"{idx + 1}번째 상품의 a 태그를 찾을 수 없습니다.") self.logger.log(f"{idx + 1}번째 상품의 a 태그를 찾을 수 없습니다.", level=logging.WARNING)
continue continue
name_element = await item.query_selector(".info-wrapper-title-text") name_element = await item.query_selector(".info-wrapper-title-text")
name = await name_element.inner_text() if name_element else "N/A" name = await name_element.inner_text() if name_element else "N/A"
logger.debug(f"{idx + 1}번째 상품명: {name}") self.logger.log(f"{idx + 1}번째 상품명: {name}", level=logging.DEBUG)
price_element = await item.query_selector(".price-value") price_element = await item.query_selector(".price-value")
price = await price_element.inner_text() if price_element else "0" price = await price_element.inner_text() if price_element else "0"
logger.debug(f"{idx + 1}번째 상품 가격: {price}") self.logger.log(f"{idx + 1}번째 상품 가격: {price}", level=logging.DEBUG)
image_element = await item.query_selector(".img-wrapper") image_element = await item.query_selector(".img-wrapper")
image_style = await image_element.get_attribute("style") if image_element else "" 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('")') if image_style else "N/A"
image_url = (image_style.split("url(")[-1].strip('");').strip("'").replace("//", "https://", 1) if image_style else "N/A") image_url = (image_style.split("url(")[-1].strip('");').strip("'").replace("//", "https://", 1) if image_style else "N/A")
logger.debug(f"{idx + 1}번째 상품 이미지 URL: {image_url}") self.logger.log(f"{idx + 1}번째 상품 이미지 URL: {image_url}", level=logging.DEBUG)
sales_element = await item.query_selector(".month-sale") sales_element = await item.query_selector(".month-sale")
sales = await sales_element.inner_text() if sales_element else "0" sales = await sales_element.inner_text() if sales_element else "0"
logger.debug(f"{idx + 1}번째 상품 판매량: {sales}") self.logger.log(f"{idx + 1}번째 상품 판매량: {sales}", level=logging.DEBUG)
items_data.append((item_id, pc_url, name, float(price), image_url, sales)) items_data.append((item_id, pc_url, name, float(price), image_url, sales))
logger.debug(f"수집된 상품 수 : {len(items_data)}") self.logger.log(f"수집된 상품 수 : {len(items_data)}", level=logging.DEBUG)
return items_data return items_data
except Exception as e: except Exception as e:
logger.error(f"데이터 수집 중 오류 발생: {e}") self.logger.log(f"데이터 수집 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return None return None
async def wait_for_user(self): async def wait_for_user(self):

View File

@ -12,9 +12,13 @@ from src.titleManager import TitleManager
from src.categoryManager import CategoryManager from src.categoryManager import CategoryManager
from src.naver_parser import NaverParser from src.naver_parser import NaverParser
from src.gpt_client import GPTClient from src.gpt_client import GPTClient
from src.xlsSerachThread import XlsSerachThread
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import openpyxl
class PostProcessor: class PostProcessor:
def __init__(self, logger, db_manager): def __init__(self, logger, db_manager):
self.logger = logger self.logger = logger
@ -28,6 +32,8 @@ class PostProcessor:
self.title_manager = TitleManager(self.logger, self.gpt) self.title_manager = TitleManager(self.logger, self.gpt)
self.categoryManager = CategoryManager(self.logger, base_xls_path) self.categoryManager = CategoryManager(self.logger, base_xls_path)
self.naver_parser = NaverParser(self.logger) self.naver_parser = NaverParser(self.logger)
self.xlThread = XlsSerachThread(self.logger, self.db_manager)
# 설정 파일 로드 # 설정 파일 로드
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
@ -128,19 +134,21 @@ class PostProcessor:
# self.logger.info(f"선택된 폴더: {selected_folder}") # self.logger.info(f"선택된 폴더: {selected_folder}")
# self._post_by_XLS(selected_folder) # self._post_by_XLS(selected_folder)
def post_by_XLS(self, folder_path):
import openpyxl async def post_by_XLS(self, folder_path):
""" """
주어진 폴더 경로에서 모든 엑셀 파일을 순회하며 데이터를 수집 DB에 저장. 주어진 폴더 경로에서 모든 엑셀 파일을 순회하며 데이터를 수집 DB에 저장.
:param folder_path: 엑셀 파일이 위치한 폴더 경로 :param folder_path: 엑셀 파일이 위치한 폴더 경로
""" """
try: try:
# PlaywrightThread 초기화 및 브라우저 시작
page = await self.xlThread.start_br()
# 폴더 내 모든 엑셀 파일 가져오기 # 폴더 내 모든 엑셀 파일 가져오기
excel_files = [f for f in os.listdir(folder_path) if f.endswith('.xls') or f.endswith('.xlsx')] excel_files = [f for f in os.listdir(folder_path) if f.endswith('.xls') or f.endswith('.xlsx')]
if not excel_files: if not excel_files:
self.logger.log(f"엑셀 파일이 폴더 '{folder_path}'에 없습니다.", level=logging.WARNING) self.logger.log(f"엑셀 파일이 폴더 '{folder_path}'에 없습니다.", level=logging.WARNING)
return return
self.logger.log(f"{len(excel_files)}개의 엑셀 파일을 발견했습니다.", level=logging.DEBUG) self.logger.log(f"{len(excel_files)}개의 엑셀 파일을 발견했습니다.", level=logging.DEBUG)
@ -160,6 +168,9 @@ class PostProcessor:
# 데이터 추출 (B4~B53, C4~C53) # 데이터 추출 (B4~B53, C4~C53)
items = [] items = []
url_list = []
row_map = {}
for row in range(4, 54): # 4번 행부터 53번 행까지 for row in range(4, 54): # 4번 행부터 53번 행까지
pc_url = sheet[f"B{row}"].value # PC_URL pc_url = sheet[f"B{row}"].value # PC_URL
name = sheet[f"C{row}"].value # 상품명 name = sheet[f"C{row}"].value # 상품명
@ -169,20 +180,42 @@ class PostProcessor:
continue continue
id_value = self.parse_id_from_url(pc_url) # URL에서 ID 추출 id_value = self.parse_id_from_url(pc_url) # URL에서 ID 추출
price, image_url = self.fetch_price_and_image(pc_url) # 가격 및 이미지 URL 수집 if id_value:
url_list.append(pc_url)
row_map[pc_url] = (id_value, name, row)
if id_value and price and image_url: # URL 목록 처리하여 가격 및 이미지 URL 수집
items.append((id_value, pc_url, name, price, image_url, 0)) # sales는 0으로 설정 for url in url_list:
result = await self.xlThread.goto_url_and_parsing(db_name, page, id_value, url)
price = result.get("price") if result else None
image_url = result.get("image_url") if result else None
if not price or not image_url:
self.logger.log(f"데이터 수집 실패: URL {url}", level=logging.WARNING)
continue
self.logger.log(f"url: {url}", level=logging.DEBUG)
self.logger.log(f"price: {price}", level=logging.DEBUG)
self.logger.log(f"image_url: {image_url}", level=logging.DEBUG)
id_value, name, row = row_map[url]
items.append((id_value, url, name, price, image_url, 0)) # sales는 0으로 설정
# 수집된 데이터 DB 저장
self.logger.log(f"수집된 items: {items}", level=logging.DEBUG)
if items: if items:
self.db_manager.insert_items(items) self.db_manager.insert_items(items, db_name)
self.logger.log(f"{file_path}의 데이터를 DB에 저장했습니다.", level=logging.DEBUG) self.logger.log(f"{file_path}의 데이터를 DB에 저장했습니다.", level=logging.DEBUG)
# 처리 완료 후 브라우저 종료
await self.xlThread.close_br()
except Exception as e: except Exception as e:
self.logger.log(f"엑셀 파일 처리 중 오류 발생: {file_path}, 오류: {e}", level=logging.ERROR, exc_info=True) self.logger.log(f"엑셀 파일 처리 중 오류 발생: {file_path}, 오류: {e}", level=logging.ERROR, exc_info=True)
# 처리되지 않은 상품 로드 및 후처리 # 처리되지 않은 상품 로드 및 후처리
# products = self.db_manager.fetch_all().query("is_export == 0").to_dict('records')
products = self.db_manager.fetch_all(db_path=db_name).query("is_export == 0").to_dict('records') products = self.db_manager.fetch_all(db_path=db_name).query("is_export == 0").to_dict('records')
self.logger.log(f"{len(products)}개의 처리되지 않은 상품 로드 완료.", level=logging.DEBUG) self.logger.log(f"{len(products)}개의 처리되지 않은 상품 로드 완료.", level=logging.DEBUG)
self.process_products(products) self.process_products(products)

View File

@ -1,5 +1,5 @@
from PySide6.QtCore import QThread, Signal from PySide6.QtCore import QThread, Signal
import asyncio
class XLSProcessingThread(QThread): class XLSProcessingThread(QThread):
progress = Signal(str) # 진행 상태를 GUI에 전달할 시그널 progress = Signal(str) # 진행 상태를 GUI에 전달할 시그널
@ -14,7 +14,10 @@ class XLSProcessingThread(QThread):
""" """
try: try:
self.progress.emit(f"'{self.folder_path}'에서 XLS 파일 처리 시작") self.progress.emit(f"'{self.folder_path}'에서 XLS 파일 처리 시작")
self.post_processor.post_by_XLS(self.folder_path)
asyncio.run(self.post_processor.post_by_XLS(self.folder_path))
# self.post_processor.post_by_XLS(self.folder_path)
self.progress.emit("XLS 파일 처리 완료") self.progress.emit("XLS 파일 처리 완료")
except Exception as e: except Exception as e:
self.progress.emit(f"XLS 파일 처리 중 오류 발생: {e}") self.progress.emit(f"XLS 파일 처리 중 오류 발생: {e}")

168
src/xlsSerachThread.py Normal file
View File

@ -0,0 +1,168 @@
import os
import sys
import random
import logging
import re
from bs4 import BeautifulSoup
from playwright.async_api import async_playwright
class XlsSerachThread():
def __init__(self, logger, db_manager):
super().__init__()
self.logger = logger
self.db_manager = db_manager
self.page = None
self.browser = None # 브라우저 상태를 유지하기 위한 속성
async def start_br(self):
try:
# Playwright 객체 초기화
if not hasattr(self, "playwright") or not self.playwright:
self.playwright = await async_playwright().start()
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 모드)
self.browser = await self.playwright.chromium.launch(
headless=True, # headless 모드로 설정
executable_path=browser_path,
args=[
'--disable-popup-blocking',
'--start-maximized',
'--window-size=1920,1080'
]
)
# 시크릿 브라우저 컨텍스트 생성
self.context = await self.browser.new_context(
user_agent=user_agent,
geolocation={"latitude": 37.5665, "longitude": 126.9780},
locale="ko-KR",
permissions=["geolocation", "notifications"]
)
# 페이지 열기
self.page = await self.context.new_page()
return self.page
except Exception as e:
self.logger.log(f"브라우저 작업 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
async def goto_url_and_parsing(self, db_name, page, id_value, url):
"""
URL로 이동 HTML을 파싱하여 데이터를 추출.
"""
try:
# URL로 이동
await page.goto(url)
self.logger.log(f"{url} 이동 완료", level=logging.INFO)
# 페이지 로드 완료 대기
await page.wait_for_load_state("domcontentloaded")
# # 페이지 HTML 가져오기
# html = await page.content()
# soup = BeautifulSoup(html, "html.parser")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await page.wait_for_timeout(1000) # 스크롤 후 로드 대기
await page.wait_for_selector("div.summaryInfoWrap--Ndc7k4Hv", timeout=7000)
html = await page.content()
soup = BeautifulSoup(html, "html.parser")
# self.logger.log(f"soup: {soup}", level=logging.DEBUG)
# summaryInfoWrap--Ndc7k4Hv 요소만 가져오기
summary_info = soup.select_one("div.summaryInfoWrap--Ndc7k4Hv")
if not summary_info:
self.logger.log(f"요소 누락: summaryInfoWrap--Ndc7k4Hv, URL: {url}", level=logging.WARNING)
return None
# self.logger.log(f"summary_info: {summary_info}", level=logging.DEBUG)
# 이미지 URL 추출
image_tag = summary_info.select_one("div.mainPicWrap--Ns5WQiHr img")
image_url = image_tag["src"] if image_tag else None
# 가격 추출
price_tag = summary_info.select_one("span.text--Mdqy24Ex")
fallback_price_tag = summary_info.select_one("span.unit--i1DKXW20 + span")
price = price_tag.text.strip() if price_tag else (fallback_price_tag.text.strip() if fallback_price_tag else None)
# 상품명(title) 추출
title_tag = summary_info.select_one("div.ItemTitle--UReZzEW5 h1.mainTitle--O1XCl8e2")
product_name = title_tag["title"].strip() if title_tag and "title" in title_tag.attrs else None
# 데이터 검증
if not image_url or not price or not product_name:
self.logger.log(
f"데이터 누락 발생: URL {url}, 이미지 URL: {image_url}, 가격: {price}, 상품명: {product_name}",
level=logging.WARNING,
)
return None
self.logger.log(
f"데이터 추출 완료: {url}, 이미지 URL: {image_url}, 가격: {price}, 상품명: {product_name}",
level=logging.INFO,
)
# # 필요한 정보만 데이터베이스 업데이트
# self.db_manager.update_item_by_field({
# "id": id_value,
# "image_url": image_url,
# "price": float(price.replace(",", "").strip("₩")) if price else None,
# "name": product_name,
# }, db_name)
# self.logger.log(f"데이터베이스 업데이트 완료: {url}", level=logging.INFO)
# 필요한 정보 반환
# return {"image_url": image_url, "price": price, "product_name": product_name}
return {"price": price, "image_url": image_url}
except Exception as e:
self.logger.log(f"URL 처리 중 오류 발생: {url}, {e}", level=logging.ERROR, exc_info=True)
return None
async def close_br(self):
"""
브라우저 종료 메서드.
"""
try:
if self.page and not self.page.is_closed():
await self.page.close()
if self.context:
await self.context.close()
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
self.logger.log("브라우저와 Playwright 리소스가 성공적으로 종료되었습니다.", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"브라우저 종료 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)

BIN
xls_db.db

Binary file not shown.