AutoPercenty/edit/options.py

356 lines
18 KiB
Python

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import time
from edit.action_elements import click_element, return_element
from ai.deepl import trans
# from ai.compare import find_most_similar_image_by_one
import re
import logging
# 로거 인스턴스 가져오기
logger = logging.getLogger('default_logger')
def modify_option_page(driver, product_info):
# 특수문자와 대체할 문자를 정의합니다.
allowed_special_chars = "!$~()._-=+/"
special_char_replacements = {
"*" : "X", # '*' 특수문자를 'X'로 대체합니다.
"" : "(",
"" : ")",
"[" : "(",
"]" : ")",
"," : ".",
}
# 옵션 정보를 담을 딕셔너리 초기화
options_info = {}
# 옵션 탭으로 이동
logger.debug("옵션탭으로 이동")
option_tab_XPATH = "//div[@id='rc-tabs-0-tab-1']"
click_element(driver, 'XPATH', option_tab_XPATH, 10, 'js')
logger.debug("옵션탭으로 이동 완료")
logger.debug("페이지 로딩 대기")
time.sleep(2) # 페이지 로딩 대기.
logger.debug("옵션타입 체크")
# 옵션 타입 처리
for i in range(1, 4): # 옵션 타입이 1부터 3까지 존재한다고 가정합니다.
option_type_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{i}]/div/div/div[2]/div/div/div[2]/div/span/input"
option_type = return_element(driver, 'XPATH', option_type_xpath, 3)
if option_type:
option_type_value = option_type.get_attribute('value')
options_info[f'option_type_{i}'] = {'options_name': option_type_value, 'options_count': 0, 'price_ranges': []}
logger.debug(f"옵션 타입 추가: {option_type_value}")
else:
logger.debug(f"{option_type_xpath} 요소를 찾을 수 없음.")
option_type_nums = sum(1 for key in options_info.keys() if key.startswith('option_type_'))
logger.debug(f"총 옵션 타입 수: {option_type_nums}")
# 현재 전체 옵션갯수 가져오기
# 각 옵션 타입별로 XPath를 문자열 포맷을 사용하여 한 줄로 작성
option_num_xpaths = [
"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{}]/div/div/div[2]/div/div/div[5]/div/div/label/span[2]".format(idx)
for idx in range(1, 4) # 옵션 타입이 3개까지 있음
]
# 각 옵션 타입의 갯수를 가져와서 options_info에 업데이트
for idx, option_num_xpath in enumerate(option_num_xpaths, start=1):
try:
option_num_element = return_element(driver, 'XPATH', option_num_xpath, 3)
if option_num_element:
option_number = int(re.search(r'\d+', option_num_element.text).group())
logger.debug(f"옵션타입 {idx} 갯수 : {option_number}")
options_info[f'option_type_{idx}']['options_count'] = option_number
else:
logger.debug(f"옵션타입 {idx} 요소를 찾을 수 없음.")
options_info[f'option_type_{idx}']['options_count'] = None
except Exception as e:
logger.error(f"옵션 타입 {idx} 갯수를 가져오는 중 오류 발생: {e}")
# # 가격 낮은 순으로 정렬
# logger.debug("가격 낮은 순으로 정렬")
# low_price_order_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[5]/div/div[3]/button/span"
# click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js')
### ===================================================== #####
### ==이미지 비교는 정확도가 낮아 일단 보류==== #####
# 이미지 비교
# logger.debug("이미지 비교로 유사도 판단 후 옵션 선택")
# # 옵션 이미지 URL을 저장할 리스트 초기화
# option_images_urls = []
# selected_option_indexes = []
# for i in range(1, option_number + 1):
# # 옵션 이미지의 XPath 경로 동적 생성
# opt_img_xpath = f"//li[{i}]/div/div/div/div[2]/div/img"
# try:
# # 해당 XPath를 이용하여 웹 요소 찾기
# opt_img_element = driver.find_element(By.XPATH, opt_img_xpath)
# except Exception as e:
# logger.debug(f"옵션 이미지 xpath를 찾을 수 없음 {e}")
# opt_img_element = None
# # 옵션이미지가 없을 경우 처리
# if not opt_img_element:
# logger.debug(f"{i}번째 옵션이미지가 없음")
# # 아래의 2가지 상황에 맞추어 추가코드 필요
# # 1. 전체 옵션의 이미지가 없는 경우 처리
# # 2. 일부 옵션만 이미지가 없는 경우 처리
# else:
# # 웹 요소의 src 속성에서 이미지 URL 추출
# img_url = opt_img_element.get_attribute('src')
# # 추출한 이미지 URL을 리스트에 추가
# option_images_urls.append(img_url)
# # 유사도 비교로 적합한 이미지를 가진 옵션번호 저장
# # similarity = find_most_similar_image_by_one(original_img, img_url)
# similarity = 0.7
# if similarity > 0.6:
# selected_option_indexes.append(i)
# logger.debug(f"옵션번호 {i}번이 이미지 유사도 {similarity}로 선택되었습니다.")
### ===================================================== #####
# # 선택된 옵션인덱스로 실제 옵션 체크
# # 옵션타입1의 옵션전체 선택&해제 버튼 xpath=(//input[@type='checkbox'])[22]
# # 옵션타입1의 1번 옵션 선택&해제 버튼 xpath=(//input[@type='checkbox'])[23]
# # 옵션타입1의 2번 옵션 선택&해제 버튼 xpath=(//input[@type='checkbox'])[24]
# # 주의점은 해당 옵션을 체크해제할 경우 이미지 주소를 가져올 수 없으므로, 체크 해제 전 모든 옵션에 해당하는 이미지 주소를 가져올 것.
# # 만약 이미지 주소가 none이거나 없으면 이미지 비교를 하지말고 옵션명으로 비교할 것.
# logger.debug("옵션갯수 다시 가져오기")
# selected_option_num_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[5]/div/div/label/span[2]"
# selected_option_num_element = return_element(driver, 'XPATH', selected_option_num_xpath, 10)
# selected_options_num = int(re.search(r'\d+', selected_option_num_element.text).group())
# logger.debug("선택된 옵션 인덱스로 옵션박스 체크 실행")
# 각 옵션 타입에 대한 처리
for option_idx in range(1, option_type_nums + 1):
try:
logger.debug(f"옵션타입 {option_idx}에 대한 처리 시작")
logger.debug("가격 낮은 순으로 정렬")
low_price_order_xpath = f"//div[{option_idx}]/div/div/div[2]/div/div/div[5]/div/div[3]/button"
click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js')
edit_option(driver, option_idx, options_info[f'option_type_{option_idx}']['options_count']) # 각 옵션의 전체 체크박스 해제
# 가격 낮은 순으로 재정렬
logger.debug("가격 낮은 순으로 재정렬")
click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js')
option_name_trans(driver, option_idx, options_info[f'option_type_{option_idx}']['options_count'], allowed_special_chars, special_char_replacements)
except KeyError:
logger.error(f"옵션타입 {option_idx}에 대한 정보가 없습니다.")
except Exception as e:
logger.error(f"옵션타입 {option_idx} 처리 중 오류 발생: {e}")
logger.debug("옵션선택과 옵션명 수정 완료 후 옵션명 정리")
# 빈칸제거
# xpath=//span[contains(.,'빈칸 제거')]
space_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div/div/div/div[3]/button/span"
click_element(driver, 'XPATH', space_xpath, 10, 'normal')
# A-Z
# xpath=//span[contains(.,'A-Z')]
AtoZ_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div/div/div/div/button/span"
click_element(driver, 'XPATH', AtoZ_xpath, 10, 'normal')
# 1-99
# xpath=//span[contains(.,'1-99')]
# xpath=//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div/div/div/div[2]/button/span
logger.debug("옵션 가격범위를 product_info에 업데이트")
update_price_range(driver, options_info, product_info)
save_xpath="//button[contains(.,'저장하기')]"
click_element(driver, 'XPATH', save_xpath, 10, 'js')
logger.debug("옵션 정리 후 저장버튼 클릭 완료")
def edit_option(driver, option_type, option_count):
"""
주어진 옵션 타입에 대해 옵션 편집 동작을 수행합니다.
Parameters:
- driver: WebDriver 인스턴스
- option_type: 옵션 타입 (1 또는 2)
- option_count: 옵션 타입 내의 옵션 갯수
"""
try:
# 전체 체크박스 선택 해제
logger.debug("전체 체크박스 선택 해제")
select_all_checkbox_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type}]/div/div/div[2]/div/div/div[5]/div/div/label/span"
click_element(driver, 'XPATH', select_all_checkbox_xpath, 10, 'js')
logger.debug(f"옵션타입 {option_type} 전체 선택 체크박스 해제")
time.sleep(1)
logger.debug(f"옵션타입 {option_type}의 옵션 선택 시작")
if option_count <= 2:
# 옵션 갯수가 2개 이하면 모든 옵션 체크
for i in range(1, option_count + 1):
option_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[1]/label/span"
click_element(driver, 'XPATH', option_xpath, 10, 'ac')
logger.debug(f"옵션 {i} 체크")
time.sleep(0.5)
else:
# 옵션 갯수가 3개 이상인 경우, 첫 번째 옵션 제외 최대 5개 옵션 체크
for i in range(2, min(option_count, 6) + 1): # 첫 번째 옵션 제외, 최대 5개 선택
option_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[1]/label/span"
click_element(driver, 'XPATH', option_xpath, 10, 'ac')
logger.debug(f"옵션 {i} 체크")
time.sleep(0.5)
except Exception as e:
logger.error(f"옵션 타입 {option_type} 편집 중 예외 발생: {e}")
def option_name_trans(driver, option_type_number, option_count, allowed_special_chars, special_char_replacements):
"""
원본 옵션명을 수집하여 번역 후 새로 입력
Parameters:
- param driver: WebDriver 인스턴스
- param options_info : 옵션 정보 Dict
- param allowed_special_chars: 허용되는 특수문자 (기본값: "!$~()._-=+/")
- type allowed_special_chars: str
- param special_char_replacements: 특수문자 대체 규칙 (기본값: None)
- type special_char_replacements: dict
"""
logger.debug("선택된 옵션명 DeepL로 번역 후 새로운 옵션명 입력")
ori_optionNames = []
# 원본 옵션명 추출
try:
for i in range(1, option_count + 1):
if option_type_number == 1:
ori_optionName_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[3]/span"
elif option_type_number == 2:
ori_optionName_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div[3]/span"
else: # option_type_number == 3
ori_optionName_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div[4]/div/span"
ori_optionName_element = driver.find_element(By.XPATH, ori_optionName_xpath)
ori_optionName = ori_optionName_element.text.strip()
ori_optionNames.append(ori_optionName)
except Exception as e:
logger.debug(f"원본옵션명 처리중 에러발생 : {e}")
# 원본 옵션명을 하나의 텍스트로 합치기
combined_ori_optionNames = '\n\n'.join(ori_optionNames)
logger.debug("특수문자 제거 및 대체")
cleaned_ori_text = replace_or_remove_special_chars(combined_ori_optionNames, allowed_special_chars, special_char_replacements)
logger.debug(f"원본옵션명 집합 \n {cleaned_ori_text}")
logger.debug("번역 시행")
trans_optionNames_text = trans(cleaned_ori_text) # DeepL 번역 함수
logger.debug(f"번역된 텍스트 \n {trans_optionNames_text} ")
trans_optionNames = trans_optionNames_text.split('\n\n')
logger.debug("번역 텍스트 나누기")
# 번역된 옵션명을 각 input 요소에 입력
try:
for i, trans_optionName in enumerate(trans_optionNames, 1):
optionName_input_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[2]/div[1]/span/input" if option_type_number != 2 else f"//div[{option_type_number}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div[2]/div[1]/span/input"
optionName_input_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, optionName_input_xpath)))
logger.debug("마우스 액션체인으로 클릭하여 안정적인 포커스 이동")
ActionChains(driver).click(optionName_input_element).perform()
logger.debug("기존 텍스트 클리어")
optionName_input_element.send_keys(Keys.CONTROL + "a") # 전체 선택
optionName_input_element.send_keys(Keys.DELETE) # 삭제
time.sleep(0.5)
logger.debug("번역 텍스트 sendkey")
optionName_input_element.send_keys(trans_optionName) # 번역된 옵션명 입력
logger.debug(f"{i}번째 옵션명에 '{trans_optionName}' 입력")
except Exception as e:
logger.debug(f"번역 옵션명 처리중 에러발생 : {e}")
# 특수문자를 대체하거나 제거하는 함수를 정의합니다.
def replace_or_remove_special_chars(text, allowed_special_chars="!$~()._-=+/", special_char_replacements=None):
"""
주어진 문자열에서 특정 특수문자를 대체하거나 제거합니다.
:param text: 대상 문자열
:type text: str
:param allowed_special_chars: 허용되는 특수문자 (기본값: "!$~()._-=+/")
:type allowed_special_chars: str
:param special_char_replacements: 특수문자 대체 규칙 (기본값: None)
:type special_char_replacements: dict
:return: 특수문자가 대체 또는 제거된 문자열
:rtype: str
"""
# 특수문자 대체 규칙을 설정합니다.
if special_char_replacements is None:
special_char_replacements = {}
# 특수문자를 대체합니다.
for char, replacement in special_char_replacements.items():
text = text.replace(char, replacement)
# 허용되는 특수문자 패턴을 정의합니다.
pattern = f"[^{re.escape(allowed_special_chars)}\w\s]"
# 허용되는 특수문자를 제외하고 모든 특수문자를 제거합니다.
cleaned_text = re.sub(pattern, "", text, flags=re.UNICODE)
return cleaned_text
def update_price_range(driver, options_info, product_info):
try:
# 모든 옵션 타입에서 수집된 가격 범위를 저장할 리스트 초기화
all_price_ranges = []
# 각 옵션 타입에 대한 처리
for option_type_key, option_data in options_info.items():
option_number = option_data.get('options_count')
# 각 옵션의 가격 범위 수집
for i in range(1, option_number + 1):
option_type_number = [int(s) for s in option_type_key.split('_') if s.isdigit()][0]
middle_index = '3' if option_type_number == 1 else '2'
price_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[6]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[{middle_index}]/div[1]/div[2]/span/sup"
try:
price_element = return_element(driver, 'XPATH', price_xpath, 5)
price_text = re.sub(r'[^\d\-]', '', price_element.text.strip())
price_parts = price_text.split('-')
price_range = [int(price.strip()) for price in price_parts]
all_price_ranges.extend(price_range)
except Exception as e:
logger.error(f"옵션 타입 {option_type_key}{i}번 가격 범위를 가져오는데 실패했습니다. 에러: {e}")
# 모든 옵션의 가격 범위에서 전체 최소값과 최대값 추출
if all_price_ranges:
overall_min_price = min(all_price_ranges)
overall_max_price = max(all_price_ranges)
# product_info 업데이트
product_info.option_high_price = overall_max_price
product_info.option_low_price = overall_min_price
logger.debug(f"전체 옵션의 가격 범위 업데이트: 최소값 {overall_min_price}, 최대값 {overall_max_price}")
except Exception as e:
logger.error(f"가격 범위 업데이트 중 오류 발생: {e}")