200 lines
8.4 KiB
Python
200 lines
8.4 KiB
Python
import re
|
|
|
|
class LensParser:
|
|
def __init__(self, app, logger):
|
|
self.app = app
|
|
self.logger = logger
|
|
|
|
def extract_product_data(self, list_box):
|
|
"""
|
|
ListBox에서 제품 데이터를 추출합니다.
|
|
|
|
Args:
|
|
list_box: ListBox 객체
|
|
Returns:
|
|
list: 추출된 제품 정보의 리스트
|
|
"""
|
|
product_data = []
|
|
try:
|
|
# ListBox 내부의 모든 ListItem 순회
|
|
list_items = list_box.descendants(control_type="ListItem")
|
|
for item in list_items:
|
|
# 첫 번째 Hyperlink를 찾기
|
|
hyperlink = item.child_window(control_type="Hyperlink", found_index=0)
|
|
if hyperlink.exists():
|
|
title = hyperlink.window_text()
|
|
product_info = self.parse_product_info(title)
|
|
if product_info:
|
|
product_data.append(product_info)
|
|
except Exception as e:
|
|
print(f"데이터 추출 중 오류 발생: {e}")
|
|
return product_data
|
|
|
|
# @staticmethod
|
|
def parse_product_info_ori(self, titles):
|
|
"""
|
|
쇼핑 렌즈 결과에서 상품 정보를 파싱합니다.
|
|
:param titles: Hyperlink title의 리스트
|
|
:return: 구조화된 상품 정보 리스트
|
|
"""
|
|
products = []
|
|
|
|
for title in titles:
|
|
try:
|
|
# 정규식으로 상품명, 가격, 배송비, 판매처 정보 추출
|
|
match = re.search(
|
|
r"(?P<name>.+?)\s(?P<price>최저\s[0-9,]+원|[0-9,]+원)\s(?P<shipping>무료|배송비\s무료|배송비\s[0-9,]+원)(?:\s판매처\s(?P<seller_count>\d+))?(?:\s별점\s(?P<rating>[0-9.]+))?",
|
|
title
|
|
)
|
|
if match:
|
|
product_info = {
|
|
"name": match.group("name"),
|
|
"price": match.group("price"),
|
|
"shipping": match.group("shipping"),
|
|
"seller_count": match.group("seller_count") or "1",
|
|
"rating": match.group("rating") or None,
|
|
}
|
|
# 필터링된 상품명에서 중복 제거
|
|
filtered_name = self._filter_duplicate_name(product_info["name"])
|
|
product_info["filtered_name"] = filtered_name
|
|
products.append(product_info)
|
|
print(f"product_info : {product_info}")
|
|
else:
|
|
print(f"상품 정보를 파싱할 수 없습니다: {title}")
|
|
except Exception as e:
|
|
print(f"상품 정보 파싱 중 오류 발생: {title} -> {e}")
|
|
|
|
return products
|
|
|
|
def get_listbox_type(self, listbox_texts):
|
|
"""
|
|
ListBox 텍스트 내용을 기반으로 타입을 판별합니다.
|
|
:param listbox_texts: ListBox 텍스트 리스트
|
|
:return: 타입 (1 또는 2)
|
|
"""
|
|
if any("이 정보가 표시된 이유" in text for text in listbox_texts):
|
|
return 1
|
|
elif any("북마크 관리자로 가기" in text for text in listbox_texts):
|
|
return 2
|
|
return None
|
|
|
|
def parse_product_info(self, list_boxes, whale_window):
|
|
"""
|
|
쇼핑 렌즈 결과에서 상품 정보를 파싱합니다.
|
|
:param titles: Hyperlink title의 리스트
|
|
:param whale_window: Whale 브라우저 창 객체
|
|
:return: 구조화된 상품 정보 리스트
|
|
"""
|
|
products = []
|
|
|
|
try:
|
|
# 모든 ListBox - '' 검색
|
|
# list_boxes = whale_window.descendants(control_type="List")
|
|
|
|
for list_box in list_boxes:
|
|
listbox_texts = list_box.texts()
|
|
listbox_type = self.get_listbox_type(listbox_texts)
|
|
print(f"타입 : {listbox_type}")
|
|
|
|
if listbox_type == 1:
|
|
print("1번 타입 ListBox 감지됨, 첫 번째 ListBox에서 검색")
|
|
# 1번 타입 처리
|
|
for title in listbox_texts:
|
|
try:
|
|
# 기존의 1번 타입 처리 방식 유지
|
|
match = re.search(
|
|
r"(?P<name>.+?)\s(?P<price>최저\s[0-9,]+원|[0-9,]+원)\s(?P<shipping>무료|배송비\s무료|배송비\s[0-9,]+원)(?:\s판매처\s(?P<seller_count>\d+))?(?:\s별점\s(?P<rating>[0-9.]+))?",
|
|
title
|
|
)
|
|
if match:
|
|
product_info = {
|
|
"type": "type1",
|
|
"name": match.group("name"),
|
|
"price": match.group("price"),
|
|
"shipping": match.group("shipping"),
|
|
"seller_count": match.group("seller_count") or "1",
|
|
"rating": match.group("rating") or None,
|
|
}
|
|
products.append(product_info)
|
|
else:
|
|
print(f"1번 타입 - 상품 정보를 파싱할 수 없습니다: {title}")
|
|
except Exception as e:
|
|
print(f"1번 타입 - 상품 정보 파싱 중 오류 발생: {title} -> {e}")
|
|
|
|
elif listbox_type == 2:
|
|
print("2번 타입 ListBox 감지됨, 두 번째 ListBox에서 검색")
|
|
# 2번 타입 처리
|
|
list_items = list_box.descendants(control_type="ListItem")
|
|
for item in list_items:
|
|
try:
|
|
# Image 요소에서 제목 추출
|
|
image_elem = item.child_window(control_type="Image")
|
|
title = image_elem.window_text()
|
|
|
|
# Text 요소에서 가격 추출
|
|
text_elements = item.descendants(control_type="Text")
|
|
if len(text_elements) > 1:
|
|
price_text = text_elements[1].window_text()
|
|
price = int(re.sub(r"[^\d]", "", price_text))
|
|
|
|
product_info = {
|
|
"type": "type2",
|
|
"title": title,
|
|
"price": price,
|
|
}
|
|
products.append(product_info)
|
|
else:
|
|
print(f"2번 타입 - 가격 정보를 찾을 수 없습니다: {item}")
|
|
except Exception as e:
|
|
print(f"2번 타입 - 상품 정보 파싱 중 오류 발생: {item} -> {e}")
|
|
|
|
else:
|
|
print("알 수 없는 타입의 ListBox입니다. 스킵합니다.")
|
|
continue
|
|
|
|
except Exception as e:
|
|
print(f"ListBox 검색 및 파싱 중 오류 발생: {e}")
|
|
|
|
return products
|
|
|
|
|
|
@staticmethod
|
|
def _filter_duplicate_name(name):
|
|
"""
|
|
상품명에서 중복된 내용을 제거합니다.
|
|
:param name: 원본 상품명
|
|
:return: 중복 제거된 상품명
|
|
"""
|
|
words = name.split()
|
|
seen = set()
|
|
filtered_words = []
|
|
for word in words:
|
|
if word not in seen:
|
|
filtered_words.append(word)
|
|
seen.add(word)
|
|
return " ".join(filtered_words)
|
|
|
|
def filter_titles(self, titles):
|
|
"""
|
|
주어진 제목 리스트에서 '다른 사이트 더보기' 이후 '더보기' 이전의 데이터를 필터링합니다.
|
|
|
|
Args:
|
|
titles (list): 전체 제목 리스트
|
|
|
|
Returns:
|
|
list: 필터링된 제목 리스트
|
|
"""
|
|
try:
|
|
# 시작과 끝 인덱스를 찾기
|
|
start_index = titles.index("다른 사이트 더보기") + 1
|
|
end_index = titles.index("더보기")
|
|
|
|
# 필터링된 데이터 추출
|
|
filtered_titles = titles[start_index:end_index]
|
|
return filtered_titles
|
|
|
|
except ValueError as e:
|
|
# '다른 사이트 더보기' 또는 '더보기'가 없을 경우 예외 처리
|
|
print(f"필터링 중 오류 발생: {e}")
|
|
return []
|