test_OK
This commit is contained in:
parent
623671f586
commit
017958d2a8
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
|
|
@ -35,14 +35,21 @@ class DatabaseManager():
|
|||
except sqlite3.Error as e:
|
||||
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:
|
||||
with sqlite3.connect(self.db_path, check_same_thread=False) as conn:
|
||||
conn.executemany('''INSERT OR REPLACE INTO items (id, pc_url, name, price, image_url, sales)
|
||||
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)
|
||||
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:
|
||||
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):
|
||||
# try:
|
||||
|
|
@ -96,6 +103,30 @@ class DatabaseManager():
|
|||
except sqlite3.Error as e:
|
||||
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):
|
||||
# 별도의 연결을 사용하는 구조에서는 close 메서드가 필요하지 않을 수 있습니다.
|
||||
self.logger.log(f"DBManager 종료", level=logging.DEBUG)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import xlwings as xw
|
|||
import logging
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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.base_excel_path = base_excel_path
|
||||
self.saved_files = []
|
||||
|
|
@ -17,44 +17,44 @@ class ExcelExporter:
|
|||
def fetch_data_from_db(self):
|
||||
try:
|
||||
df = self.db_manager.fetch_all() # DBManager 인스턴스를 사용하여 데이터 가져오기
|
||||
logger.debug("DB에서 데이터 로드 완료")
|
||||
self.logger.log(f"DB에서 데이터 로드 완료", level=logging.DEBUG)
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.error(f"DB에서 데이터 로드 중 오류 발생: {e}")
|
||||
self.logger.log(f"DB에서 데이터 로드 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
return pd.DataFrame()
|
||||
|
||||
def save_to_excel(self, output_path="output.xlsx"):
|
||||
df = self.fetch_data_from_db()
|
||||
|
||||
if df.empty:
|
||||
logger.warning("DB에서 불러온 데이터가 없습니다.")
|
||||
self.logger.log(f"DB에서 불러온 데이터가 없습니다.", level=logging.WARNING)
|
||||
return False # 성공 여부 반환
|
||||
|
||||
# 조건에 맞는 데이터 필터링
|
||||
filtered_df = df[(df['is_valid'] == 1) & (df['is_export'] == 0)]
|
||||
if filtered_df.empty:
|
||||
logger.warning("조건에 맞는 데이터가 없습니다.")
|
||||
self.logger.log(f"조건에 맞는 데이터가 없습니다.", level=logging.WARNING)
|
||||
return False # 성공 여부 반환
|
||||
|
||||
|
||||
app = xw.App(visible=False)
|
||||
logger.debug("xlwings 시작")
|
||||
self.logger.log(f"xlwings 시작", level=logging.DEBUG)
|
||||
|
||||
try:
|
||||
for i in range(0, len(df), 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')
|
||||
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)
|
||||
ws = wb.sheets['multi_ss']
|
||||
|
||||
for index, row in df_subset.iterrows():
|
||||
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'C{row_num}').value = row['name']
|
||||
ws.range(f'D{row_num}').value = row['price']
|
||||
|
|
@ -74,17 +74,17 @@ class ExcelExporter:
|
|||
'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.close()
|
||||
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 # 성공 여부 반환
|
||||
|
||||
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 반환
|
||||
finally:
|
||||
app.quit()
|
||||
logger.debug("xlwings 종료")
|
||||
self.logger.log(f"xlwings 종료", level=logging.DEBUG)
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ class TaobaoScraperApp(QWidget):
|
|||
|
||||
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.excel_exporter = ExcelExporter(self.db_manager)
|
||||
self.excel_exporter = ExcelExporter(self.logger, self.db_manager)
|
||||
self.postProcessor = PostProcessor(self.logger, self.db_manager)
|
||||
|
||||
@Slot()
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ import re
|
|||
from PySide6.QtCore import QThread, Signal
|
||||
from playwright.async_api import async_playwright, Page
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PlaywrightThread(QThread):
|
||||
data_collected = Signal(bool, str)
|
||||
|
||||
def __init__(self, db_manager):
|
||||
def __init__(self, logger, db_manager):
|
||||
super().__init__()
|
||||
self.logger = logger
|
||||
self.db_manager = db_manager
|
||||
|
||||
async def collect_data(self):
|
||||
|
|
@ -26,7 +25,7 @@ class PlaywrightThread(QThread):
|
|||
else:
|
||||
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([
|
||||
|
|
@ -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 (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 모드)
|
||||
browser = await p.chromium.launch(
|
||||
|
|
@ -60,11 +59,11 @@ class PlaywrightThread(QThread):
|
|||
# 페이지 열기
|
||||
page = await context.new_page()
|
||||
await page.goto("https://world.taobao.com")
|
||||
logger.info("타오바오 사이트에 접속했습니다.")
|
||||
self.logger.log(f"타오바오 사이트에 접속했습니다.", level=logging.INFO)
|
||||
|
||||
# 페이지 로딩 확인 및 pagedown
|
||||
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.wait_for_timeout(1000) # 1초 대기
|
||||
await page.keyboard.press("PageDown")
|
||||
|
|
@ -82,23 +81,23 @@ class PlaywrightThread(QThread):
|
|||
await browser.close()
|
||||
|
||||
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)
|
||||
|
||||
async def scrape_items(self, page: Page):
|
||||
try:
|
||||
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 = []
|
||||
for idx, item in enumerate(items):
|
||||
logger.debug(f"{idx + 1}번째 상품 카드 처리 중 - XPath 확인")
|
||||
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")
|
||||
logger.debug(f"{idx + 1}번째 상품 href: {item_href}")
|
||||
self.logger.log(f"{idx + 1}번째 상품 href: {item_href}", level=logging.DEBUG)
|
||||
|
||||
if item_href:
|
||||
# 숫자만 추출하고 9~12자리 필터링
|
||||
|
|
@ -106,42 +105,42 @@ class PlaywrightThread(QThread):
|
|||
if item_id_match:
|
||||
item_id = item_id_match.group(1)
|
||||
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:
|
||||
logger.warning(f"{idx + 1}번째 상품 ID를 올바르게 추출할 수 없습니다.")
|
||||
self.logger.log(f"{idx + 1}번째 상품 ID를 올바르게 추출할 수 없습니다.", level=logging.WARNING)
|
||||
continue
|
||||
else:
|
||||
logger.warning(f"{idx + 1}번째 상품 ID를 가져올 수 없습니다.")
|
||||
self.logger.log(f"{idx + 1}번째 상품 ID를 가져올 수 없습니다.", level=logging.WARNING)
|
||||
continue
|
||||
else:
|
||||
logger.warning(f"{idx + 1}번째 상품의 a 태그를 찾을 수 없습니다.")
|
||||
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"
|
||||
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 = 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_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")
|
||||
|
||||
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 = 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))
|
||||
|
||||
logger.debug(f"수집된 상품 수 : {len(items_data)}")
|
||||
self.logger.log(f"수집된 상품 수 : {len(items_data)}", level=logging.DEBUG)
|
||||
return items_data
|
||||
except Exception as e:
|
||||
logger.error(f"데이터 수집 중 오류 발생: {e}")
|
||||
self.logger.log(f"데이터 수집 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
return None
|
||||
|
||||
async def wait_for_user(self):
|
||||
|
|
|
|||
|
|
@ -12,9 +12,13 @@ from src.titleManager import TitleManager
|
|||
from src.categoryManager import CategoryManager
|
||||
from src.naver_parser import NaverParser
|
||||
from src.gpt_client import GPTClient
|
||||
from src.xlsSerachThread import XlsSerachThread
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import openpyxl
|
||||
|
||||
class PostProcessor:
|
||||
def __init__(self, logger, db_manager):
|
||||
self.logger = logger
|
||||
|
|
@ -28,6 +32,8 @@ class PostProcessor:
|
|||
self.title_manager = TitleManager(self.logger, self.gpt)
|
||||
self.categoryManager = CategoryManager(self.logger, base_xls_path)
|
||||
self.naver_parser = NaverParser(self.logger)
|
||||
|
||||
self.xlThread = XlsSerachThread(self.logger, self.db_manager)
|
||||
|
||||
# 설정 파일 로드
|
||||
self.config = configparser.ConfigParser()
|
||||
|
|
@ -128,19 +134,21 @@ class PostProcessor:
|
|||
# self.logger.info(f"선택된 폴더: {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에 저장.
|
||||
:param folder_path: 엑셀 파일이 위치한 폴더 경로
|
||||
"""
|
||||
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')]
|
||||
|
||||
if not excel_files:
|
||||
self.logger.log(f"엑셀 파일이 폴더 '{folder_path}'에 없습니다.", level=logging.WARNING)
|
||||
|
||||
return
|
||||
|
||||
self.logger.log(f"총 {len(excel_files)}개의 엑셀 파일을 발견했습니다.", level=logging.DEBUG)
|
||||
|
|
@ -160,6 +168,9 @@ class PostProcessor:
|
|||
|
||||
# 데이터 추출 (B4~B53, C4~C53)
|
||||
items = []
|
||||
url_list = []
|
||||
row_map = {}
|
||||
|
||||
for row in range(4, 54): # 4번 행부터 53번 행까지
|
||||
pc_url = sheet[f"B{row}"].value # PC_URL
|
||||
name = sheet[f"C{row}"].value # 상품명
|
||||
|
|
@ -169,20 +180,42 @@ class PostProcessor:
|
|||
continue
|
||||
|
||||
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:
|
||||
items.append((id_value, pc_url, name, price, image_url, 0)) # sales는 0으로 설정
|
||||
# URL 목록 처리하여 가격 및 이미지 URL 수집
|
||||
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:
|
||||
self.db_manager.insert_items(items)
|
||||
self.db_manager.insert_items(items, db_name)
|
||||
self.logger.log(f"{file_path}의 데이터를 DB에 저장했습니다.", level=logging.DEBUG)
|
||||
|
||||
# 처리 완료 후 브라우저 종료
|
||||
await self.xlThread.close_br()
|
||||
|
||||
except Exception as e:
|
||||
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')
|
||||
self.logger.log(f"총 {len(products)}개의 처리되지 않은 상품 로드 완료.", level=logging.DEBUG)
|
||||
self.process_products(products)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
import asyncio
|
||||
class XLSProcessingThread(QThread):
|
||||
progress = Signal(str) # 진행 상태를 GUI에 전달할 시그널
|
||||
|
||||
|
|
@ -14,7 +14,10 @@ class XLSProcessingThread(QThread):
|
|||
"""
|
||||
try:
|
||||
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 파일 처리 완료")
|
||||
except Exception as e:
|
||||
self.progress.emit(f"XLS 파일 처리 중 오류 발생: {e}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue