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 # 로거 인스턴스 가져오기 logger = logging.getLogger('default_logger') def modify_option_page(driver, product_info, 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}") # 현재 전체 옵션갯수 가져오기 # 각 옵션 타입별로 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" click_element(driver, 'XPATH', low_price_order_xpath, 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 = [] # 원본 옵션명 추출 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" 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) 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)