Compare commits

..

No commits in common. "master" and "v1.0.7" have entirely different histories.

22 changed files with 398 additions and 1876 deletions

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ Scripts/
build/ build/
dist/ dist/
Include/ Include/
imgtrans_test/
__pycache__/ __pycache__/
*.log *.log
*.log.* *.log.*

View File

@ -56,7 +56,7 @@ exe = EXE(
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=True, console=False,
onefile=False, onefile=False,
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,

View File

@ -60,7 +60,7 @@ class ImageDescriptionGenerator:
너는 온라인 쇼핑몰 상세페이지 제작 전문가야. 니가 만든 상품페이지는 모두를 감동시키고, 구매로 이어지는 마법같은 능력이 있어. 너는 온라인 쇼핑몰 상세페이지 제작 전문가야. 니가 만든 상품페이지는 모두를 감동시키고, 구매로 이어지는 마법같은 능력이 있어.
이제 주어진 이미지와 내가 원하는 상세페이지에 대한 정보를 줄께. 이제 주어진 이미지와 내가 원하는 상세페이지에 대한 정보를 줄께.
하나씩 차근차근 진행해보자. 하나씩 차근차근 진행해보자.
질문에 대한 답변을 기존의 html에 추가할수 있는 형태로 (HTML 태그로 이루어진) 만들어줘. HTML편집기에 바로 추가하여 사용할 있게 말이야. 문단기호, 구분기호등으로 분리해서 답변해줘. 질문에 대한 답변을 마크다운 편집기에 바로 사용할 있게 문단기호, 구분기호등으로 분리해서 답변해줘.
[정보] [정보]
A. 해당상품의 타오바오 상품명은 '{product_info.tao_title}' A. 해당상품의 타오바오 상품명은 '{product_info.tao_title}'
B. 해당상품의 키워드는 '{product_info.keyword_title}' B. 해당상품의 키워드는 '{product_info.keyword_title}'
@ -85,27 +85,27 @@ class ImageDescriptionGenerator:
- 상품명 제작 권장 사항 3 : 경쟁사의 상품명들을 띄어쓰기를 중심으로 단어를 분리하고, 중복된 단어를 모두 제거한 나머지 단어들 4개를 가져와서 상품명 제작에 사용해줘. - 상품명 제작 권장 사항 3 : 경쟁사의 상품명들을 띄어쓰기를 중심으로 단어를 분리하고, 중복된 단어를 모두 제거한 나머지 단어들 4개를 가져와서 상품명 제작에 사용해줘.
- 반드시 준시해야 상품명 제한 사항 : 형용사 사용 금지, 중복단어 절대 사용 금지, 쉼표나 별표를 포함한 모든 특수문자 금지. - 반드시 준시해야 상품명 제한 사항 : 형용사 사용 금지, 중복단어 절대 사용 금지, 쉼표나 별표를 포함한 모든 특수문자 금지.
- 상품명 형식 : 한글의 글자수로 35 이상 되어야 . - 상품명 형식 : 한글의 글자수로 35 이상 되어야 .
답변은 html 형식으로, 폰트형식은 css로 설정해줘. '폰트크기 24, 폰트 스타일 굵게' 해줘 답변은 마크다운 형식으로 '폰트크기 24, 폰트 스타일 굵게' 해줘
[상품의 용도] [상품의 용도]
상품이 무엇인지, 어떤 용도로 사용하는지 자세하게 설명해줘. 상품이 무엇인지, 어떤 용도로 사용하는지 자세하게 설명해줘.
답변은 html 형식으로, 폰트형식은 css로 설정해줘. 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘 답변은 마크다운 형식으로 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘
[상품의 무게] [상품의 무게]
상품의 배송비 산정을 위해 무게를 kg으로 알려줘. 상품의 배송비 산정을 위해 무게를 kg으로 알려줘.
답변은 html 형식으로, 폰트형식은 css로 설정해줘. 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘 답변은 마크다운 형식으로 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘
[상품의 강점과 특징] [상품의 강점과 특징]
상품의 강점, 특징과 함께 이상품을 사야하는지, 다른제품들과의 차별점은 무엇인지, 기존제품의 어떠한 단점을 개선했는지를 중심으로 홍보문구를 상품이미지에 맞게 자세하고 친절하게 만들어줘." 상품의 강점, 특징과 함께 이상품을 사야하는지, 다른제품들과의 차별점은 무엇인지, 기존제품의 어떠한 단점을 개선했는지를 중심으로 홍보문구를 상품이미지에 맞게 자세하고 친절하게 만들어줘."
답변은 html 형식으로, 폰트형식은 css로 설정해줘. 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘 답변은 마크다운 형식으로 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘
[사용상 주의점] [사용상 주의점]
상품의 사용상 주의점을 자세히 알려줘 상품의 사용상 주의점을 자세히 알려줘
답변은 html 형식으로, 폰트형식은 css로 설정해줘. 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘 답변은 마크다운 형식으로 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘
[상품의 추가 정보] [상품의 추가 정보]
상품의 도움이 될만한 다른 추가정보를 자세하게 알려줘 상품의 도움이 될만한 다른 추가정보를 자세하게 알려줘
답변은 html 형식으로, 폰트형식은 css로 설정해줘. 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘 답변은 마크다운 형식으로 제목을 '폰트크기 20, 폰트 스타일 굵게', 내용은 '폰트크기 16, 들여쓰기'로해줘
''' '''
@ -187,9 +187,9 @@ class ImageDescriptionGenerator:
# 후보 결과 확인 및 처리 # 후보 결과 확인 및 처리
if response.candidates and len(response.candidates) > 0: if response.candidates and len(response.candidates) > 0:
logger.debug(f"response.text : {response.text}") logger.debug(f"response.text : {response.text}")
# response_html = markdown.markdown(response.text) response_html = markdown.markdown(response.text)
# logger.debug(f"response_html : {response_html}") logger.debug(f"response_html : {response_html}")
return response.text return response_html
else: else:
# 후보 결과가 없거나 유효하지 않을 경우 기본 응답 반환 # 후보 결과가 없거나 유효하지 않을 경우 기본 응답 반환
@ -208,35 +208,3 @@ class ImageDescriptionGenerator:
except Exception as e: except Exception as e:
logger.debug(f"예상치 못한 오류 발생: {e}", exc_info=True) logger.debug(f"예상치 못한 오류 발생: {e}", exc_info=True)
return "예상치 못한 오류 발생: 처리할 수 없습니다." 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

@ -1,5 +1,3 @@
from tkinter import EXCEPTION
from bson import RE_TYPE
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
@ -24,76 +22,6 @@ logger = logging.getLogger('default_logger')
avg_price = 0 avg_price = 0
def detail_ai(driver, textarea, aicontents, combined_value, product_info, isTransSuccess):
try:
logger.debug("detail_ai 맞추기")
aicontent_html = aicontents
time.sleep(0.5)
current_value = product_info.current_value
# current_value = textarea.get_attribute("data-value")
# logger.debug("current_value에서 기존 이미지 삭제")
# deleted_original_html_tags = original_html(current_value)
if isTransSuccess:
logger.debug(f"isTransSuccess : {isTransSuccess} 이므로 기존 이미지 삭제")
modified_html_tags = product_info.current_value
# modified_html_tags = remove_resized_content(current_value)
else:
modified_html_tags = product_info.deleted_value
logger.debug(f"isTransSuccess : {isTransSuccess} 이므로 기존 이미지 유지")
ai_value = combined_value + aicontent_html + modified_html_tags
# ai_value = combined_value + aicontent_html + current_value
# contents 변수의 값이 None이 아닐 때만 send_keys 메서드를 호출합니다.
if aicontent_html is not None:
# logger.debug("aicontent_html 입력시작")
# html_insert_btn_xpath="//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div/div[1]/div[2]/div/div/button[7]"
# click_element(driver, 'XPATH', html_insert_btn_xpath, 5, 'ac')
# logger.debug("html_insert_btn_xpath 클릭")
# html_insert_TEXTAREA_xpath="/html/body/div[7]/div/div[3]/div/div[2]/div[1]/div/div/div[2]/div[2]/div[2]/div/div[2]/div[2]/textarea"
# click_element(driver, 'XPATH', html_insert_TEXTAREA_xpath, 5, 'ac')
# html_insert_TEXTAREA_element = return_element(driver, 'XPATH', 'html_insert_TEXTAREA_xpath', 10)
# html_insert_TEXTAREA_element.send_keys(aicontent_html)
# logger.debug("html 요소로 전송")
# html_insert_ok_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div[2]/div[2]/div/div[1]/div[2]/div/button[1]"
# click_element(driver, 'XPATH', html_insert_ok_xpath, 5, 'ac')
# logger.debug("html_insert_ok_xpath 클릭")
# logger.debug(f"AI 컨텐츠 입력완료")
driver.execute_script("arguments[0].setAttribute('data-value', arguments[1]);", textarea, ai_value)
logger.debug(f"AI 컨텐츠 입력완료")
else:
# contents 변수가 None일 때의 대체 처리
# 예: detail_content.send_keys("") 또는 아무 동작도 수행하지 않음
pass
except Exception as e:
logger.debug(f"AI 컨텐츠 수정 중 에러발생 : {e}", exc_info=True)
def detail_html(driver):
try:
logger.debug("HTML 수정 버튼 클릭 .")
html_btn_by_contains_xpath="//button[contains(.,'소스')]"
html_btn_css = "ck.ck-button.ck-source-editing-button.ck-off.ck-button_with-text"
html_btn_xpath="//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div/div/div[2]/div/div/button[8]"
click_element(driver, 'XPATH', html_btn_xpath, 5, 'ac')
# html_btn = WebDriverWait(driver, 10).until(
# # EC.presence_of_element_located((By.CSS_SELECTOR, ".ck-source-editing-button"))
# EC.presence_of_element_located(By.CLASS_NAME, html_btn_xpath)
# # EC.presence_of_element_located((By.CSS_SELECTOR, "button.ck.ck-button.ck-source-editing-button.ck-off.ck-button_with-text"))
# )
# logger.debug("HTML 수정 버튼을 성공적으로 찾았습니다.")
# html_btn.click()
except Exception as e:
logger.debug(f"HTML 수정 버튼 요소를 찾을 수 없습니다. : {e}", exc_info=True)
def detail_textarea(driver): def detail_textarea(driver):
try: try:
# textarea_css = "ck-source-editing-area" # textarea_css = "ck-source-editing-area"
@ -329,58 +257,6 @@ def find_delivery_fee(weight, delv_collection):
# product_info_text += f'네이버 상품의 최고 가격은 [{high_price}]' # product_info_text += f'네이버 상품의 최고 가격은 [{high_price}]'
# return product_info_text # return product_info_text
def remove_all_image_tags(current_html):
# BeautifulSoup 객체 생성
soup = BeautifulSoup(current_html, 'html.parser')
# 모든 img, figure, picture 태그를 찾아 제거
image_tags = soup.find_all(['img', 'figure', 'picture'])
for tag in image_tags:
tag.decompose() # 태그 자체를 문서에서 제거
# 수정된 HTML을 문자열로 변환하여 반환
deleted_html = str(soup)
return deleted_html
def remove_resized_content_ori(current_html):
# BeautifulSoup 객체 생성
soup = BeautifulSoup(current_html, 'html.parser')
# "img class='image_resized'" 태그와 "figure class='image_resized'" 태그 모두 추출
resized_elements = soup.find_all(['img', 'figure'], class_='image_resized')
# 각 요소의 src 속성을 빈 문자열로 설정하거나 요소 자체를 삭제
for element in resized_elements:
if element.name == 'img':
element['src'] = "" # 이미지 src를 비워 렌더링되지 않게 함
elif element.name == 'figure':
element.decompose() # figure 태그 자체를 문서에서 제거
# 수정된 HTML을 문자열로 변환하여 반환
modified_html = str(soup)
return modified_html
def remove_resized_content(current_html):
# BeautifulSoup 객체 생성
soup = BeautifulSoup(current_html, 'html.parser')
# "img" 태그 중 src 속성이 'https://img.alicdn.com'로 시작하는 것을 찾아 모두 삭제
images_to_remove = soup.find_all('img', src=lambda x: x and x.startswith('https://img.alicdn.com'))
images_to_remove_percenty = soup.find_all('img', src=lambda x: x and x.startswith('https://file.percenty.co.kr'))
logger.debug(f"https://img.alicdn.com 의갯수 : [{len(images_to_remove)}] 개")
logger.debug(f"https://file.percenty.co.kr 의갯수 : [{len(images_to_remove_percenty)}] 개")
for img in images_to_remove:
img.decompose() # img 태그 자체를 문서에서 제거
# 수정된 HTML을 문자열로 변환하여 반환
modified_html = str(soup)
logger.debug("기존이미지 삭제 완료")
return modified_html
def original_html(current_html): def original_html(current_html):
# BeautifulSoup 객체 생성 # BeautifulSoup 객체 생성
@ -524,32 +400,32 @@ def modify_detail_page(driver, product_info, gemini, translator, delv_collection
naver_code = "" naver_code = ""
# if not simpleMode: # 심플모드일 경우 네이버 파싱 패스 if not simpleMode: # 심플모드일 경우 네이버 파싱 패스
try: try:
logger.debug("네이버쇼핑 파싱 중") logger.debug("네이버쇼핑 파싱 중")
products = parse_naver_shopping(product_keyword, naver_code, isOverseas=1, sortcount=5) products = parse_naver_shopping(product_keyword, naver_code, isOverseas=1, sortcount=5)
logger.debug("네이버쇼핑 파싱 완료") logger.debug("네이버쇼핑 파싱 완료")
# logger.debug(f"네이버 파싱된 상품 리스트 \n {products}") # logger.debug(f"네이버 파싱된 상품 리스트 \n {products}")
product_info.naver_products = products product_info.naver_products = products
# 네이버 상품가격 담기 # 네이버 상품가격 담기
naver_price = naver_prices(products) naver_price = naver_prices(products)
if naver_price[0]: # 리턴값이 있는 경우 사용 if naver_price[0]: # 리턴값이 있는 경우 사용
product_info.naver_low_price = naver_price[0] product_info.naver_low_price = naver_price[0]
product_info.naver_avg_price = naver_price[1] product_info.naver_avg_price = naver_price[1]
product_info.naver_high_price = naver_price[2] product_info.naver_high_price = naver_price[2]
else: # 리턴값이 None인 경우 기본값인 100원 사용 else: # 리턴값이 None인 경우 기본값인 100원 사용
product_info.naver_low_price = 100 product_info.naver_low_price = 100
product_info.naver_avg_price = 100 product_info.naver_avg_price = 100
product_info.naver_high_price = 100 product_info.naver_high_price = 100
# product_info_text = NS_info(products) # product_info_text = NS_info(products)
product_info_card = NS_info_with_HTML(products) product_info_card = NS_info_with_HTML(products)
# logger.debug(f"수집된 정보 \n {product_info_card} \n") # logger.debug(f"수집된 정보 \n {product_info_card} \n")
logger.debug("네이버쇼핑 파싱 완료") logger.debug("네이버쇼핑 파싱 완료")
except Exception as e: except Exception as e:
logger.debug(f"네이버쇼핑 파싱 중 에러발생 : {e}", exc_info=True) logger.debug(f"네이버쇼핑 파싱 중 에러발생 : {e}", exc_info=True)
@ -587,76 +463,70 @@ def modify_detail_page(driver, product_info, gemini, translator, delv_collection
# detail_content = driver.find_element(By.XPATH, "//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div[2]/div") # detail_content = driver.find_element(By.XPATH, "//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div[2]/div")
# detail_content = driver.find_element(By.ID, "rc-tabs-0-tab-5") # detail_content = driver.find_element(By.ID, "rc-tabs-0-tab-5")
# if not simpleMode: # 심플모드일 경우 AI처리 하지않음 # if not simpleMode: # 심플모드일 경우 AI처리 하지않음
try: try:
ActionChains(driver).move_to_element(detail_content).click().perform() ActionChains(driver).move_to_element(detail_content).click().perform()
time.sleep(1) time.sleep(1)
logger.debug("AI 이미지 처리중") logger.debug("AI 이미지 처리중")
# contents = bard_img(image_src) # contents = bard_img(image_src)
# contents = gemini.generate_description(image_src, product_title) # contents = gemini.generate_description(image_src, product_title)
aicontents = [] aicontents = safe_generate_content(gemini, product_info)
aicontents = safe_generate_content(gemini, product_info) logger.debug(f"{aicontents}")
logger.debug(f"{aicontents}") product_info.ai_contents = aicontents
product_info.ai_contents_html = aicontents logger.debug("AI 이미지 처리 완료")
logger.debug("AI 이미지 처리 완료")
weight = extract_weight(aicontents) weight = extract_weight(aicontents)
logger.debug(f"무게 추정 : {weight}") logger.debug(f"무게 추정 : {weight}")
product_info.weight = weight product_info.weight = weight
delv_fee = find_delivery_fee(weight, delv_collection) delv_fee = find_delivery_fee(weight, delv_collection)
logger.debug(f"무게배송비 추정 : {delv_fee}") logger.debug(f"무게배송비 추정 : {delv_fee}")
product_info.w_delv_fee = delv_fee product_info.w_delv_fee = delv_fee
packing_fee = 0 # 추가포장비의 경우 크기와 소재에 따라 래핑, 우드포장등 세분화 필요. 기본은 0 packing_fee = 0 # 추가포장비의 경우 크기와 소재에 따라 래핑, 우드포장등 세분화 필요. 기본은 0
logger.debug(f"추가포장비 추정 : {packing_fee}") logger.debug(f"추가포장비 추정 : {packing_fee}")
product_info.packing_fee = packing_fee product_info.packing_fee = packing_fee
except Exception as e: except Exception as e:
logger.debug(f"상세페이지 편집 중 에러발생 : {e}", exc_info=True) logger.debug(f"상세페이지 편집 중 에러발생 : {e}", exc_info=True)
# if not simpleMode: # 심플모드일 경우 가격계산 패스 # if not simpleMode: # 심플모드일 경우 가격계산 패스
try: try:
# 가격 정보 계산 # 가격 정보 계산
low_cost, low_margin = selling_price(product_low_cost, delv_fee, 0.29) low_cost, low_margin = selling_price(product_low_cost, delv_fee, 0.29)
high_cost, high_margin = selling_price(product_high_cost, delv_fee, 0.29) high_cost, high_margin = selling_price(product_high_cost, delv_fee, 0.29)
low_delv_fee = avg_price - low_cost low_delv_fee = avg_price - low_cost
high_delv_fee = avg_price - high_cost high_delv_fee = avg_price - high_cost
# 마진율 계산 # 마진율 계산
low_margin_r = round((low_margin / low_cost) * 100, 2) low_margin_r = round((low_margin / low_cost) * 100, 2)
high_margin_r = round((high_margin / high_cost) * 100, 2) high_margin_r = round((high_margin / high_cost) * 100, 2)
# 텍스트 조합 # 텍스트 조합
weight_text = f''' cost_add_text = f"""
해당 제품의 무게배송비는 <span style="font-size:16px;"><strong><u>{delv_fee:,.0f}</u></strong></span>원입니다.<br> <h1>==== 가격과 마진율 ====</h1><br>
해당 제품의 네이버 평균가격은 <span style="font-size:16px;"><strong><u>{avg_price:,.0f}</u></strong></span>원입니다.<br>
해당 제품의 무게배송비는 <span style="font-size:16px;"><strong><u>{delv_fee:,.0f}</u></strong></span>원입니다.<br>
''' **마진율 확보를 위한 판매가 범위:** {low_cost:,.0f} ~ {high_cost:,.0f}<br>
cost_add_text = f"""
<h1>==== 가격과 마진율 ====</h1><br>
해당 제품의 네이버 평균가격은 <span style="font-size:16px;"><strong><u>{avg_price:,.0f}</u></strong></span>원입니다.<br>
해당 제품의 무게배송비는 <span style="font-size:16px;"><strong><u>{delv_fee:,.0f}</u></strong></span>원입니다.<br>
**마진율 확보를 위한 판매가 범위:** {low_cost:,.0f} ~ {high_cost:,.0f}<br> <h3>**저가 중심 계산:**</h3><br>
* 무게 배송비 대비: {low_delv_fee:,.0f} {('낮은' if low_delv_fee < 0 else '높은')} <br>
* 마진율: {low_margin_r:,.2f}%<br>
<h3>**고가중심 계산:**</h3><br>
* 무게 배송비 대비: {high_delv_fee:,.0f} {('낮은' if high_delv_fee < 0 else '높은')} <br>
* 마진율: {high_margin_r:,.2f}%<br>
<h3>**저가 중심 계산:**</h3><br> <h3>**추가 정보:**</h3><br>
* 무게 배송비 대비: {low_delv_fee:,.0f} {('낮은' if low_delv_fee < 0 else '높은')} <br> <h2>* 해당 제품의 파손 여부 무게 오차를 고려하여 더하기 마진을 설정하세요.</h2><br>
* 마진율: {low_margin_r:,.2f}%<br> <h3>=======================================</h3><br><br>
<h3>**고가중심 계산:**</h3><br> """
* 무게 배송비 대비: {high_delv_fee:,.0f} {('낮은' if high_delv_fee < 0 else '높은')} <br>
* 마진율: {high_margin_r:,.2f}%<br>
<h3>**추가 정보:**</h3><br> # time.sleep(2)
<h2>* 해당 제품의 파손 여부 무게 오차를 고려하여 더하기 마진을 설정하세요.</h2><br> # detail_content.send_keys(cost_add_text)
<h3>=======================================</h3><br><br>
"""
# time.sleep(2) except Exception as e:
# detail_content.send_keys(cost_add_text) logger.debug(f"가격정보 생성 중 에러 : {e}", exc_info=True)
except Exception as e:
logger.debug(f"가격정보 생성 중 에러 : {e}", exc_info=True)
weight_text = ""
cost_add_text = ""
try: try:
logger.debug("HTML 수정 버튼 클릭 .") logger.debug("HTML 수정 버튼 클릭 .")
@ -677,6 +547,7 @@ def modify_detail_page(driver, product_info, gemini, translator, delv_collection
# html_btn = driver.find_element(By.CSS_SELECTOR, ".ck-source-editing-button") # html_btn = driver.find_element(By.CSS_SELECTOR, ".ck-source-editing-button")
try: try:
# textarea_css = "ck-source-editing-area" # textarea_css = "ck-source-editing-area"
# click_element(driver, 'CSS_SELECTOR', textarea_css, 10, 'js') # click_element(driver, 'CSS_SELECTOR', textarea_css, 10, 'js')
@ -700,35 +571,26 @@ def modify_detail_page(driver, product_info, gemini, translator, delv_collection
# # data-value 속성 값을 가져옵니다. # # data-value 속성 값을 가져옵니다.
current_value = textarea.get_attribute("data-value") current_value = textarea.get_attribute("data-value")
product_info.current_value = current_value product_info.current_value = current_value
logger.debug("현재 속성값 수집") logger.debug("현재 속성값 수집 완료")
# original_html_tags = original_html(current_value) # original_html_tags = original_html(current_value)
detail_images = fetch_image_urls(current_value) detail_images = fetch_image_urls(current_value)
product_info.detail_image_urls = detail_images product_info.detail_image_urls = detail_images
logger.debug("현재 속성값에서 이미지 URL 추출")
logger.debug(f"원본 이미지 URLs [{len(detail_images)}]개 수집 완료") logger.debug(f"원본 이미지 URLs [{len(detail_images)}]개 수집 완료")
deleted_value = remove_all_image_tags(current_value)
product_info.deleted_value = deleted_value
logger.debug("현재 속성값에서 이미지 제거")
# logger.debug(f"detail_images List : {detail_images}") # logger.debug(f"detail_images List : {detail_images}")
# logger.debug(f"현재 속성값 :{current_value}") # logger.debug(f"현재 속성값 :{current_value}")
# if not simpleMode: # 심플모드일 경우 상품카드 작성하지 않음 if not simpleMode: # 심플모드일 경우 상품카드 작성하지 않음
logger.debug("product_info_card 결합.")
info_value = product_info_card + '<br><br>' + cost_add_text + '<br><br>'
# textarea.send_keys(product_info_card)
time.sleep(0.2)
combined_value = info_value
logger.debug("product_info_card 결합.") logger.debug(f"{combined_value}")
# info_value = product_info_card + '<br><br>' + cost_add_text + '<br><br>'
if not weight_text:
weight_text =""
combined_value = product_info_card + '<br><br>' + weight_text + '<br><br>'
# textarea.send_keys(product_info_card)
time.sleep(0.2)
logger.debug(f"{combined_value}") # # data-value 속성을 새로운 값으로 설정합니다.
logger.debug("product_info_card + 기존 내용 입력.")
# # data-value 속성을 새로운 값으로 설정합니다. driver.execute_script("arguments[0].setAttribute('data-value', arguments[1]);", textarea, combined_value)
logger.debug("product_info_card + 기존 내용 입력.")
# driver.execute_script("arguments[0].setAttribute('data-value', arguments[1]);", textarea, combined_value)
logger.debug("HTML 수정 버튼 다시 클릭하여 기본편집페이지로 돌아감.") logger.debug("HTML 수정 버튼 다시 클릭하여 기본편집페이지로 돌아감.")
click_element(driver, 'XPATH', html_btn_xpath, 5, 'ac') click_element(driver, 'XPATH', html_btn_xpath, 5, 'ac')
@ -737,114 +599,84 @@ def modify_detail_page(driver, product_info, gemini, translator, delv_collection
except Exception as e: except Exception as e:
logger.debug(f"product_info_card 수정 중 에러발생 : {e}", exc_info=True) logger.debug(f"product_info_card 수정 중 에러발생 : {e}", exc_info=True)
# try: try:
# # 상세페이지 내용에 텍스트 추가 # 상세페이지 내용에 텍스트 추가
# logger.debug("커서위치 맞추기") logger.debug("커서위치 맞추기")
# detail_content.send_keys(Keys.LEFT) detail_content.send_keys(Keys.LEFT)
# detail_content.send_keys(Keys.HOME) detail_content.send_keys(Keys.HOME)
# detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER)
# detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER)
# detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.UP)
# detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.UP)
# detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.UP)
# detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.UP)
# detail_content.send_keys(Keys.HOME) detail_content.send_keys(Keys.HOME)
# detail_content.send_keys(Keys.HOME) detail_content.send_keys(Keys.HOME)
# time.sleep(0.5) time.sleep(0.5)
# # contents 변수의 값이 None이 아닐 때만 send_keys 메서드를 호출합니다. # contents 변수의 값이 None이 아닐 때만 send_keys 메서드를 호출합니다.
# if aicontents is not None: if aicontents is not None:
# logger.debug("aicontents 입력시작") html_insert_btn_xpath="//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div/div/div[2]/div/div/button[7]"
click_element(driver, 'XPATH', html_insert_btn_xpath, 5, 'ac')
# html_insert_btn_xpath="//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div/div[1]/div[2]/div/div/button[7]" detail_content.send_keys(aicontents)
# click_element(driver, 'XPATH', html_insert_btn_xpath, 5, 'ac') html_insert_ok_xpath="/html/body/div[6]/div/div[3]/div/div[2]/div[1]/div/div/div[2]/div[2]/div[2]/div/div[1]/div[2]/div/button[1]"
# logger.debug("html_insert_btn_xpath 클릭") click_element(driver, 'XPATH', html_insert_ok_xpath, 5, 'ac')
# html_insert_TEXTAREA_xpath="/html/body/div[7]/div/div[3]/div/div[2]/div[1]/div/div/div[2]/div[2]/div[2]/div/div[2]/div[2]/textarea" logger.debug(f"AI 컨텐츠 입력완료")
# click_element(driver, 'XPATH', html_insert_TEXTAREA_xpath, 5, 'ac') detail_content.send_keys(Keys.DOWN)
# html_insert_TEXTAREA_element = return_element(driver, 'XPATH', 'html_insert_TEXTAREA_xpath', 10) else:
# html_insert_TEXTAREA_element.send_keys(aicontents) # contents 변수가 None일 때의 대체 처리
# logger.debug("html 요소로 전송") # 예: detail_content.send_keys("") 또는 아무 동작도 수행하지 않음
# html_insert_ok_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div[2]/div[2]/div/div[1]/div[2]/div/button[1]" pass
# click_element(driver, 'XPATH', html_insert_ok_xpath, 5, 'ac') except Exception as e:
# logger.debug("html_insert_ok_xpath 클릭") logger.debug(f"AI 컨텐츠 수정 중 에러발생 : {e}", exc_info=True)
# logger.debug(f"AI 컨텐츠 입력완료")
# detail_content.send_keys(Keys.DOWN)
# else:
# # contents 변수가 None일 때의 대체 처리
# # 예: detail_content.send_keys("") 또는 아무 동작도 수행하지 않음
# pass
# except Exception as e:
# logger.debug(f"AI 컨텐츠 수정 중 에러발생 : {e}", exc_info=True)
trans_img_tag = login_info["whether_modifyImageTanslation"] trans_img_tag = login_info["whether_modifyImageTanslation"]
logger.debug(f"whether_modifyImageTanslation : {trans_img_tag}") logger.debug(f"whether_modifyImageTanslation : {trans_img_tag}")
isTransSuccess = False # 이미지 번역 성공여부
if trans_img_tag: if trans_img_tag:
logger.debug("상세페이지 이미지 번역 시작") logger.debug("상세페이지 이미지 번역 시작")
for i in range(30): for i in range(20):
detail_content.send_keys(Keys.PAGE_DOWN) detail_content.send_keys(Keys.PAGE_DOWN)
detail_content.send_keys(Keys.DOWN)
logger.debug("이미지 번역을 위해 페이지 최하단 이동")
# detail_content.send_keys(Keys.HOME) detail_content.send_keys(Keys.HOME)
detail_content.send_keys(Keys.DOWN) detail_content.send_keys(Keys.DOWN)
detail_content.send_keys(Keys.DOWN) detail_content.send_keys(Keys.DOWN)
detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER)
detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER)
detail_content.send_keys(Keys.ENTER)
detail_content.send_keys(Keys.ENTER)
try: try:
for i, detail_image in enumerate(detail_images): for i, detail_image in enumerate(detail_images):
logger.debug(f"상세페이지 [{i+1}]/[{len(detail_images)}]번째 이미지 번역 시작") logger.debug(f"상세페이지 {i+1}번째 이미지 번역 시작")
logger.debug(f"이미지 타입 : {type(detail_image)}") logger.debug(f"이미지 타입 : {type(detail_image)}")
returned_img = image_trans(detail_image, translator, 'translate', logger) returned_img = image_trans(detail_image, translator, 'translate')
if returned_img: image_to_clipboard(returned_img)
image_to_clipboard(returned_img) logger.debug("번역 완료 및 부텨넣기")
logger.debug("번역 완료 및 부텨넣기") detail_content.send_keys(Keys.CONTROL, 'v')
detail_content.send_keys(Keys.CONTROL, 'v') detail_content.send_keys(Keys.ENTER)
detail_content.send_keys(Keys.ENTER)
logger.debug(f"{i+1}번째 이미지 붙여넣기 완료") logger.debug(f"{i+1}번째 이미지 붙여넣기 완료")
else:
logger.warning(f"[{i+1}]/[{len(detail_images)}]번째 이미지 실패")
# translated_image_urls.append(returned_img_path) # translated_image_urls.append(returned_img_path)
logger.debug("이미지 번역 완료") logger.debug("이미지 번역 완료")
detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER)
time.sleep(0.5) time.sleep(0.5)
isTransSuccess = True
except Exception as e: except Exception as e:
logger.error(f"이미지 번역 중 에러발생 : {e}", exc_info=True) logger.error(f"이미지 번역 중 에러발생 : {e}", exc_info=True)
isTransSuccess = False
finally: finally:
num_detail_images = len(detail_image)
logger.debug(f"====기존 이미지 삭제=====\n DEL키 [{num_detail_images}]번 보내기")
# DELETE 키를 50번 보내기
for _ in range(num_detail_images + 5):
detail_content.send_keys(Keys.DELETE)
logger.debug("====번역이미지 붙여넣기 완료=====") logger.debug("====번역이미지 붙여넣기 완료=====")
detail_html(driver)
time.sleep(0.5)
textarea = detail_textarea(driver)
time.sleep(0.5)
detail_ai(driver, textarea, aicontents, combined_value, product_info ,isTransSuccess)
time.sleep(0.5)
detail_html(driver)
time.sleep(0.5)
detail_content.send_keys(Keys.ENTER)
# try:
# num_detail_images = len(detail_images)
# logger.debug(f"====기존 이미지 삭제=====\n DEL키 [{num_detail_images}]번 보내기")
# # DELETE 키를 50번 보내기
# for _ in range(num_detail_images):
# detail_content.send_keys(Keys.DELETE)
# except Exception as e:
# logger.error(f"기존 이미지 삭제 중 에러발생 : {e}", exc_info=True)
logger.debug("상세페이지 편집 저장") logger.debug("상세페이지 편집 저장")
click_element(driver, "XPATH", save_button_xpath, 5, 'js') click_element(driver, "XPATH", save_button_xpath, 5, 'js')

View File

@ -12,81 +12,10 @@ from ai.deepl_with_playwright import trans_text, trans_list_text
import re import re
import logging import logging
import tempfile
from img_trans.image_trans import image_trans
# 로거 인스턴스 가져오기 # 로거 인스턴스 가져오기
logger = logging.getLogger('default_logger') logger = logging.getLogger('default_logger')
def modify_option_page2(driver, product_info, gemini, translator, login_info): def modify_option_page(driver, product_info, translator, login_info):
# 1. 현재 옵션에 대한 정보 수집
options_info = collect_option_data(driver)
# 2. 옵션타입이 단일옵션상품이 아닌지 체크
if not is_single_option_product(driver):
# 3. 각 옵션타입별로 낮은가격순으로 정렬
sort_options_by_price(driver, options_info)
# 4. 미끼상품 체크 및 제외
options_info = filter_out_bait_items(options_info)
# 5. 각 옵션타입별로 옵션아이템 가격에 따라 상품 선택
select_valid_options(driver, options_info, login_info['whether_simpleMode'])
# 6. 선택된 옵션들의 원본 옵션명과 옵션이미지 수집
option_names, option_images = collect_option_names_and_images(driver, options_info)
# 7. 수집된 원본 옵션명은 AI 처리 메서드를 통해 번역 및 편집 후 입력
update_option_names(driver, option_names, translator)
# 8. 수집된 옵션 이미지 URL은 이미지 번역 및 업로드 과정 수행
update_option_images(driver, option_images, translator)
# 기타 작업이 필요한 경우 추가
finalize_modifications(driver, options_info)
# 저장 작업 등
save_changes(driver)
def sort_options_by_price(driver, options_info):
for idx in range(1, len(options_info) + 1):
try:
# 옵션 타입별로 낮은가격순 정렬
sort_button_xpath = f"//div[{idx}]/div/div/div[2]/div/div/div[4]/div[2]/div[2]/div/div[4]/button"
click_element(driver, 'XPATH', sort_button_xpath, 10, 'js')
logger.debug(f"옵션 타입 {idx} 낮은 가격순 정렬 완료")
except Exception as e:
logger.error(f"옵션 타입 {idx} 낮은 가격순 정렬 중 오류 발생: {e}", exc_info=True)
def filter_out_bait_items(options_info):
for option_type_key, option_data in options_info.items():
item_prices = []
for item in option_data['option_elements']:
price_css = "div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > span > sup"
price_element = item.find_element(By.CSS_SELECTOR, price_css)
price_text = price_element.get_attribute('title').replace(',', '').replace('', '').strip()
if '-' in price_text:
min_price, max_price = map(int, price_text.split('-'))
else:
min_price = max_price = int(price_text)
item_prices.append((min_price, max_price))
# 미끼 상품 필터링 로직 적용
valid_items = []
base_prices = sorted([price[0] for price in item_prices])
for base_price in base_prices:
threshold = base_price * 1.5
valid_items = [price for price in item_prices if price[0] <= threshold]
if len(valid_items) > 1:
break
options_info[option_type_key]['valid_items'] = valid_items
return options_info
def modify_option_page(driver, product_info, gemini, translator, login_info):
simpleMode = login_info['whether_simpleMode'] simpleMode = login_info['whether_simpleMode']
option_css = ".ant-tabs-tab:nth-child(2)" option_css = ".ant-tabs-tab:nth-child(2)"
@ -119,48 +48,41 @@ def modify_option_page(driver, product_info, gemini, translator, login_info):
logger.debug("옵션타입 체크") logger.debug("옵션타입 체크")
# # 옵션 타입 처리 # 옵션 타입 처리
# for i in range(1, 4): # 옵션 타입이 1부터 3까지 존재한다고 가정합니다. 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_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) option_type = return_element(driver, 'XPATH', option_type_xpath, 3)
# if option_type: if option_type:
# option_type_value = option_type.get_attribute('value') option_type_value = option_type.get_attribute('value')
# options_info[f'option_type_{i}'] = {'options_name': option_type_value, 'options_count': 0, 'price_ranges': []} options_info[f'option_type_{i}'] = {'options_name': option_type_value, 'options_count': 0, 'price_ranges': []}
# logger.debug(f"옵션 타입 추가: {option_type_value}") logger.debug(f"옵션 타입 추가: {option_type_value}")
# else: else:
# logger.debug(f"{option_type_xpath} 요소를 찾을 수 없음.") logger.debug(f"{option_type_xpath} 요소를 찾을 수 없음.")
# option_type_nums = sum(1 for key in options_info.keys() if key.startswith('option_type_')) option_type_nums = sum(1 for key in options_info.keys() if key.startswith('option_type_'))
# logger.debug(f"총 옵션 타입 수: {option_type_nums}") 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를 문자열 포맷을 사용하여 한 줄로 작성 # 각 옵션 타입별로 XPath를 문자열 포맷을 사용하여 한 줄로 작성
# option_num_xpaths = [ 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) "//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개까지 있음 for idx in range(1, 4) # 옵션 타입이 3개까지 있음
# ] ]
# 각 옵션 타입의 갯수를 가져와서 options_info에 업데이트 # 각 옵션 타입의 갯수를 가져와서 options_info에 업데이트
# for idx, option_num_xpath in enumerate(option_num_xpaths[:option_type_nums], start=1): for idx, option_num_xpath in enumerate(option_num_xpaths[:option_type_nums], start=1):
# try: try:
# option_num_element = return_element(driver, 'XPATH', option_num_xpath, 5) option_num_element = return_element(driver, 'XPATH', option_num_xpath, 5)
# if option_num_element: if option_num_element:
# option_number = int(re.search(r'\d+', option_num_element.text).group()) option_number = int(re.search(r'\d+', option_num_element.text).group())
# logger.debug(f"옵션타입 [{idx}] 의 갯수 : {option_number}") logger.debug(f"옵션타입 [{idx}] 의 갯수 : {option_number}")
# options_info[f'option_type_{idx}']['options_count'] = option_number options_info[f'option_type_{idx}']['options_count'] = option_number
# else: else:
# logger.debug(f"옵션타입 {idx} 요소를 찾을 수 없음.") logger.debug(f"옵션타입 {idx} 요소를 찾을 수 없음.")
# options_info[f'option_type_{idx}']['options_count'] = None options_info[f'option_type_{idx}']['options_count'] = None
# except Exception as e: except Exception as e:
# logger.error(f"옵션 타입 {idx} 갯수를 가져오는 중 오류 발생: {e}", exc_info=True) logger.error(f"옵션 타입 {idx} 갯수를 가져오는 중 오류 발생: {e}", exc_info=True)
# # 가격 낮은 순으로 정렬 # # 가격 낮은 순으로 정렬
# logger.debug("가격 낮은 순으로 정렬") # logger.debug("가격 낮은 순으로 정렬")
@ -228,16 +150,13 @@ def modify_option_page(driver, product_info, gemini, translator, login_info):
try: try:
logger.debug(f"옵션타입 {option_idx}에 대한 처리 시작") logger.debug(f"옵션타입 {option_idx}에 대한 처리 시작")
logger.debug("가격 낮은 순으로 정렬") 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_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)" click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js')
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: # 심플모드일 경우 옵션 편집하지 않음. 옵션 번역만. if not simpleMode: # 심플모드일 경우 옵션 편집하지 않음. 옵션 번역만.
edit_option(driver, option_idx, options_info[f'option_type_{option_idx}']['options_count']) # 각 옵션의 전체 체크박스 해제 edit_option(driver, option_idx, options_info[f'option_type_{option_idx}']['options_count']) # 각 옵션의 전체 체크박스 해제
# 가격 낮은 순으로 재정렬 # 가격 낮은 순으로 재정렬
# logger.debug("가격 낮은 순으로 재정렬") logger.debug("가격 낮은 순으로 재정렬")
# click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js') 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) 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: except KeyError:
logger.error(f"옵션타입 {option_idx}에 대한 정보가 없습니다.") logger.error(f"옵션타입 {option_idx}에 대한 정보가 없습니다.")
@ -332,13 +251,11 @@ def option_name_trans(driver, translator, product_info, option_type_number, opti
logger.debug("선택된 옵션명 DeepL로 번역 후 새로운 옵션명 입력") logger.debug("선택된 옵션명 DeepL로 번역 후 새로운 옵션명 입력")
ori_optionNames = [] ori_optionNames = []
deepl_trans_optionNames = [] deepl_trans_optionNames = []
# 원본 옵션명 추출 및 이미지 URL 확인 # 원본 옵션명 추출
try: try:
for i in range(1, option_count + 1): for i in range(1, option_count + 1):
if option_type_number == 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" 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: 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" 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 else: # option_type_number == 3
@ -349,35 +266,10 @@ def option_name_trans(driver, translator, product_info, option_type_number, opti
cleaned_ori_optionName = replace_or_remove_special_chars(ori_optionName, allowed_special_chars, special_char_replacements) cleaned_ori_optionName = replace_or_remove_special_chars(ori_optionName, allowed_special_chars, special_char_replacements)
logger.debug(f"정제된 {i}번째 원본 옵션명 : {cleaned_ori_optionName}") logger.debug(f"정제된 {i}번째 원본 옵션명 : {cleaned_ori_optionName}")
ori_optionNames.append(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: except Exception as e:
logger.debug(f"원본옵션명 처리중 에러발생 : {e}", exc_info=True) logger.debug(f"원본옵션명 처리중 에러발생 : {e}", exc_info=True)
# 원본 옵션명을 하나의 텍스트로 합치기 # 원본 옵션명을 하나의 텍스트로 합치기
# combined_ori_optionNames = '\n\n'.join(ori_optionNames) # combined_ori_optionNames = '\n\n'.join(ori_optionNames)
@ -498,327 +390,3 @@ def update_price_range(driver, options_info, product_info):
logger.debug(f"전체 옵션의 가격 범위 업데이트: 최소값 {overall_min_price}, 최대값 {overall_max_price}") logger.debug(f"전체 옵션의 가격 범위 업데이트: 최소값 {overall_min_price}, 최대값 {overall_max_price}")
except Exception as e: except Exception as e:
logger.error(f"가격 범위 업데이트 중 오류 발생: {e}", exc_info=True) 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(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_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_option_product(driver):
"""
웹페이지에서 현재 상품이 '단일 상품'인지 '옵션 상품'인지 확인합니다.
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("상품 유형: 옵션 상품등록")
return True
else:
logger.debug("상품 유형: 단일 상품등록")
return False
except Exception as e:
logger.error(f"상품 유형 확인 중 오류 발생: {e}", exc_info=True)
logger.debug("상품 유형 오류 발생으로 기본설정인 단일 상품등록으로 진행합니다.")
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 click_sort_by_price(driver, option_type_number):
"""
옵션 타입별로 가격 내림차순 정렬 버튼을 클릭합니다.
Parameters:
- driver: WebDriver 인스턴스
- option_type_number: 옵션 타입 번호 (1, 2, 3 )
"""
try:
sort_button_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type_number}]/div/div/div[2]/div/div/div[4]/div[2]/div[2]/div/div[4]/button"
sort_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, sort_button_xpath))
)
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

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

@ -30,14 +30,11 @@ def modify_price_page(driver, product_infos):
time.sleep(2) # 페이지 로딩 대기. time.sleep(2) # 페이지 로딩 대기.
try: try:
plus_fee_xpath = "//div[@id='productMainContentContainerId']/div/div/div/div/div[2]/div/div/div[8]/div/div/div[3]/div/div/div/div/div[2]/input"
plus_fee_css = "div#productMainContentContainerId div:nth-child(8) > div > div > div:nth-child(3) > div > div > div > div.ant-input-number.css-1li46mu.ant-input-number-outlined > div.ant-input-number-input-wrap > input" plus_fee_xpath="//div[@id='productMainContentContainerId']/div/div/div/div/div[2]/div/div/div[8]/div/div/div[3]/div/div/div/div/div[2]/input"
plus_fee_element = return_element(driver, 'XPATH', plus_fee_xpath, 10) plus_fee_element = return_element(driver, 'XPATH', plus_fee_xpath, 10)
plus_fee_element_by_css = return_element(driver, 'CSS', plus_fee_css, 10) plus_fee_text = plus_fee_element.text()
plus_fee_text = plus_fee_element.text
print(f"plus_fee_text : {plus_fee_text}")
plus_fee_by_css_text = plus_fee_element_by_css.text
print(f"plus_fee_by_css_text : {plus_fee_by_css_text}")
option_high_price = product_infos.option_high_price option_high_price = product_infos.option_high_price
option_low_price = product_infos.option_low_price option_low_price = product_infos.option_low_price
@ -135,7 +132,7 @@ def modify_price_page(driver, product_infos):
return_fee_element.send_keys(Keys.CLEAR) # 기존 가격 삭제 return_fee_element.send_keys(Keys.CLEAR) # 기존 가격 삭제
driver.execute_script("arguments[0].value = '';", return_fee_element) # 기존 가격 삭제 JS driver.execute_script("arguments[0].value = '';", return_fee_element) # 기존 가격 삭제 JS
logger.debug("기존가격 삭제") logger.debug("기존가격 삭제")
return_fee = 199000 return_fee = 199,000
return_fee_element.send_keys(return_fee) # 새 가격 입력 return_fee_element.send_keys(return_fee) # 새 가격 입력
logger.debug(f"반품비 수정 완료 : {return_fee}") logger.debug(f"반품비 수정 완료 : {return_fee}")
except Exception as e: except Exception as e:
@ -148,7 +145,7 @@ def modify_price_page(driver, product_infos):
first_delv_fee_element.send_keys(Keys.CLEAR) # 기존 가격 삭제 first_delv_fee_element.send_keys(Keys.CLEAR) # 기존 가격 삭제
driver.execute_script("arguments[0].value = '';", first_delv_fee_element) # 기존 가격 삭제 JS driver.execute_script("arguments[0].value = '';", first_delv_fee_element) # 기존 가격 삭제 JS
logger.debug("기존가격 삭제") logger.debug("기존가격 삭제")
init_delv_fee = 199000 init_delv_fee = 199,000
first_delv_fee_element.send_keys(init_delv_fee) # 새 가격 입력 first_delv_fee_element.send_keys(init_delv_fee) # 새 가격 입력
logger.debug(f"초도배송비 수정 완료 : {init_delv_fee}") logger.debug(f"초도배송비 수정 완료 : {init_delv_fee}")
except Exception as e: except Exception as e:

View File

@ -23,18 +23,13 @@ class ProductInfo:
self.return_fee = None # 반품비 self.return_fee = None # 반품비
self.init_delv_fee = None # 초기반품비 self.init_delv_fee = None # 초기반품비
self.exchange_fee = None # 교환배송비 self.exchange_fee = None # 교환배송비
self.ai_contents_html = None # ai가 생성한 컨텐츠 결과 self.ai_contents = None # ai가 생성한 컨텐츠 결과
self.ai_contents_mark = None # ai가 생성한 컨텐츠 결과
self.current_value = None # 상세페이지의 현재 내용 self.current_value = None # 상세페이지의 현재 내용
self.deleted_value = None # 상세페이지의 이미지가 삭제된 내용
self.new_value = None # 상세페이지의 바뀐 내용 self.new_value = None # 상세페이지의 바뀐 내용
self.naver_products = [] # 네이버 파싱된 상품들 self.naver_products = [] # 네이버 파싱된 상품들
self.is_option_product = False
self.option_datas = {}
self.option_1_names = [] # 상품 옵션 이름 리스트 self.option_1_names = [] # 상품 옵션 이름 리스트
self.trans_option_1_names = [] # 번역된 상품 옵션 이름 리스트 self.trans_option_1_names = [] # 번역된 상품 옵션 이름 리스트
self.trans_option_1_name_common_parts = [] # 번역된 상품 옵션 이름 리스트의 단어별 리스트 저장 self.trans_option_1_name_common_parts = [] # 번역된 상품 옵션 이름 리스트의 단어별 리스트 저장
@ -80,8 +75,7 @@ class ProductInfo:
'return_fee': self.return_fee, 'return_fee': self.return_fee,
'init_delv_fee': self.init_delv_fee, 'init_delv_fee': self.init_delv_fee,
'exchange_fee': self.exchange_fee, 'exchange_fee': self.exchange_fee,
'ai_contents_html': self.ai_contents_html, 'ai_contents': self.ai_contents,
'ai_contents_mark': self.ai_contents_mark,
'current_value': self.current_value, 'current_value': self.current_value,
'new_value': self.new_value, 'new_value': self.new_value,
'naver_products': self.naver_products, 'naver_products': self.naver_products,

17
edit/trans_image.py Normal file
View File

@ -0,0 +1,17 @@
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, re, random
from edit.action_elements import click_element, return_element, click_and_confirm_tab
from edit.price_cal import calculate_margin_and_price
# from ai.compare import find_most_similar_image_by_one
import logging
# 로거 인스턴스 가져오기
logger = logging.getLogger('default_logger')
def tran_detail_image(driver, product_infos):
return None

View File

@ -4,8 +4,7 @@ import base64
from PIL import Image from PIL import Image
import urllib, requests import urllib, requests
import numpy as np import numpy as np
import time
import random
from img_trans.src.text_detection_with_paddle import detect_text from img_trans.src.text_detection_with_paddle import detect_text
from img_trans.src.removal import process_image_based_on_text_area from img_trans.src.removal import process_image_based_on_text_area
from img_trans.src.translation import translate_texts_translatepy from img_trans.src.translation import translate_texts_translatepy
@ -13,13 +12,13 @@ from img_trans.src.translation import translate_texts_deepl
from img_trans.src.text_insertion import insert_text from img_trans.src.text_insertion import insert_text
from img_trans.src.mini_crop_process import resize_and_crop_image from img_trans.src.mini_crop_process import resize_and_crop_image
from img_trans.src.rotate_img import flip_and_rotate_image from img_trans.src.rotate_img import flip_and_rotate_image
from img_trans.src.inpainting import inpaint_text # 기존 인페인팅 메서드 from img_trans.src.inpainting import inpaint_text, inpaint_text_with_deepfill # 기존 인페인팅 메서드
# import logging import logging
# 로거 인스턴스 가져오기 # 로거 인스턴스 가져오기
# logger = logging.getLogger('default_logger') logger = logging.getLogger('default_logger')
def image_trans(image_url, translator, convert_type, logger): def image_trans(image_url, translator, convert_type):
# 이미지를 URL로부터 읽어옵니다. # 이미지를 URL로부터 읽어옵니다.
try: try:
# image = read_image_from_url(image_url) # image = read_image_from_url(image_url)
@ -66,7 +65,7 @@ def image_trans(image_url, translator, convert_type, logger):
elif convert_type == 'translate': elif convert_type == 'translate':
# 2단계: 인페이팅 기법으로 텍스트 제거 # 2단계: 인페이팅 기법으로 텍스트 제거
inpainted_image = inpaint_text(image, detected_texts, logger) inpainted_image = inpaint_text(image, detected_texts)
# cv2.imwrite(inpainted_image_path, inpainted_image) # cv2.imwrite(inpainted_image_path, inpainted_image)
# 이미지 번역 처리 # 이미지 번역 처리
logger.debug("이미지 번역 처리 시작") logger.debug("이미지 번역 처리 시작")
@ -74,21 +73,21 @@ def image_trans(image_url, translator, convert_type, logger):
texts_to_translate = [text for _, text, _, _, _ in detected_texts] texts_to_translate = [text for _, text, _, _, _ in detected_texts]
logger.debug(f"원본 텍스트: {texts_to_translate}") logger.debug(f"원본 텍스트: {texts_to_translate}")
logger.debug(f"DEEPL 번역시작") logger.debug(f"DEEPL 번역시작")
# translated_texts = translate_texts_deepl(texts_to_translate, logger) # translated_texts = translate_texts_deepl(texts_to_translate)
try: try:
translated_texts = translator.translate(texts_to_translate, logger) translated_texts = translator.translate(texts_to_translate)
# translated_texts = None if not translated_texts: # 빈 리스트 체크
if not translated_texts or translated_texts == texts_to_translate: # 빈 리스트 체크
logger.debug(f"DEEPL 번역실패로 translatepy 시작") logger.debug(f"DEEPL 번역실패로 translatepy 시작")
translated_texts = translate_texts_translatepy(texts_to_translate, logger) translated_texts = translate_texts_translatepy(texts_to_translate)
except Exception as e: except Exception as e:
translated_texts = translate_texts_translatepy(texts_to_translate, logger) translated_texts = translate_texts_translatepy(texts_to_translate)
logger.debug(f"번역 텍스트: {texts_to_translate}") logger.debug(f"번역 텍스트: {texts_to_translate}")
# translated_image = insert_text(inpainted_image_path, translated_texts, detected_texts) # translated_image = insert_text(inpainted_image_path, translated_texts, detected_texts)
translated_image = insert_text(inpainted_image, translated_texts, detected_texts, logger) translated_image = insert_text(inpainted_image, translated_texts, detected_texts)
# cv2.imwrite(translated_image_path, translated_image) # cv2.imwrite(translated_image_path, translated_image)
logger.debug(f"번역된 이미지 생성완료 : {translated_image_path}") logger.debug(f"번역된 이미지 생성완료 : {translated_image_path}")
# 번역된 이미지 변환합니다. # 번역된 이미지 변환합니다.
pil_img = Image.fromarray(cv2.cvtColor(translated_image, cv2.COLOR_BGR2RGB)) pil_img = Image.fromarray(cv2.cvtColor(translated_image, cv2.COLOR_BGR2RGB))
# buffered = BytesIO() # buffered = BytesIO()
@ -114,7 +113,7 @@ def read_image_from_url(image_url):
logger.error(f"이미지 로딩 중 오류 발생: {e}") logger.error(f"이미지 로딩 중 오류 발생: {e}")
return None return None
def read_image_from_url_with_requests_ori(image_url): def read_image_from_url_with_requests(image_url):
if not image_url: # URL이 비어있는지 확인 if not image_url: # URL이 비어있는지 확인
logger.error("빈 URL이 제공되었습니다.") logger.error("빈 URL이 제공되었습니다.")
return None return None
@ -137,43 +136,6 @@ def read_image_from_url_with_requests_ori(image_url):
logger.error(f"이미지 로딩 중 오류 발생: {e}") logger.error(f"이미지 로딩 중 오류 발생: {e}")
return None return None
def read_image_from_url_with_requests(image_url, max_retries=3):
if not image_url: # URL이 비어있는지 확인
logger.error("빈 URL이 제공되었습니다.")
return None
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"DNT": "1", # Do Not Track 요청 헤더
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Cache-Control": "max-age=0"
}
retries = 0
while retries < max_retries:
try:
response = requests.get(image_url, headers=headers)
if response.status_code == 200:
image = np.asarray(bytearray(response.content), dtype="uint8")
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
return image
else:
# 응답 코드가 200이 아닐 때 로그를 남기고 재시도
logger.error(f"이미지 url 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries+1}/{max_retries} : 대상URL : {image_url}")
retries += 1
time.sleep(random.randint(2, 5)) # 5초에서 10초 사이의 랜덤한 시간 동안 대기
except Exception as e:
logger.error(f"이미지 로딩 중 오류 발생: {e}")
retries += 1
time.sleep(random.randint(2, 5)) # 예외 발생 시 랜덤 대기 후 재시도
logger.error("최대 재시도 횟수 초과")
return None
# 외부에서 함수를 사용하는 예 # 외부에서 함수를 사용하는 예
# 이미지 URL과 원하는 변환 유형('thumbnail' 또는 'translate')을 인자로 넘깁니다. # 이미지 URL과 원하는 변환 유형('thumbnail' 또는 'translate')을 인자로 넘깁니다.
# result = image_trans('http://example.com/image.jpg', 'thumbnail') # result = image_trans('http://example.com/image.jpg', 'thumbnail')

View File

@ -1,4 +1,5 @@
import torch import torch
from model import InpaintingModel # 모델과 관련된 클래스를 포함하는 가상의 파일
class DeepInpainter: class DeepInpainter:
def __init__(self, model_path): def __init__(self, model_path):

View File

@ -1,12 +1,12 @@
import cv2 import cv2
import numpy as np import numpy as np
# from img_trans.generative_inpainting.inpaint_model import InpaintCAModel # from img_trans.generative_inpainting.inpaint_model import InpaintCAModel
# import logging import logging
# # 로거 인스턴스 가져오기 # 로거 인스턴스 가져오기
# logger = logging.getLogger('default_logger') logger = logging.getLogger('default_logger')
def inpaint_text(image, detected_texts, logger, expansion=5): def inpaint_text(image, detected_texts, expansion=5):
""" """
이미지 텍스트 제거를 위해 인페이팅 기법을 사용합니다. 이미지 텍스트 제거를 위해 인페이팅 기법을 사용합니다.
expansion 파라미터는 텍스트 주변의 마스크 확장 크기를 지정합니다. expansion 파라미터는 텍스트 주변의 마스크 확장 크기를 지정합니다.
@ -27,9 +27,76 @@ def inpaint_text(image, detected_texts, logger, expansion=5):
# 여기에서 딥러닝 기반 인페인팅 모델을 적용할 수 있습니다. # 여기에서 딥러닝 기반 인페인팅 모델을 적용할 수 있습니다.
# 인페이팅 알고리즘 적용 (TELEA 또는 NS 방식 중 선택) # 인페이팅 알고리즘 적용 (TELEA 또는 NS 방식 중 선택)
inpainted_image = cv2.inpaint(image, mask, inpaintRadius=2, flags=cv2.INPAINT_TELEA) inpainted_image = cv2.inpaint(image, mask, inpaintRadius=3, flags=cv2.INPAINT_TELEA)
# 결과 이미지 반환 # 결과 이미지 반환
return inpainted_image return inpainted_image
def inpaint_text_with_deepfill(image, detected_texts, model_checkpoint, expansion=5):
"""
이미지 텍스트를 제거하기 위해 DeepFill v2 기반의 인페이팅 기법을 사용합니다.
함수는 detect_text에서 반환된 텍스트 위치와 관련 데이터를 사용하여 인페인팅을 수행합니다.
Parameters:
- image: 인페인팅을 수행할 이미지 (OpenCV 이미지 포맷)
- detected_texts: 감지된 텍스트 목록, 텍스트는 (box, text, confidence, width, height) 포맷
- model_checkpoint: DeepFill v2 모델 체크포인트 경로
- expansion: 텍스트 박스 확장 크기 (기본값 5)
"""
try:
# 모델 로드
model = InpaintCAModel()
model.load_checkpoint(model_checkpoint)
logging.info("모델이 성공적으로 로드되었습니다.")
# 마스크 초기화
mask = np.zeros(image.shape[:2], dtype=np.uint8)
# 감지된 텍스트를 기반으로 마스크 생성
for (box, text, confidence, text_width, text_height) in detected_texts:
x_min, y_min = np.min(box, axis=0)
x_max, y_max = np.max(box, axis=0)
# 박스 확장
x_min = max(x_min - expansion, 0)
y_min = max(y_min - expansion, 0)
x_max = min(x_max + expansion, image.shape[1])
y_max = min(y_max + expansion, image.shape[0])
cv2.rectangle(mask, (int(x_min), int(y_min)), (int(x_max), int(y_max)), 255, thickness=-1)
# 이미지와 마스크 정규화
image = image / 255.0
mask = mask / 255.0
# 인페인팅 실행
inpainted_image = model.test(image, mask)
inpainted_image = (inpainted_image * 255).astype(np.uint8)
logging.info("인페인팅이 성공적으로 완료되었습니다.")
return inpainted_image
except FileNotFoundError as e:
logging.error(e)
except Exception as e:
logging.error(f"인페인팅 과정에서 예상치 못한 오류가 발생했습니다: {e}")
return None
# # 모델 로드 (예: 모델 파일 경로가 필요함)
# model = InpaintCAModel()
# model.load_checkpoint(checkpoint_model_path)
# # 이미지 로드
# image = cv2.imread('path_to_your_image.jpg')
# # 텍스트 위치와 정보 (예시 데이터)
# detected_texts = [((50, 50, 150, 100), "Example text", 0.95)]
# # 인페인팅 실행
# inpainted_image = inpaint_text(image, detected_texts, model)
# # 결과 이미지 저장
# cv2.imwrite('inpainted_image.jpg', inpainted_image)

View File

@ -0,0 +1 @@
model_checkpoint_path: "snap-0"

Binary file not shown.

Binary file not shown.

View File

@ -2,10 +2,54 @@ from PIL import Image, ImageDraw, ImageFont
import numpy as np import numpy as np
import cv2 import cv2
from colorsys import rgb_to_hsv, hsv_to_rgb from colorsys import rgb_to_hsv, hsv_to_rgb
# import logging import logging
# 로거 인스턴스 가져오기 # 로거 인스턴스 가져오기
# logger = logging.getLogger('default_logger') logger = logging.getLogger('default_logger')
def draw_translucent_banner_with_GPT(draw, box, text, font, fill_color=(255, 255, 255, 220)):
"""텍스트 주변에 반투명 배너를 그립니다."""
try:
margin = 10 # 텍스트와 배너 사이의 여백
text_width, text_height = draw.textsize(text, font=font)
x_min, y_min, x_max, y_max = box
banner_box = [x_min - margin, y_min - margin, x_min + text_width + margin, y_min + text_height + margin]
draw.rectangle(banner_box, fill=fill_color)
except Exception as e:
logger.error(f"Error drawing translucent banner: {e}")
def draw_translucent_banner_with_gemini(draw, box, text, font, fill_color=(255, 255, 255, 220)):
"""텍스트 주변에 반투명 배너를 그립니다."""
try:
margin = 10 # 텍스트와 배너 사이의 여백
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
# text_metrics = font.getmetrics(text)
# text_width = text_metrics[0]
# text_height = text_metrics[1]
# text_width, text_height = font.getsize(text)
x_min, y_min, x_max, y_max = box
banner_box = [x_min - margin, y_min - margin, x_min + text_width + margin, y_min + text_height + margin]
draw.rectangle(banner_box, fill=fill_color)
except Exception as e:
logger.error(f"Error drawing translucent banner: {e}")
def get_complementary_color_ori(image, box):
"""주어진 박스 영역 내의 평균 색상의 보색을 계산합니다."""
x_min, y_min, x_max, y_max = box
cropped_image = image.crop((x_min, y_min, x_max, y_max))
np_image = np.array(cropped_image)
average_color = np_image.mean(axis=(0, 1))
# RGB에서 HSV로 변환
hsv = rgb_to_hsv(*(average_color/255))
# H 채널을 조정하여 보색 구하기 (Hue를 반전)
hsv = ((hsv[0] + 0.5) % 1.0, hsv[1], hsv[2])
# HSV에서 RGB로 다시 변환
complementary_color = hsv_to_rgb(*hsv)
return tuple(int(c * 255) for c in complementary_color)
def get_dominant_color(image, box, expand=10): def get_dominant_color(image, box, expand=10):
"""이미지의 주어진 영역에서 지배적인 색상을 찾습니다.""" """이미지의 주어진 영역에서 지배적인 색상을 찾습니다."""
@ -22,7 +66,7 @@ def get_complementary_color(color):
complementary = (255 - color[0], 255 - color[1], 255 - color[2]) complementary = (255 - color[0], 255 - color[1], 255 - color[2])
return complementary return complementary
def calculate_font_size(text, logger, font_path="NotoSansKR-Bold.ttf", desired_width=None, desired_height=None): def calculate_font_size(text, font_path="NotoSansKR-Bold.ttf", desired_width=None, desired_height=None):
font_size = 10 # 초기 폰트 크기 font_size = 10 # 초기 폰트 크기
max_font_size = 100 # 초기 폰트 크기 max_font_size = 100 # 초기 폰트 크기
font = ImageFont.truetype(font_path, font_size) font = ImageFont.truetype(font_path, font_size)
@ -47,48 +91,8 @@ def calculate_font_size(text, logger, font_path="NotoSansKR-Bold.ttf", desired_w
return font_size return font_size
def draw_text_with_border(draw, translated_text, position, font, text_color, border_color):
# 텍스트 테두리 그리기
x, y = position
shadow_offset = 2 # 테두리 굵기 설정
draw.text((x - shadow_offset, y), translated_text, font=font, fill=border_color)
draw.text((x + shadow_offset, y), translated_text, font=font, fill=border_color)
draw.text((x, y - shadow_offset), translated_text, font=font, fill=border_color)
draw.text((x, y + shadow_offset), translated_text, font=font, fill=border_color)
draw.text((x - shadow_offset, y - shadow_offset), translated_text, font=font, fill=border_color)
draw.text((x + shadow_offset, y - shadow_offset), translated_text, font=font, fill=border_color)
draw.text((x - shadow_offset, y + shadow_offset), translated_text, font=font, fill=border_color)
draw.text((x + shadow_offset, y + shadow_offset), translated_text, font=font, fill=border_color)
# 본래 텍스트 그리기
draw.text(position, translated_text, font=font, fill=text_color)
def draw_text_with_effects(draw, logger, translated_text, position, font, text_color="white", border_color="black"):
try:
# 그림자 효과
shadow_offset = (2, 2)
draw.text((position[0] + shadow_offset[0], position[1] + shadow_offset[1]), translated_text, fill="gray", font=font)
# 텍스트 테두리
offsets = [(-1, -1), (-1, 1), (1, -1), (1, 1), (-1, 0), (1, 0), (0, -1), (0, 1)]
for offset in offsets:
draw.text((position[0] + offset[0], position[1] + offset[1]), translated_text, fill=border_color, font=font)
# 본래 텍스트
draw.text(position, translated_text, font=font, fill=text_color)
except Exception as e:
logger.error(f"draw_text_with_effects 메서드 중 에러발생 : {e}", exc_info=True)
def draw_translucent_background(draw, box, fill_color=(255, 255, 255, 50)):
"""텍스트 주변에 반투명 배경을 추가합니다."""
try:
margin = 10 # 배너와 텍스트 사이의 여백
expanded_box = [box[0] - margin, box[1] - margin, box[2] + margin, box[3] + margin]
draw.rectangle(expanded_box, fill=fill_color)
except Exception as e:
logger.error(f"draw_translucent_background 메서드 중 에러발생 : {e}", exc_info=True)
# def insert_text(image_path, translated_texts, detected_texts): # def insert_text(image_path, translated_texts, detected_texts):
def insert_text(inpainted_image, translated_texts, detected_texts, logger): def insert_text(inpainted_image, translated_texts, detected_texts):
try: try:
# # PIL 이미지 객체로 변환합니다. (OpenCV 이미지가 아닐 경우) # # PIL 이미지 객체로 변환합니다. (OpenCV 이미지가 아닐 경우)
if isinstance(inpainted_image, np.ndarray): if isinstance(inpainted_image, np.ndarray):
@ -102,9 +106,6 @@ def insert_text(inpainted_image, translated_texts, detected_texts, logger):
for detected_text, translated_text in zip(detected_texts, translated_texts): for detected_text, translated_text in zip(detected_texts, translated_texts):
box = detected_text[0] # (box, text, confidence) 형태의 detected_text에서 box를 추출 box = detected_text[0] # (box, text, confidence) 형태의 detected_text에서 box를 추출
# text_position = (box[0], box[1]) # 텍스트 위치 설정
text_position = (box[0][0], box[0][1])
# box 형태: [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] # box 형태: [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
# 이미지 영역에서 지배적인 색상을 찾기 위해 x, y 좌표 최소값과 최대값을 사용 # 이미지 영역에서 지배적인 색상을 찾기 위해 x, y 좌표 최소값과 최대값을 사용
x_coords = [coord[0] for coord in box] x_coords = [coord[0] for coord in box]
@ -112,33 +113,55 @@ def insert_text(inpainted_image, translated_texts, detected_texts, logger):
x_min, x_max = min(x_coords), max(x_coords) x_min, x_max = min(x_coords), max(x_coords)
y_min, y_max = min(y_coords), max(y_coords) y_min, y_max = min(y_coords), max(y_coords)
new_box = [x_min, y_min, x_max, y_max]
# draw_translucent_background(draw, new_box)
# 수정된 부분: 올바른 x, y 좌표를 사용하여 dominant_color 계산 # 수정된 부분: 올바른 x, y 좌표를 사용하여 dominant_color 계산
# dominant_color = get_dominant_color(image, (x_min, y_min, x_max, y_max)) dominant_color = get_dominant_color(image, (x_min, y_min, x_max, y_max))
# complementary_color = get_complementary_color(dominant_color) complementary_color = get_complementary_color(dominant_color)
# translated_text = detected_text[1] # 번역된 텍스트 # translated_text = detected_text[1] # 번역된 텍스트
logger.debug(f"translated_text : {translated_text}") print(f"translated_text : {translated_text}")
text_width = np.linalg.norm(np.array(box[0]) - np.array(box[1])) text_width = np.linalg.norm(np.array(box[0]) - np.array(box[1]))
text_height = np.linalg.norm(np.array(box[1]) - np.array(box[2])) text_height = np.linalg.norm(np.array(box[1]) - np.array(box[2]))
font_size = calculate_font_size(translated_text, logger, "NotoSansKR-Bold.ttf", text_width, text_height) font_size = calculate_font_size(translated_text, "NotoSansKR-Bold.ttf", text_width, text_height)
font_background = ImageFont.truetype("NotoSansKR-Bold.ttf", font_size) font_background = ImageFont.truetype("NotoSansKR-Bold.ttf", font_size)
font = ImageFont.truetype("NotoSansKR-Bold.ttf", font_size) font = ImageFont.truetype("NotoSansKR-Bold.ttf", font_size)
# 반투명 배너 그리기 # 반투명 배너 그리기
# draw_translucent_banner(draw, (x_min, y_min, x_max, y_max), translated_text, font) # draw_translucent_banner(draw, (x_min, y_min, x_max, y_max), translated_text, font)
# draw.text((x_min+1, y_min+1), translated_text, fill="white", font=font_background) draw.text((x_min+1, y_min+1), translated_text, fill="white", font=font_background)
# draw.text((x_min-1, y_min+1), translated_text, fill="white", font=font_background) draw.text((x_min-1, y_min+1), translated_text, fill="white", font=font_background)
# draw.text((x_min+1, y_min-1), translated_text, fill="white", font=font_background) draw.text((x_min+1, y_min-1), translated_text, fill="white", font=font_background)
# draw.text((x_min-1, y_min-1), translated_text, fill="white", font=font_background) draw.text((x_min-1, y_min-1), translated_text, fill="white", font=font_background)
# draw.text((x_min, y_min), translated_text, fill="black", font=font) draw.text((x_min, y_min), translated_text, fill="black", font=font)
draw_text_with_effects(draw, logger, translated_text, text_position, font)
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGR) cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGR)
return cv_image return cv_image
except Exception as e: except Exception as e:
logger.error(f"Error inserting text: {e}", exc_info=True) logger.error(f"Error inserting text: {e}")
return inpainted_image return inpainted_image
# def insert_text(image_path, translated_texts, detected_texts):
# image = Image.open(image_path).convert("RGBA")
# draw = ImageDraw.Draw(image)
# for detected_text, translated_text in zip(detected_texts, translated_texts):
# box, _, _, _, _ = detected_text # detected_texts에서 추가 정보가 있다면 여기서 언패킹
# text_width = np.linalg.norm(np.array(box[0]) - np.array(box[1]))
# text_height = np.linalg.norm(np.array(box[1]) - np.array(box[2]))
# desired_width = text_width # 폰트 크기 계산을 위한 desired_width
# desired_height = text_height # 폰트 크기 계산을 위한 desired_height
# # 지배적인 색상 계산
# dominant_color = get_dominant_color(image, box)
# # font_size = calculate_font_size(text_width, text_height, translated_text, "NotoSansKR-Bold.ttf", desired_width, desired_height)
# font_size = calculate_font_size(translated_text, "NotoSansKR-Bold.ttf", desired_width, desired_height)
# font = ImageFont.truetype("NotoSansKR-Bold.ttf", font_size)
# x_min, y_min = min(box[0][0], box[3][0]), min(box[0][1], box[1][1])
# draw.text((x_min, y_min), translated_text, fill=dominant_color, font=font)
# cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGR)
# return cv_image

View File

@ -8,7 +8,7 @@ import logging
# 로거 인스턴스 가져오기 # 로거 인스턴스 가져오기
logger = logging.getLogger('default_logger') logger = logging.getLogger('default_logger')
def translate_texts_google(texts, logger, src_lang='zh-cn', dest_lang='ko'): def translate_texts_google(texts, src_lang='zh-cn', dest_lang='ko'):
"""텍스트 리스트를 지정된 언어에서 다른 언어로 번역""" """텍스트 리스트를 지정된 언어에서 다른 언어로 번역"""
translated_texts = [] translated_texts = []
translator = Translator() translator = Translator()
@ -22,7 +22,7 @@ def translate_texts_google(texts, logger, src_lang='zh-cn', dest_lang='ko'):
return translated_texts return translated_texts
def translate_texts_deepl_ori(texts, logger): def translate_texts_deepl_ori(texts):
"""텍스트 리스트를 지정된 언어에서 다른 언어로 번역""" """텍스트 리스트를 지정된 언어에서 다른 언어로 번역"""
# 텍스트 리스트를 엔터로 구분된 하나의 문자열로 합침 # 텍스트 리스트를 엔터로 구분된 하나의 문자열로 합침
combined_text = "\n".join(texts) combined_text = "\n".join(texts)
@ -43,7 +43,7 @@ def translate_texts_deepl_ori(texts, logger):
return translated_texts return translated_texts
def translate_texts_deepl(texts, logger): def translate_texts_deepl(texts):
"""텍스트 리스트를 지정된 언어에서 다른 언어로 번역""" """텍스트 리스트를 지정된 언어에서 다른 언어로 번역"""
translated_texts = [] translated_texts = []
@ -60,7 +60,7 @@ def translate_texts_deepl(texts, logger):
return translated_texts return translated_texts
def translate_texts_translatepy(texts, logger, src_lang='zh-cn', dest_lang='ko'): def translate_texts_translatepy(texts, src_lang='zh-cn', dest_lang='ko'):
"""translatepy 라이브러리를 사용하여 텍스트 리스트를 번역합니다.""" """translatepy 라이브러리를 사용하여 텍스트 리스트를 번역합니다."""
translator = Translator() translator = Translator()
translated_texts = [] translated_texts = []

View File

@ -1,4 +1,4 @@
# from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import check_password_hash
from PyQt5.QtCore import Qt, QSettings from PyQt5.QtCore import Qt, QSettings
from PyQt5 import QtWidgets, QtCore, QtWidgets from PyQt5 import QtWidgets, QtCore, QtWidgets
from datetime import datetime from datetime import datetime
@ -419,8 +419,7 @@ class LoginWidget(QtWidgets.QWidget):
# MongoDB에서 사용자 문서 조회 # MongoDB에서 사용자 문서 조회
user_doc = self.login_db.users.find_one({"email": email}) user_doc = self.login_db.users.find_one({"email": email})
# if user_doc and check_password_hash(user_doc['password'], password): if user_doc and check_password_hash(user_doc['password'], password):
if user_doc:
# 비밀번호 검증 성공 # 비밀번호 검증 성공
self.email = email # 여기에서 사용자 이메일을 self.email 속성에 저장 self.email = email # 여기에서 사용자 이메일을 self.email 속성에 저장

View File

@ -212,7 +212,6 @@ def main():
#웹 드라이버 종료 #웹 드라이버 종료
driver.quit() driver.quit()
translator.close() translator.close()
QtWidgets.QMessageBox.information("알림", "작업이 완료되었습니다.")
if __name__ == "__main__": if __name__ == "__main__":
CURRENT_VERSION = "1.0.0" CURRENT_VERSION = "1.0.0"

View File

@ -14,6 +14,7 @@ from edit.options import modify_option_page
from edit.price import modify_price_page from edit.price import modify_price_page
from edit.title import modify_product_title from edit.title import modify_product_title
from edit.thumbnail import modify_thumb_page from edit.thumbnail import modify_thumb_page
from edit.trans_image import *
from edit.uploadMarket import * from edit.uploadMarket import *
from edit.action_elements import click_element, return_element, wait_element from edit.action_elements import click_element, return_element, wait_element
import logging import logging
@ -112,8 +113,8 @@ def modify_products(driver, gemini, translator, mongo_config, login_info, set_nu
# product_info = set_product_info() # 상품정보리스트 생성 # product_info = set_product_info() # 상품정보리스트 생성
autoPercentyProductsDB = AutoPercentyProductsDB(mongo_config) autoPercentyProductsDB = AutoPercentyProductsDB(mongo_config)
# autoPercentyProductsDB.reset_product_by_id('660d636271e50111cf71284e') autoPercentyProductsDB.reset_product_by_id('660d636271e50111cf71284e')
# autoPercentyProductsDB.reset_product_by_id('660d635371e50111cf712842') autoPercentyProductsDB.reset_product_by_id('660d635371e50111cf712842')
try: try:
if login_info['per_mode']: if login_info['per_mode']:
@ -197,7 +198,7 @@ def modify_products(driver, gemini, translator, mongo_config, login_info, set_nu
steps_and_actions = [ steps_and_actions = [
('tag_modification', edit_tag, (driver, product_infos[i-1])), ('tag_modification', edit_tag, (driver, product_infos[i-1])),
('thumbnail_modification', modify_thumb_page, (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], gemini, translator, login_info)), ('option_modification', modify_option_page, (driver, product_infos[i-1], translator, login_info)),
('detail_page_modification', modify_detail_page, (driver, product_infos[i-1], gemini, translator, delv_collection, json_naver_codes, 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])), ('price_modification', modify_price_page, (driver, product_infos[i-1])),
('title_modification', modify_product_title, (driver, product_infos[i-1], login_info)), ('title_modification', modify_product_title, (driver, product_infos[i-1], login_info)),

File diff suppressed because one or more lines are too long