195 lines
8.9 KiB
Python
195 lines
8.9 KiB
Python
from pywinauto import Application, findwindows, timings
|
|
from pywinauto.timings import wait_until
|
|
import time, logging, os, re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ShoppingLensScraper:
|
|
def __init__(self, whale_exe_path=None, user_data_dir=None, cache_dir=None, logger=None):
|
|
# self.whale_exe_path = whale_exe_path
|
|
# self.user_data_dir = user_data_dir
|
|
# self.cache_dir = cache_dir
|
|
self.logger = logger or logging.getLogger(__name__)
|
|
|
|
def save_control_identifiers(self, window, output_file="debug_controls.txt", logger=None):
|
|
"""
|
|
특정 윈도우의 컨트롤 식별자를 파일로 저장합니다.
|
|
:param window: 탐지 대상 윈도우
|
|
:param output_file: 출력 파일 경로
|
|
:param logger: 로깅 객체 (선택적)
|
|
"""
|
|
try:
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
original_stdout = os.sys.stdout # 기존 stdout 백업
|
|
os.sys.stdout = f # stdout을 파일로 변경
|
|
window.print_control_identifiers() # 컨트롤 식별자 출력
|
|
os.sys.stdout = original_stdout # stdout 복원
|
|
if logger:
|
|
logger.info(f"컨트롤 식별자가 {output_file}에 저장되었습니다.")
|
|
else:
|
|
print(f"컨트롤 식별자가 {output_file}에 저장되었습니다.")
|
|
except Exception as e:
|
|
if logger:
|
|
logger.error(f"컨트롤 식별자를 저장하는 중 오류 발생: {e}")
|
|
else:
|
|
print(f"컨트롤 식별자를 저장하는 중 오류 발생: {e}")
|
|
|
|
def search(self, whale_window, image_url, max_retries=3):
|
|
"""네이버 웨일을 이용하여 쇼핑 렌즈 검색 수행"""
|
|
for attempt in range(max_retries):
|
|
try:
|
|
print(f"검색 시도 {attempt + 1}/{max_retries}")
|
|
|
|
# URL 이동
|
|
print("주소창으로 이동")
|
|
address_bar = whale_window.child_window(title="주소창 및 검색창", control_type="Edit")
|
|
address_bar.click_input()
|
|
for chunk in [image_url[i:i + 10] for i in range(0, len(image_url), 10)]:
|
|
address_bar.type_keys(chunk, with_spaces=True)
|
|
address_bar.type_keys("{ENTER}")
|
|
time.sleep(2) # 페이지 로딩 대기
|
|
|
|
# 이미지 우클릭 및 쇼핑 렌즈 실행
|
|
image = whale_window.child_window(control_type="Image")
|
|
image.right_click_input()
|
|
|
|
print("쇼핑 렌즈 검색")
|
|
menu_item = whale_window.child_window(title="쇼핑렌즈로 검색하기", control_type="MenuItem")
|
|
menu_item.click_input()
|
|
|
|
print("쇼핑 렌즈 검색 결과 기다리기")
|
|
timings.wait_until(10, 0.5, lambda: whale_window.child_window(control_type="List", found_index=0).exists())
|
|
|
|
time.sleep(0.5)
|
|
|
|
# control_type="Document" 요소 검색
|
|
document_elements = whale_window.descendants(control_type="Document")
|
|
if not document_elements:
|
|
raise RuntimeError("Document 요소를 찾을 수 없습니다.")
|
|
|
|
type_index = None
|
|
for document in document_elements:
|
|
title_text = document.window_text() # Document의 title 텍스트 가져오기
|
|
|
|
if "쇼핑렌즈 검색결과" in title_text:
|
|
type_index = 1 # "쇼핑렌즈 검색결과" 포함 시 type_index=1
|
|
break
|
|
elif "본문 바로가기" in title_text:
|
|
type_index = 0 # "본문 바로가기" 포함 시 type_index=0
|
|
break
|
|
else:
|
|
type_index = None
|
|
|
|
if type_index is None:
|
|
raise RuntimeError("적절한 type_index를 결정할 수 없습니다.")
|
|
|
|
print(f"결정된 type_index: {type_index}")
|
|
|
|
list_box = whale_window.child_window(control_type="List", found_index=type_index)
|
|
listbox_texts = list_box.texts()
|
|
|
|
products_infos = self.process_listbox_data(listbox_texts, type_index)
|
|
|
|
print(f"스크래핑 완료. 추출된 데이터 : {len(products_infos)}개 \n {products_infos}")
|
|
|
|
self.click_back_and_img_buttons(whale_window)
|
|
|
|
return products_infos
|
|
|
|
except Exception as e:
|
|
print(f"검색 시도 {attempt + 1}/{max_retries} 실패: {e}")
|
|
if attempt == max_retries - 1:
|
|
print("최대 재시도 횟수에 도달했습니다.")
|
|
try:
|
|
# 검색실패시 해당시점의 컨트롤식별자 저장
|
|
self.save_control_identifiers(whale_window, "debug_controls.txt", self.logger)
|
|
print("컨트롤 식별자가 저장되었습니다.")
|
|
except Exception as ee:
|
|
print(f"컨트롤 식별자 저장 중 오류 발생: {ee}")
|
|
return []
|
|
|
|
time.sleep(2) # 재시도 전 대기
|
|
|
|
def click_back_and_img_buttons(self, window):
|
|
"""
|
|
뒤로가기 버튼을 클릭하는 함수
|
|
:param window: pywinauto WindowSpecification 객체
|
|
"""
|
|
try:
|
|
# 뒤로가기 버튼 클릭
|
|
back_button = window.child_window(title="뒤로", control_type="Button")
|
|
img_button = window.child_window(title="누락된 이미지 설명을 확인하려면 컨텍스트 메뉴를 여세요.", control_type="Image")
|
|
if back_button.exists():
|
|
print("이미지버튼을 클릭합니다.")
|
|
img_button.click_input()
|
|
print("뒤로가기 버튼을 클릭합니다.")
|
|
back_button.click_input()
|
|
else:
|
|
print("버튼을 찾을 수 없습니다.")
|
|
except Exception as e:
|
|
print(f"버튼 클릭 중 오류 발생: {e}")
|
|
|
|
def process_listbox_data(self, listbox_texts, type_index, max_product_count=5):
|
|
"""
|
|
list_box.texts() 데이터를 가공하여 상품 정보를 추출합니다.
|
|
|
|
:param listbox_texts: ListBox 텍스트 리스트
|
|
:param type_index: 0 또는 1로 구분된 데이터 타입
|
|
:return: 가공된 상품 정보 리스트 (최대 5개)
|
|
"""
|
|
processed_data = []
|
|
|
|
if type_index == 0:
|
|
for idx, text_list in enumerate(listbox_texts):
|
|
if idx >= max_product_count: # 최대 max_product_count개의 상품만 처리
|
|
break
|
|
try:
|
|
# 리스트 형태인지 확인 후 처리
|
|
if isinstance(text_list, list):
|
|
text = " ".join(text_list) # 리스트를 문자열로 변환
|
|
else:
|
|
text = text_list
|
|
|
|
# 상품명 추출 (반복된 단어 찾아서 처리)
|
|
match = re.search(r"(.*?)\s\1", text)
|
|
product_name = match.group(1) if match else text.split()[0]
|
|
|
|
# 가격 정보 추출
|
|
prices = [int(price.replace(",", "")) for price in re.findall(r"(\d{1,3}(?:,\d{3})*)원", text)]
|
|
product_price = prices[0] if prices else 0
|
|
shipping_price = prices[1] if len(prices) > 1 else 0
|
|
|
|
processed_data.append({
|
|
"상품명": product_name,
|
|
"상품가격": product_price,
|
|
"배송비": shipping_price
|
|
})
|
|
except Exception as e:
|
|
print(f"데이터 처리 중 오류 발생 (type_index=0): {text_list} -> {e}")
|
|
|
|
elif type_index == 1:
|
|
for idx, text_list in enumerate(listbox_texts):
|
|
if idx >= max_product_count: # 최대 max_product_count 개의 상품만 처리
|
|
break
|
|
try:
|
|
if isinstance(text_list, list):
|
|
product_name = re.sub(r"[^\w\s]", "", text_list[0]).strip()
|
|
product_price = int(text_list[2].replace(",", ""))
|
|
else:
|
|
raise ValueError("type_index=1 데이터가 리스트 형식이 아님")
|
|
|
|
shipping_price = 0
|
|
|
|
processed_data.append({
|
|
"상품명": product_name,
|
|
"상품가격": product_price,
|
|
"배송비": shipping_price
|
|
})
|
|
except Exception as e:
|
|
print(f"데이터 처리 중 오류 발생 (type_index=1): {text_list} -> {e}")
|
|
|
|
else:
|
|
raise ValueError(f"알 수 없는 type_index 값: {type_index}")
|
|
|
|
return processed_data
|