1
0
Fork 0
This commit is contained in:
Envy_PC 2024-07-27 23:03:14 +09:00
commit dba048c26e
5 changed files with 909 additions and 3 deletions

View File

@ -208,3 +208,35 @@ class ImageDescriptionGenerator:
except Exception as e:
logger.debug(f"예상치 못한 오류 발생: {e}", exc_info=True)
return "예상치 못한 오류 발생: 처리할 수 없습니다."
def generate_option_description(self, product_info, top_k=3):
product_info.option_datas
prompt = f'''
나는 타오바오에서 상품을 가져와서 한국의 온라인 쇼핑몰에서 구매대행업을 하는 사업가야.
너는 온라인 쇼핑몰 상세페이지 제작 전문가야. 니가 만든 상품페이지는 모두를 감동시키고, 구매로 이어지는 마법같은 능력이 있어.
여기 중국어로 상품의 옵션들이 있어.
다음 옵션 목록에서 옵션의 고유한 특징만 추출하여 한국어로 번역하고, 최대 45바이트를 넘지 않도록 간결하게 작성해주세요. 특수 문자는 제외하고 허용되는 특수 문자(!$~()._-=+/) 포함해주세요.
하나씩 차근차근 진행해보자.
[정보]
중국어 옵션 목록은 다음과 같아. '{product_info.option_datas}'
[출력형식]
product_info.option_datas['name']
[
{"options1": "특징1"},
{"options2": "특징2"},
...
]
options_info[f'option_type_{idx}'] = {
'name': option_type_name,
'items_count': option_items_count,
'items': option_items_info
}
'''

View File

@ -19,7 +19,7 @@ from img_trans.image_trans import image_trans
# 로거 인스턴스 가져오기
logger = logging.getLogger('default_logger')
def modify_option_page2(driver, product_info, translator, login_info):
def modify_option_page2(driver, product_info, gemini, translator, login_info):
# 1. 현재 옵션에 대한 정보 수집
options_info = collect_option_data(driver)
@ -86,7 +86,7 @@ def filter_out_bait_items(options_info):
return options_info
def modify_option_page(driver, product_info, translator, login_info):
def modify_option_page(driver, product_info, gemini, translator, login_info):
simpleMode = login_info['whether_simpleMode']
option_css = ".ant-tabs-tab:nth-child(2)"

871
edit/options_trans.py Normal file
View File

@ -0,0 +1,871 @@
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)

View File

@ -32,6 +32,9 @@ class ProductInfo:
self.naver_products = [] # 네이버 파싱된 상품들
self.is_option_product = False
self.option_datas = {}
self.option_1_names = [] # 상품 옵션 이름 리스트
self.trans_option_1_names = [] # 번역된 상품 옵션 이름 리스트
self.trans_option_1_name_common_parts = [] # 번역된 상품 옵션 이름 리스트의 단어별 리스트 저장

View File

@ -197,7 +197,7 @@ def modify_products(driver, gemini, translator, mongo_config, login_info, set_nu
steps_and_actions = [
('tag_modification', edit_tag, (driver, product_infos[i-1])),
('thumbnail_modification', modify_thumb_page, (driver, product_infos[i-1])),
('option_modification', modify_option_page, (driver, product_infos[i-1], translator, login_info)),
('option_modification', modify_option_page, (driver, product_infos[i-1], gemini, translator, login_info)),
('detail_page_modification', modify_detail_page, (driver, product_infos[i-1], gemini, translator, delv_collection, json_naver_codes, login_info)),
('price_modification', modify_price_page, (driver, product_infos[i-1])),
('title_modification', modify_product_title, (driver, product_infos[i-1], login_info)),