AutoPercenty/edit/options_trans.py

872 lines
46 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, click_and_confirm_tab
from ai.split import parse_and_extract
from ai.deepl import trans, trans_list
from ai.deepl_with_playwright import trans_text, trans_list_text
# from ai.compare import find_most_similar_image_by_one
import re
import logging
import tempfile
from img_trans.image_trans import image_trans
# 로거 인스턴스 가져오기
logger = logging.getLogger('default_logger')
def modify_option_page2(driver, product_info, gemini, translator, login_info):
# 1. 옵션타입이 단일옵션상품이 아닌지 체크
is_option_product = not is_single_option_product(driver)
product_info.option_datas['is_option_product'] = is_option_product
if is_option_product:
# 2. 현재 옵션에 대한 정보 수집
options_info = collect_option_data(driver)
product_info.option_datas['options_info'] = options_info
# 3. 각 옵션타입별로 낮은가격순으로 정렬
sort_options_by_price(driver, options_info)
# 4. 미끼상품 체크 및 제외
filtered_options_info = filter_out_bait_items(options_info)
product_info.option_datas['filtered_options_info'] = filtered_options_info
# 5. 각 옵션타입별로 옵션아이템 가격에 따라 상품 선택
select_valid_options(driver, filtered_options_info, login_info['whether_simpleMode'])
# 6. 선택된 옵션들의 원본 옵션명과 옵션이미지 수집
option_names, option_images = collect_option_names_and_images_and_prices(driver, filtered_options_info)
product_info.option_datas['option_names'] = option_names
product_info.option_datas['option_images'] = option_images
# 7. 수집된 원본 옵션명은 AI 처리 메서드를 통해 번역 및 편집 후 입력
update_option_names(driver, option_names, translator)
# 8. 수집된 옵션 이미지 URL은 이미지 번역 및 업로드 과정 수행
update_option_images(driver, option_images, translator)
# 기타 작업이 필요한 경우 추가
finalize_modifications(driver, product_info.option_datas)
# 저장 작업 등
save_changes(driver)
def modify_option_page(driver, product_info, gemini, translator, login_info):
simpleMode = login_info['whether_simpleMode']
option_css = ".ant-tabs-tab:nth-child(2)"
thumb_data_note = "1"
click_and_confirm_tab(driver, thumb_data_note, 10)
# 특수문자와 대체할 문자를 정의합니다.
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(0.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}")
options_info = collect_option_data(driver)
logger.debug(f"수집된 옵션 정보: {options_info}")
option_type_nums = len(options_info)
time.sleep(30)
logger.debug(f"============================== 잠시 대기 =============================")
# 현재 전체 옵션갯수 가져오기
# 각 옵션 타입별로 XPath를 문자열 포맷을 사용하여 한 줄로 작성
# option_num_xpaths = [
# "//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{}]/div/div/div[2]/div/div/div[4]/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[:option_type_nums], start=1):
# try:
# option_num_element = return_element(driver, 'XPATH', option_num_xpath, 5)
# 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}", exc_info=True)
# # 가격 낮은 순으로 정렬
# 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}", exc_info=True)
# 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[4]/div[2]/div[2]/div/div[4]/button"
# low_price_order_css = f"div#productMainContentContainerId div:nth-child(2) > div > button[type=\"button\"]:nth-child(3)"
low_price_order_css = f"div#productMainContentContainerId div:nth-child({option_idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-cAkrUM.flTgaK > div.ant-row.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(2) > div > button[type=\"button\"]:nth-child(3)"
# click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js')
click_element(driver, 'CSS_SELECTOR', low_price_order_css, 10, 'js')
if not simpleMode: # 심플모드일 경우 옵션 편집하지 않음. 옵션 번역만.
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, translator, product_info, 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}", exc_info=True)
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"
if not simpleMode: # 심플모드일 경우 옵션 AtoZ 편집하지 않음. 옵션 번역만.
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"
select_all_checkbox_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]"
# logger.debug("전체체크박스의 상태확인")
# select_all_checkbox_state_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]/input"
# select_all_checkbox_state_element = return_element(driver, 'XPATH', select_all_checkbox_state_xpath, 10)
# # select_all_checkbox_state_element = driver.find_element_by_xpath(select_all_checkbox_state_xpath)
# if select_all_checkbox_state_element.get_attribute("aria-checked") == "mixed":
# logger.debug("전체체크박스의 일부 선택상태 확인")
# click_element(driver, 'XPATH', select_all_checkbox_xpath, 10, 'js')
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[5]/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[5]/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}", exc_info=True)
def option_name_trans(driver, translator, product_info, 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 = []
deepl_trans_optionNames = []
# 원본 옵션명 추출 및 이미지 URL 확인
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[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[3]/span"
optionImage_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type_number}]/div/div[2]/div/div[{i}]/div/div[2]/div/img"
deleteButton_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type_number}]/div/div[2]/div/div[{i}]/div/div[2]/div/span"
elif option_type_number == 2:
ori_optionName_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/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[5]/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()
cleaned_ori_optionName = replace_or_remove_special_chars(ori_optionName, allowed_special_chars, special_char_replacements)
logger.debug(f"정제된 {i}번째 원본 옵션명 : {cleaned_ori_optionName}")
ori_optionNames.append(cleaned_ori_optionName)
# 1번 옵션타입의 이미지를 번역하고 업로드
if option_type_number == 1:
try:
option_image_element = return_element(driver, 'XPATH', optionImage_xpath, 3)
if option_image_element:
option_image_url = option_image_element.get_attribute('src')
logger.debug(f"옵션 이미지 URL : {option_image_url}")
translated_image = image_trans(option_image_url, translator, 'translate', logger)
if translated_image:
# 이미지 번역 후 업로드
temp_file_path = save_image_to_tempfile(translated_image)
# 이미지 삭제 버튼 클릭
click_element(driver, 'XPATH', deleteButton_xpath, 5, 'ac')
logger.debug(f"이미지 삭제 버튼 클릭 완료")
# 새로운 이미지 업로드
upload_image(driver)
else:
logger.debug(f"옵션 이미지 번역 실패: {option_image_url}")
else:
logger.debug(f"{i}번째 옵션에 이미지 없음")
except Exception as e:
logger.error(f"옵션 이미지 처리 중 오류 발생: {e}", exc_info=True)
except Exception as e:
logger.debug(f"원본옵션명 처리중 에러발생 : {e}", exc_info=True)
# 원본 옵션명을 하나의 텍스트로 합치기
# 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(f"원본옵션 총 {len(ori_optionNames)}개 번역 시행")
deepl_trans_optionNames = translator.translate(ori_optionNames) # DeepL 번역 함수
# deepl_trans_optionNames = trans_list_text(ori_optionNames) # DeepL 번역 함수
#deepl_trans_optionNames = trans_list(ori_optionNames) # DeepL 번역 함수
# trans_optionNames_text = trans_text(cleaned_ori_text) # DeepL_with_playwright 번역 함수
# trans_optionNames_text = trans(cleaned_ori_text) # DeepL 번역 함수
logger.debug(f"번역된 텍스트 \n {deepl_trans_optionNames} ")
logger.debug("번역 텍스트의 특수문자 제거 및 대체")
trans_optionNames = []
for deepl_trans_optionName in deepl_trans_optionNames:
cleand_trans_optionName = replace_or_remove_special_chars(deepl_trans_optionName, allowed_special_chars, special_char_replacements)
trans_optionNames.append(cleand_trans_optionName)
parsed_trans_optionNames, common_names = parse_and_extract(trans_optionNames)
# trans_optionNames = trans_optionNames_text.split('\n\n')
# logger.debug("번역 텍스트 나누기")
logger.debug("product_info 옵션명 업데이트")
try:
product_info.update_option_names(option_type_number, ori_optionNames, parsed_trans_optionNames, common_names)
except Exception as e:
logger.debug(f"product_info 옵션명 업데이트 중 에러 발생 {e}", exc_info=True)
# 번역된 옵션명을 각 input 요소에 입력
try:
logger.debug(f"parsed_trans_optionNames 갯수 : {len(parsed_trans_optionNames)}")
for i, trans_optionName in enumerate(parsed_trans_optionNames, 1):
optionName_input_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/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[5]/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}", exc_info=True)
# 특수문자를 대체하거나 제거하는 함수를 정의합니다.
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[5]/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}", exc_info=True)
# 모든 옵션의 가격 범위에서 전체 최소값과 최대값 추출
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}", exc_info=True)
def save_image_to_tempfile(image):
"""
이미지를 임시 파일로 저장합니다.
Args:
- image (PIL.Image): 번역된 이미지 객체
Returns:
- temp_file_path (str): 임시 파일 경로
"""
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
image.save(temp_file, format="PNG")
temp_file_path = temp_file.name
temp_file.close()
logger.debug(f"임시 파일로 저장된 이미지 경로: {temp_file_path}")
return temp_file_path
def upload_image(driver, option_item_index, temp_file_path):
"""
번역된 이미지를 업로드합니다.
Args:
- driver: WebDriver 인스턴스
- option_item_index (int): 옵션 아이템의 인덱스 (1, 2, 3 등)
- temp_file_path (str): 임시 파일 경로
"""
try:
# 기존 이미지 삭제 버튼 클릭
delete_button_css = f"div#productMainContentContainerId li:nth-child({option_item_index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div > span"
delete_button_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, delete_button_css))
)
delete_button_element.click()
logger.debug(f"{option_item_index}번째 옵션 아이템의 이미지 삭제 버튼 클릭 완료")
time.sleep(1) # 이미지 삭제 대기
# 이미지 업로드 버튼 클릭
upload_button_css = f"div#productMainContentContainerId li:nth-child({option_item_index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img"
upload_button_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, upload_button_css))
)
upload_button_element.click()
logger.debug(f"{option_item_index}번째 옵션 아이템의 이미지 업로드 버튼 클릭 완료")
# 이미지 업로드 다이얼로그의 파일 업로드 버튼 클릭
file_upload_dialog_css = "span > div > div > div:nth-child(2) > div"
file_upload_dialog_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, file_upload_dialog_css))
)
file_upload_dialog_element.click()
logger.debug("파일 업로드 버튼 클릭 완료")
# 파일 선택 대화상자에 임시 이미지 파일 경로를 입력하여 업로드
driver.find_element(By.CSS_SELECTOR, 'input[type="file"]').send_keys(temp_file_path)
logger.debug(f"임시 이미지 파일 경로 지정: {temp_file_path}")
# 이미지 삽입 버튼 클릭
insert_button_css = "div.ant-modal-footer > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary"
insert_button_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, insert_button_css))
)
insert_button_element.click()
logger.debug("이미지 삽입 버튼 클릭 완료")
# 임시 파일 삭제
os.remove(temp_file_path)
logger.debug(f"임시 파일 삭제 완료: {temp_file_path}")
except Exception as e:
logger.error(f"이미지 업로드 중 에러 발생: {e}", exc_info=True)
def collect_option_data_ori(driver):
"""
웹페이지에서 옵션 타입의 갯수와 각 타입별 옵션 아이템의 갯수를 수집합니다.
Parameters:
- driver: WebDriver 인스턴스
Returns:
- filtered_options_info (dict): 옵션 타입과 각 타입별 옵션 아이템 갯수를 포함하는 필터링된 딕셔너리
"""
options_info = {}
try:
# 먼저 상품 유형을 확인합니다.
if not is_option_product(driver):
logger.debug("단일 상품이므로 옵션 정보 수집을 생략합니다.")
return options_info # 단일 상품일 경우 빈 딕셔너리를 반환
# 옵션 타입 컨테이너의 CSS 선택자
option_types_container_css = "div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2)"
# 옵션 타입 컨테이너 요소를 찾습니다.
option_types_container = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, option_types_container_css))
)
# 옵션 타입 갯수를 추출하기 위한 CSS 선택자
option_type_css = "div.ant-col.css-1li46mu > div.sc-stxIr.eeUZbv"
option_type_elements = option_types_container.find_elements(By.CSS_SELECTOR, option_type_css)
option_type_count = len(option_type_elements)
logger.debug(f"옵션 타입의 총 갯수: {option_type_count}")
for idx in range(1, option_type_count + 1):
# 옵션 타입 이름 추출
# option_type_name_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{idx}]/div/div/div[2]/div/div/div[2]/div/span/input"
# option_type_name_element = WebDriverWait(driver, 10).until(
# EC.presence_of_element_located((By.XPATH, option_type_name_xpath))
# )
# option_type_name = option_type_name_element.get_attribute('value').strip()
option_type_name_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.ant-row.ant-row-no-wrap.ant-row-middle.css-1li46mu > div:nth-child(1) > span > input"
option_type_name__by_css_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, option_type_name_css))
)
option_type_name = option_type_name__by_css_element.get_attribute('value').strip()
# 옵션 타입별 옵션 아이템 갯수 추출
# option_items_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{idx}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li"
# option_items = driver.find_elements(By.CSS_SELECTOR, option_items_xpath)
# option_items_count = len(option_items)
option_items_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li"
option_items = driver.find_elements(By.CSS_SELECTOR, option_items_css)
option_items_count = len(option_items)
# 각 옵션 타입별 옵션명, 이미지 및 가격을 수집
option_names_and_images = collect_option_names_and_images_and_price(driver, idx, option_items_count)
options_info[f'option_type_{idx}'] = {
'name': option_type_name,
'items_count': option_items_count,
'items': option_names_and_images
}
logger.debug(f"옵션 타입 {idx}: {option_type_name}, 옵션 아이템 수: {option_items_count}")
for item in option_names_and_images:
logger.debug(f" - 옵션 아이템: 이름={item['name']}, 이미지 URL={item['image_url']}, 가격={item['low_price']} - {item['high_price']}")
except Exception as e:
logger.error(f"옵션 타입 및 아이템 수집 중 오류 발생: {e}", exc_info=True)
return options_info
def collect_option_data(driver, product_info):
options_info = {}
# 옵션 타입을 찾는 CSS 선택자
option_types_container_css = "div#productMainContentContainerId div.ant-col.css-1li46mu div.sc-stxIr.eeUZbv"
option_type_elements = driver.find_elements(By.CSS_SELECTOR, option_types_container_css)
# 각 옵션 타입을 순회하며 정보 수집
for idx, option_type_element in enumerate(option_type_elements, start=1):
try:
# 옵션 타입 이름 추출
option_type_name_css = f"div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2) > div > div:nth-child({idx}) > div > div > div.ant-row.ant-row-no-wrap.ant-row-middle.css-1li46mu > div:nth-child(1) > span > input"
option_type_name_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, option_type_name_css))
)
option_type_name = option_type_name_element.get_attribute('value').strip()
# 옵션 아이템을 찾는 CSS 선택자
option_items_css = f"div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2) > div > div:nth-child({idx}) div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li"
option_items_elements = driver.find_elements(By.CSS_SELECTOR, option_items_css)
option_items_count = len(option_items_elements)
# 옵션 아이템 정보 수집
option_items_info = []
for item_idx, item_element in enumerate(option_items_elements, start=1):
try:
# 옵션 이름 추출
name_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({item_idx}) > div > div.ant-col.css-1li46mu > div > div.ant-col.css-1li46mu > span"
name_element = item_element.find_element(By.CSS_SELECTOR, name_css)
item_name = name_element.text.strip()
# 옵션 이미지 URL 추출
image_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({item_idx}) > div > div.ant-col.css-1li46mu > div > div > div > img"
image_element = item_element.find_element(By.CSS_SELECTOR, image_css)
image_url = image_element.get_attribute('src') if image_element else None
# 옵션 가격 범위 추출
price_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({item_idx}) > div > div.ant-col.css-1li46mu > div > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > span > sup"
price_element = item_element.find_element(By.CSS_SELECTOR, price_css)
price_text = price_element.get_attribute('title').strip()
# 가격 범위 추출 및 처리
price_range = re.findall(r'\d+', price_text.replace(',', ''))
price_range = (int(price_range[0]), int(price_range[1])) if len(price_range) == 2 else (int(price_range[0]), int(price_range[0]))
# 옵션 정보 저장
option_items_info.append({
'name': item_name,
'image_url': image_url,
'price_range': price_range
})
except Exception as e:
logger.error(f"옵션 아이템 수집 중 오류 발생: {e}", exc_info=True)
# 옵션 타입 정보 저장
options_info[f'option_type_{idx}'] = {
'name': option_type_name,
'items_count': option_items_count,
'items': option_items_info
}
except Exception as e:
logger.error(f"옵션 타입 및 아이템 수집 중 오류 발생: {e}", exc_info=True)
product_info.option_datas = options_info
return options_info
def collect_option_names_and_images_and_price(driver, option_type_number, option_items_count):
"""
각 옵션 타입별로 원본 옵션명, 옵션 이미지, 옵션 가격을 수집합니다.
Parameters:
- driver: WebDriver 인스턴스
- option_type_number: 옵션 타입 번호 (1, 2, 3 등)
- option_items_count: 해당 옵션 타입의 옵션 아이템 갯수
Returns:
- option_data (list): 옵션명, 이미지 URL, 가격을 포함하는 리스트
"""
option_data = []
try:
for i in range(1, option_items_count + 1):
# 원본 옵션명 수집
option_name_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[3]/span"
option_name_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, option_name_xpath))
)
option_name = option_name_element.text.strip()
# 옵션 이미지 URL 수집 (옵션 타입 1에만 옵션이미지 존재)
option_image_url = None
if option_type_number == 1:
option_image_xpath = f"div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(2) > div > img"
try:
option_image_element = driver.find_element(By.CSS_SELECTOR, option_image_xpath)
option_image_url = option_image_element.get_attribute('src')
except Exception:
logger.debug(f"{option_type_number}번 옵션타입의 {i}번째 옵션에 이미지 없음")
# 옵션 가격 수집
option_price_xpath = f"div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > span > sup"
try:
option_price_element = driver.find_element(By.CSS_SELECTOR, option_price_xpath)
option_price_text = option_price_element.get_attribute('title') # 또는 .text를 사용할 수도 있음
# 가격 문자열에서 숫자만 추출 (천 단위 구분자를 포함하는 경우 처리)
price_range = re.findall(r'\d+', option_price_text.replace(',', ''))
low_price, high_price = map(int, price_range) if len(price_range) == 2 else (int(price_range[0]), int(price_range[0]))
except Exception:
logger.debug(f"{option_type_number}번 옵션타입의 {i}번째 옵션에 가격 없음")
low_price = high_price = None
option_data.append({
'name': option_name,
'image_url': option_image_url,
'low_price': low_price,
'high_price': high_price
})
logger.debug(f"{option_type_number}번 옵션타입의 옵션 {i}: 이름={option_name}, 이미지 URL={option_image_url}, 가격={low_price} - {high_price}")
except Exception as e:
logger.error(f"옵션명, 이미지 및 가격 수집 중 오류 발생: {e}", exc_info=True)
return option_data
def is_single_option_product(driver, product_info):
"""
웹페이지에서 현재 상품이 '단일 상품'인지 '옵션 상품'인지 확인합니다.
Parameters:
- driver: WebDriver 인스턴스
Returns:
- bool: 옵션 상품이면 True, 단일 상품이면 False
"""
try:
# '옵션 상품등록' 또는 '단일 상품등록' 선택 여부 확인
radio_group_css = "div#productMainContentContainerId div.ant-row.css-1li46mu > div"
radio_group_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, radio_group_css))
)
# 라디오 버튼의 선택 상태 확인
option_radio_xpath = "//label[span[text()='옵션 상품등록']]//input[@type='radio' and @value='false']"
single_radio_xpath = "//label[span[text()='단일 상품등록']]//input[@type='radio' and @value='true']"
option_radio_checked = radio_group_element.find_element(By.XPATH, option_radio_xpath).is_selected()
single_radio_checked = radio_group_element.find_element(By.XPATH, single_radio_xpath).is_selected()
if option_radio_checked and not single_radio_checked:
logger.debug("상품 유형: 옵션 상품등록")
product_info.option_types_onlyone = True
return True
else:
logger.debug("상품 유형: 단일 상품등록")
product_info.option_types_onlyone = False
return False
except Exception as e:
logger.error(f"상품 유형 확인 중 오류 발생: {e}", exc_info=True)
logger.debug("상품 유형 오류 발생으로 기본설정인 단일 상품등록으로 진행합니다.")
product_info.option_types_onlyone = False
return False # 오류 발생 시 기본적으로 단일 상품으로 처리
def filter_and_select_options(driver, option_type_number, option_data):
"""
미끼 옵션을 제거하고 유효한 옵션을 선택합니다.
Parameters:
- driver: WebDriver 인스턴스
- option_type_number: 옵션 타입 번호 (1, 2, 3 등)
- option_data (list): 옵션명, 이미지 URL, 가격을 포함하는 리스트
"""
try:
# 가격 데이터만 추출하여 정렬
prices = sorted([item['low_price'] for item in option_data if item['low_price'] is not None])
if not prices:
logger.error("가격 데이터가 없습니다.")
return
# 미끼 옵션을 식별하여 제거
valid_items = []
for i, base_price in enumerate(prices):
range_limit = base_price * 1.5
within_range = [price for price in prices if price <= range_limit]
if len(within_range) == len(prices):
valid_items = [item for item in option_data if item['low_price'] in within_range]
break
if not valid_items:
valid_items = option_data
# 전체 체크박스 해제
select_all_checkbox_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]"
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, select_all_checkbox_xpath))
).click()
time.sleep(1)
# 필터링된 옵션을 낮은 가격순으로 정렬
valid_items = sorted(valid_items, key=lambda x: x['low_price'])
# 최대 10개 옵션만 선택, 옵션이 5개 이하라면 모든 옵션 선택
for i, item in enumerate(valid_items):
if i >= 10:
break
option_checkbox_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i + 1}]/div/div[1]/div/div[1]/label/span[1]"
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, option_checkbox_xpath))
).click()
time.sleep(0.5)
logger.debug(f"옵션 타입 {option_type_number}의 유효한 옵션 아이템 선택 완료")
except Exception as e:
logger.error(f"옵션 선택 중 오류 발생: {e}", exc_info=True)
def sort_options_by_price(driver, options_info):
"""
옵션 타입별로 가격 내림차순 정렬 버튼을 클릭합니다.
Parameters:
- driver: WebDriver 인스턴스
- options_info: 옵션 타입별 정보를 포함하는 딕셔너리
"""
try:
for option_type_key in options_info:
option_type_number = option_type_key.split('_')[-1] # 옵션 타입 번호 추출
sort_button_css = f"div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2) > div > div:nth-child({option_type_number}) > div > div > div:nth-child(2) > div > div > div:nth-child(4) > div:nth-child(2) > div:nth-child(2) > div > div:nth-child(4) > button"
sort_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, sort_button_css))
)
sort_button.click()
logger.debug(f"옵션 타입 {option_type_number}의 가격 내림차순 정렬 버튼 클릭 완료")
time.sleep(1) # 정렬이 완료될 시간을 줍니다.
# low_price_order_css = "div#productMainContentContainerId div:nth-child(2) > div > button[type=\"button\"]:nth-child(3)"
# # click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js')
# click_element(driver, 'CSS_SELECTOR', low_price_order_css, 10, 'js')
except Exception as e:
logger.error(f"가격 내림차순 정렬 버튼 클릭 중 오류 발생: {e}", exc_info=True)