엑셀출력조정
This commit is contained in:
parent
e9cf688f78
commit
6709877677
|
|
@ -5,4 +5,6 @@ pyvenv.cfg
|
|||
*.log
|
||||
__pycache__/
|
||||
src/__pycache__/
|
||||
src/browsers/user_data/
|
||||
src/browsers/user_data/
|
||||
*.xlsx
|
||||
!baseXLS_Percenty.xlsx
|
||||
Binary file not shown.
|
|
@ -1,50 +1,54 @@
|
|||
import sqlite3
|
||||
import logging
|
||||
import pandas as pd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# SQLite DB 초기화 및 데이터 관리
|
||||
class DatabaseManager:
|
||||
def __init__(self, db_name="taobao_items.db"):
|
||||
self.conn = sqlite3.connect(db_name)
|
||||
self.cur = self.conn.cursor()
|
||||
self.db_path = db_name
|
||||
logger.info("DBManager 초기화 완료")
|
||||
self.create_table()
|
||||
|
||||
def create_table(self):
|
||||
try:
|
||||
self.cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS items (
|
||||
item_id TEXT PRIMARY KEY,
|
||||
pc_url TEXT,
|
||||
name TEXT,
|
||||
price REAL,
|
||||
image_url TEXT,
|
||||
sales TEXT
|
||||
)
|
||||
""")
|
||||
self.conn.commit()
|
||||
logger.info("데이터베이스 테이블 초기화 완료")
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS items (
|
||||
id TEXT PRIMARY KEY,
|
||||
pc_url TEXT,
|
||||
name TEXT,
|
||||
price REAL,
|
||||
image_url TEXT,
|
||||
sales TEXT,
|
||||
translated_name TEXT, -- 번역된 상품명
|
||||
margin_price REAL, -- 계산된 더하기 마진
|
||||
international_shipping_fee REAL, -- 해외 배송비
|
||||
category_code TEXT -- 카테고리 코드
|
||||
)''')
|
||||
logger.info("데이터베이스 테이블 생성 완료")
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"DB 테이블 생성 오류: {e}")
|
||||
logger.error(f"테이블 생성 중 오류 발생: {e}")
|
||||
|
||||
def insert_items(self, items_data):
|
||||
def insert_items(self, items):
|
||||
try:
|
||||
self.cur.executemany("""
|
||||
INSERT OR REPLACE INTO items (item_id, pc_url, name, price, image_url, sales)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", items_data)
|
||||
self.conn.commit()
|
||||
logger.info("데이터 삽입 완료")
|
||||
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)
|
||||
VALUES (?, ?, ?, ?, ?, ?)''', items)
|
||||
logger.info(f"{len(items)}개의 항목이 데이터베이스에 저장되었습니다.")
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"DB 삽입 오류: {e}")
|
||||
logger.error(f"데이터베이스 저장 중 오류 발생: {e}")
|
||||
|
||||
def fetch_all_items(self):
|
||||
def fetch_all(self):
|
||||
try:
|
||||
self.cur.execute("SELECT * FROM items")
|
||||
return self.cur.fetchall()
|
||||
with sqlite3.connect(self.db_path, check_same_thread=False) as conn:
|
||||
df = pd.read_sql_query("SELECT * FROM items", conn)
|
||||
logger.info("데이터베이스에서 데이터 로드 완료")
|
||||
return df
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"DB 조회 오류: {e}")
|
||||
return []
|
||||
logger.error(f"데이터베이스 읽기 중 오류 발생: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
# 별도의 연결을 사용하는 구조에서는 close 메서드가 필요하지 않을 수 있습니다.
|
||||
logger.info("DBManager 종료")
|
||||
|
|
|
|||
|
|
@ -1,19 +1,66 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import pandas as pd
|
||||
import xlwings as xw
|
||||
import logging
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ExcelExporter:
|
||||
def __init__(self, db_manager):
|
||||
self.db_manager = db_manager
|
||||
def __init__(self, db_manager, base_excel_path="src\\baseXLS_Percenty.xlsx"):
|
||||
self.db_manager = db_manager # DBManager 인스턴스
|
||||
self.base_excel_path = base_excel_path
|
||||
self.saved_files = []
|
||||
|
||||
def export_to_excel(self, file_name="taobao_items.xlsx"):
|
||||
def fetch_data_from_db(self):
|
||||
try:
|
||||
data = self.db_manager.fetch_all_items()
|
||||
df = pd.DataFrame(data, columns=["item_id", "pc_url", "name", "price", "image_url", "sales"])
|
||||
df.to_excel(file_name, index=False)
|
||||
logger.info("엑셀 파일로 저장 완료")
|
||||
return True
|
||||
df = self.db_manager.fetch_all() # DBManager 인스턴스를 사용하여 데이터 가져오기
|
||||
logger.debug("DB에서 데이터 로드 완료")
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.error(f"엑셀 저장 오류: {e}")
|
||||
return False
|
||||
logger.error(f"DB에서 데이터 로드 중 오류 발생: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def save_to_excel(self, output_path="output.xlsx"):
|
||||
df = self.fetch_data_from_db()
|
||||
if df.empty:
|
||||
logger.warning("DB에서 불러온 데이터가 없습니다.")
|
||||
return False # 성공 여부 반환
|
||||
|
||||
app = xw.App(visible=False)
|
||||
logger.debug("xlwings 시작")
|
||||
|
||||
try:
|
||||
for i in range(0, len(df), 50):
|
||||
df_subset = df.iloc[i:i+50]
|
||||
logger.debug(f"{i}번째 출력할 데이터:\n{df_subset}") # 데이터 검증 로그 추가
|
||||
|
||||
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}' 복사 완료")
|
||||
|
||||
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}") # 셀 위치 로그 추가
|
||||
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']
|
||||
logger.debug(f"{index + 1}번째 행 기록 완료")
|
||||
|
||||
wb.save(part_file_name) # SaveCopyAs 대신 save 사용
|
||||
wb.close()
|
||||
self.saved_files.append(part_file_name)
|
||||
logger.info(f"파일 '{part_file_name}'에 데이터가 저장되었습니다.")
|
||||
return True # 성공 여부 반환
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"엑셀 저장 중 예외 발생: {e}", exc_info=True)
|
||||
return False # 실패 시 False 반환
|
||||
finally:
|
||||
app.quit()
|
||||
logger.debug("xlwings 종료")
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class TaobaoScraperApp(QWidget):
|
|||
self.collect_button = QPushButton("수집")
|
||||
self.collect_button.clicked.connect(self.collect_data)
|
||||
self.excel_button = QPushButton("엑셀출력")
|
||||
self.excel_button.clicked.connect(self.export_to_excel)
|
||||
self.excel_button.clicked.connect(self.save_to_excel)
|
||||
self.close_button = QPushButton("닫기")
|
||||
self.close_button.clicked.connect(self.close)
|
||||
|
||||
|
|
@ -45,8 +45,8 @@ class TaobaoScraperApp(QWidget):
|
|||
self.playwright_thread.run()
|
||||
|
||||
@Slot()
|
||||
def export_to_excel(self):
|
||||
success = self.excel_exporter.export_to_excel()
|
||||
def save_to_excel(self):
|
||||
success = self.excel_exporter.save_to_excel()
|
||||
if success:
|
||||
QMessageBox.information(self, "엑셀 출력", "엑셀 파일로 저장 완료")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import sys
|
|||
import random
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from playwright.async_api import async_playwright, Page
|
||||
|
||||
|
|
@ -18,19 +19,14 @@ class PlaywrightThread(QThread):
|
|||
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')
|
||||
extension_path = os.path.join(os.path.dirname(sys.executable), 'browsers', 'extensions', '1.1.100_0')
|
||||
user_data_dir = os.path.join(os.path.dirname(sys.executable), 'browsers', 'user_data')
|
||||
else:
|
||||
browser_path = os.path.join(os.path.dirname(__file__), 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
|
||||
extension_path = os.path.join(os.path.dirname(__file__), 'browsers', 'extensions', '1.1.100_0')
|
||||
user_data_dir = os.path.join(os.path.dirname(__file__), 'browsers', 'user_data')
|
||||
|
||||
logger.debug(f"브라우저 경로: {browser_path}")
|
||||
logger.debug(f"확장 프로그램 경로: {extension_path}")
|
||||
logger.debug(f"사용자 폴더 경로: {user_data_dir}")
|
||||
|
||||
# 사용자 에이전트 설정
|
||||
user_agent = random.choice([
|
||||
|
|
@ -42,71 +38,112 @@ class PlaywrightThread(QThread):
|
|||
])
|
||||
logger.debug(f"user_agent: {user_agent}")
|
||||
|
||||
# 브라우저 시작 및 설정
|
||||
context = await p.chromium.launch_persistent_context(
|
||||
user_data_dir=user_data_dir,
|
||||
headless=False,
|
||||
# 브라우저 시작 (headless 모드)
|
||||
browser = await p.chromium.launch(
|
||||
headless=True, # headless 모드로 설정
|
||||
executable_path=browser_path,
|
||||
locale="ko-KR",
|
||||
permissions=["geolocation", "notifications"],
|
||||
geolocation={"latitude": 37.5665, "longitude": 126.9780},
|
||||
user_agent=user_agent,
|
||||
args=[
|
||||
'--disable-popup-blocking',
|
||||
f'--disable-extensions-except={extension_path}',
|
||||
f'--load-extension={extension_path}',
|
||||
'--start-maximized',
|
||||
'--window-size=1920,1080'
|
||||
]
|
||||
)
|
||||
|
||||
# 현재 활성 탭 가져오기
|
||||
if context.pages:
|
||||
page = context.pages[0] # 첫 번째 탭을 가져옴
|
||||
if page.url == "about:blank":
|
||||
logger.debug("현재 탭의 URL이 about:blank입니다. 새 페이지로 이동합니다.")
|
||||
await page.goto("https://world.taobao.com/")
|
||||
else:
|
||||
page = await context.new_page()
|
||||
await page.goto("https://world.taobao.com/")
|
||||
logger.debug("새로운 페이지가 생성되었습니다.")
|
||||
# 시크릿 브라우저 컨텍스트 생성
|
||||
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")
|
||||
logger.info("타오바오 사이트에 접속했습니다.")
|
||||
await self.wait_for_user()
|
||||
|
||||
# 페이지 로딩 확인 및 pagedown
|
||||
await page.wait_for_selector(".tb-pick-content-item") # 상품 카드 로딩 대기
|
||||
logger.info("페이지 로딩 완료 - Pagedown을 두 번 누릅니다.")
|
||||
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:
|
||||
logger.error(f"브라우저 작업 중 오류 발생: {e}")
|
||||
self.data_collected.emit(False, f"오류 발생: {e}")
|
||||
logger.error(f"브라우저 작업 중 오류 발생: {e}", exec=True)
|
||||
self.data_collected.emit(False, f"오류 발생: {e}", exec=True)
|
||||
|
||||
async def scrape_items(self, page: Page):
|
||||
try:
|
||||
items = await page.query_selector_all(".tb-pick-content-item")
|
||||
items = await page.query_selector_all("div#ice-container div.tb-pick-feeds-container > div")
|
||||
logger.debug(f"총 {len(items)}개의 상품 카드가 발견되었습니다.")
|
||||
|
||||
items_data = []
|
||||
for item in items:
|
||||
item_id = await item.get_attribute("href").split("itemIds=")[-1]
|
||||
pc_url = f"https://item.taobao.com/item.htm?id={item_id}"
|
||||
name = await item.query_selector(".info-wrapper-title-text").inner_text()
|
||||
price = await item.query_selector(".price-value").inner_text()
|
||||
image_url = await item.query_selector(".img-wrapper").get_attribute("style").split("url(")[-1].strip('")')
|
||||
sales = await item.query_selector(".month-sale").inner_text()
|
||||
|
||||
for idx, item in enumerate(items):
|
||||
logger.debug(f"{idx + 1}번째 상품 카드 처리 중 - XPath 확인")
|
||||
|
||||
# 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}")
|
||||
|
||||
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}"
|
||||
logger.debug(f"{idx + 1}번째 상품 ID: {item_id}, 상품 URL: {pc_url}")
|
||||
else:
|
||||
logger.warning(f"{idx + 1}번째 상품 ID를 올바르게 추출할 수 없습니다.")
|
||||
continue
|
||||
else:
|
||||
logger.warning(f"{idx + 1}번째 상품 ID를 가져올 수 없습니다.")
|
||||
continue
|
||||
else:
|
||||
logger.warning(f"{idx + 1}번째 상품의 a 태그를 찾을 수 없습니다.")
|
||||
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}")
|
||||
|
||||
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}")
|
||||
|
||||
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"
|
||||
logger.debug(f"{idx + 1}번째 상품 이미지 URL: {image_url}")
|
||||
|
||||
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}")
|
||||
|
||||
items_data.append((item_id, pc_url, name, float(price), image_url, sales))
|
||||
|
||||
logger.debug(f"수집된 상품 수 : {len(items_data)}")
|
||||
return items_data
|
||||
except Exception as e:
|
||||
logger.error(f"데이터 수집 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
async def wait_for_user(self):
|
||||
await asyncio.sleep(2000)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
def run(self):
|
||||
asyncio.run(self.collect_data())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import sqlite3
|
||||
import logging
|
||||
import requests
|
||||
from googletrans import Translator # 구글 번역 API 라이브러리
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PostProcessor:
|
||||
def __init__(self, db_manager):
|
||||
self.db_manager = db_manager
|
||||
self.translator = Translator() # 구글 번역 초기화
|
||||
|
||||
def translate_name(self, translation_method='google'):
|
||||
try:
|
||||
# 데이터 가져오기
|
||||
items = self.db_manager.fetch_all() # 데이터프레임으로 가져오기
|
||||
if items.empty:
|
||||
logger.warning("DB에 데이터가 없습니다.")
|
||||
return
|
||||
|
||||
for index, row in items.iterrows():
|
||||
if pd.isna(row['translated_name']): # 번역된 상품명이 비어 있는 경우
|
||||
item_id = row['id']
|
||||
original_name = row['name']
|
||||
translated_name = self._translate_text(original_name, method=translation_method)
|
||||
|
||||
# 데이터베이스에 번역된 상품명 업데이트
|
||||
self.db_manager.update_translated_name(item_id, translated_name)
|
||||
logger.info(f"상품 ID {item_id}의 상품명이 번역되었습니다: {translated_name}")
|
||||
|
||||
logger.info("모든 번역 작업 완료")
|
||||
except Exception as e:
|
||||
logger.error(f"번역 처리 중 예외 발생: {e}")
|
||||
|
||||
def _translate_text(self, text, method='google'):
|
||||
if method == 'deepl':
|
||||
return self._translate_deepl(text)
|
||||
elif method == 'gpt-4o-mini':
|
||||
return self._translate_gpt(text)
|
||||
else:
|
||||
return self._translate_google(text)
|
||||
|
||||
def _translate_google(self, text):
|
||||
try:
|
||||
translated = self.translator.translate(text, src='zh-cn', dest='ko')
|
||||
return translated.text
|
||||
except Exception as e:
|
||||
logger.error(f"구글 번역 중 오류 발생: {e}")
|
||||
return text
|
||||
|
||||
def _translate_deepl(self, text):
|
||||
try:
|
||||
# DeepL API 키 설정
|
||||
api_key = 'YOUR_DEEPL_API_KEY'
|
||||
url = f"https://api-free.deepl.com/v2/translate?auth_key={api_key}&text={text}&source_lang=ZH&target_lang=KO"
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data['translations'][0]['text']
|
||||
else:
|
||||
logger.error(f"DeepL 번역 중 오류 발생: {response.text}")
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.error(f"DeepL 번역 요청 중 오류 발생: {e}")
|
||||
return text
|
||||
|
||||
def _translate_gpt(self, text):
|
||||
try:
|
||||
# GPT-4o-mini API 호출 예시 (구현 필요)
|
||||
logger.info("GPT-4o-mini 번역 기능은 구현되지 않았습니다.")
|
||||
return text # 현재는 원문 반환
|
||||
except Exception as e:
|
||||
logger.error(f"GPT-4o-mini 번역 중 오류 발생: {e}")
|
||||
return text
|
||||
BIN
taobao_items.db
BIN
taobao_items.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue