많은 수정

This commit is contained in:
R5600U_PC 2024-10-19 00:15:09 +09:00
parent 771dcd32b6
commit ea89b50a7e
19 changed files with 44935 additions and 27630 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -231,53 +231,57 @@ class BrowserController:
return 0
# async def get_product_name(self, index, selector='xpath'):
# def fetch_image_urls_ori(self, html_content):
# """
# 상품명을 수집하는 메서드
# index : 상품명을 수집하는 인덱스
# selector : 수집방법 (css 또는 xpath)
# HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출하고 중복 제거.
# """
# try:
# # config.ini에서 설정된 선택자에 인덱스를 적용하여 가져옴
# # product_name_selector = self.product_name_template.format(index=index)
# # self.logger.debug(f"사용된 선택자: {product_name_selector}") # 선택자 출력
# soup = BeautifulSoup(html_content, 'html.parser')
# product_name_xpath_selector = self.product_name_template_xpath.format(index=index)
# self.logger.debug(f"사용된 선택자: {product_name_xpath_selector}") # 선택자 출력
# # 순서를 유지하면서 중복을 제거하기 위해 리스트 사용
# image_urls = []
# seen_urls = set()
# # <figure class="image"> 내부의 모든 <img> 태그 찾기
# figures = soup.find_all('figure', class_='image')
# for figure in figures:
# img_tag = figure.find('img')
# if img_tag and 'src' in img_tag.attrs:
# url = img_tag['src']
# if url not in seen_urls:
# image_urls.append(url)
# seen_urls.add(url) # 중복 방지
# # XPath 기반으로 요소 검색
# product_name_element = await self.page.locator(f"xpath={product_name_xpath_selector}").element_handle()
# # product_name_element = await self.page.query_selector(product_name_selector)
# # product_name_element가 None인지 확인
# if product_name_element is None:
# self.logger.error(f"상품명 요소를 찾을 수 없습니다: index {index}")
# return "수집 오류 발생"
# # 요소가 존재할 경우, inner_text 수집
# product_name = await product_name_element.inner_text()
# return product_name.strip()
# except Exception as e:
# self.logger.error(f"상품명 수집 중 오류: {e}")
# return "수집 오류 발생"
# # class="image_resized"를 가진 모든 <img> 태그 찾기
# images_resized = soup.find_all('img', class_='image_resized')
# for img in images_resized:
# if img and 'src' in img.attrs:
# url = img['src']
# if url not in seen_urls:
# image_urls.append(url)
# seen_urls.add(url) # 중복 방지
# return image_urls
def fetch_image_urls(self, html_content):
"""
HTML 콘텐츠에서 모든 <img> 태그의 URL을 HTML 소스의 순서대로 추출.
HTML 콘텐츠에서 모든 <img> 태그의 URL을 추출하는 함수.
<figure> 안의 <img> 태그와 독립된 <img> 태그 모두 처리.
"""
soup = BeautifulSoup(html_content, 'html.parser')
# HTML 소스에서 순서대로 <img> 태그를 찾음
# 모든 <img> 태그를 찾기
image_urls = []
img_tags = soup.find_all('img')
for img in img_tags:
if img and 'src' in img.attrs:
image_urls.append(img['src']) # URL을 리스트에 추가
# img 태그에서 src 속성 추출
if 'src' in img.attrs:
image_url = img['src']
image_urls.append(image_url)
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 갯수 : {len(image_urls)}")
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 목록 : {image_urls}")
return image_urls
async def close_ad_if_exists(self):
@ -285,7 +289,7 @@ class BrowserController:
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
try:
# 광고 다이얼로그가 나타날 때까지 기다림
await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=3000)
await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=10000, state='visible')
self.logger.debug("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
# 닫기 버튼 클릭
@ -503,43 +507,33 @@ class BrowserController:
# await input_field.press('Enter') # 엔터 키를 입력하여 줄바꿈
# 마크다운 형식
md_prifix = "#### "
# 첫 번째 옵션에만 - 기호를 붙여 목록 시작
await input_field.type(f"{md_prifix}")
await input_field.type(f"- 1. {option_data[0]}")
# 첫 번째 옵션의 번역된 옵션명만 입력
first_key = list(option_data.keys())[0]
first_value = option_data[first_key]
await input_field.type(f"- 1. {first_value}")
await input_field.press('Enter') # 첫 번째 옵션 이후 엔터로 줄바꿈
# 나머지 옵션들은 - 없이 숫자 접두사로 표시
for i, option in enumerate(option_data[1:], start=2):
option_text = option[0] if isinstance(option, tuple) else option
# option_prefix = f"{i}. "
option_prefix = ""
await input_field.type(f"{md_prifix}")
await input_field.type(option_prefix + option_text)
# 나머지 옵션도 번역된 옵션명만 입력
for i, (key, value) in enumerate(list(option_data.items())[1:], start=2):
await input_field.type(f"{i}. {value}") # 옵션 번호와 번역된 옵션명만 입력
await input_field.press('Enter') # 엔터 키를 입력하여 줄바꿈
# 목록 끝을 알리기 위해 엔터 두 번 입력
await input_field.press('Enter')
await input_field.press('Enter')
# 목록 끝을 알리기 위해 엔터 두 번 입력
await input_field.press('Enter')
await input_field.press('Enter')
await input_field.press('Enter')
# 후두부 텍스트 입력
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
await input_field.press('Enter')
await input_field.type('---')
await input_field.press('Enter')
# 후두부 텍스트 입력
await input_field.type('---')
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
await input_field.press('Enter')
await input_field.type('---')
await input_field.press('Enter')
self.logger.debug('옵션 데이터 입력 완료 후 엔터 입력')
self.logger.debug('옵션 데이터 입력 완료 후 엔터 입력')
return image_urls
except Exception as e:
self.logger.debug(f"이미지 URL 추출 중 오류: {e}", exc_info=True)
return []
self.logger.debug(f"이미지 URL 추출 & 옵션데이터 입력 처리 중 오류: {e}", exc_info=True)
return image_urls if image_urls else []
def paste_image_in_chrome(self, clipboardImageManager, url):
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""

View File

@ -144,7 +144,7 @@ class ClipboardImageManager:
self.logger.debug("유효하지 않은 Base64 이미지 데이터입니다.")
return None
async def download_image_from_url(self, url, max_retries=3):
def download_image_from_url(self, url, max_retries=3):
"""URL에서 이미지를 다운로드하고 PIL 이미지 객체로 반환"""
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",

View File

@ -7,7 +7,8 @@ oversea_shipping_locator = '//*[@id='productMainContentContainerId']/div/div[1]/
option_count_text_locator = 'div#productMainContentContainerId th:nth-child(2) > div > span'
product_cost_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[3]/div/div/div/div[2]/input'
standard_selling_price_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[4]/div/div/div[1]/div/div[2]/input'
product_cost_for_single_locator = '//*[@id="productMainContentContainerId"]/div/div[2]/div/div/div[2]/div[1]/div/div/div/div/div[2]/table/tbody/tr[2]/td[2]/div/div/div/div[2]/input'
standard_selling_price_for_single_locator = '//*[@id="productMainContentContainerId"]/div/div[2]/div/div/div[2]/div[1]/div/div/div/div/div[2]/table/tbody/tr[2]/td[3]/div/div/div[1]/div/div[2]/input'
[OptionLocators]
# 옵션 관련 선택자
@ -23,8 +24,10 @@ ai_option_btn_selector = 'button:has-text("AI 옵션명 다듬기")'
original_name_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span'
edit_field_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input'
checkbox_selector_template = '#productMainContentContainerId li:nth-child({index}) input[type="checkbox"]'
image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
price_selector_template = '#productMainContentContainerId li:nth-child({index}) sup'
; image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
image_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > img'
; price_selector_template = '#productMainContentContainerId li:nth-child({index}) sup'
price_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[3]/div[1]/div[2]/button/span/sup'
delete_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({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'
confirm_delete_button_locator = 'body > div:nth-child(18) > div > div.ant-modal-wrap.ant-modal-confirm-centered.ant-modal-centered > div > div.sc-ddjGPC.jbwEYW > div > div > div > div.ant-modal-confirm-btns > button.ant-btn.css-1li46mu.ant-btn-primary.ant-btn-dangerous'
add_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img'
@ -97,7 +100,8 @@ staff_login_button_locator = 'button:has-text("직원 로그인 하기")'
admin_toggle_locator = 'button[role="switch"]'
# 광고 다이얼로그 관련 선택자
close_ad_dialog_locator = 'div.ant-modal-wrap.ant-modal-centered'
; close_ad_dialog_locator = 'div.ant-modal-wrap.ant-modal-centered'
close_ad_dialog_locator = 'div[role="dialog"]'
close_ad_button_locator = 'div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default'
# 상품 관련 선택자

11
gui.py
View File

@ -56,7 +56,7 @@ class TranslationApp(QWidget):
self.cmb_diag = CMBSettingsDialog(parent=self, logger=self.logger, db_manager=self.db_manager, initial_db_path=self.initial_db_path, user_db_path=self.user_db_path, debug=self.debug)
self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, self.debug)
self.optionHandler = OptionHandler(self.locator_manager, self.browser_controller, self.whale_translator, self.logger, self.vertexAI, self.debug)
self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.vertexAI, self.cmb_diag, self.debug)
self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.optionHandler, self.vertexAI, self.cmb_diag, self.debug)
self.titleHandler = TitleHandler(self.locator_manager, self.browser_controller, self.logger)
self.running = False
@ -844,10 +844,13 @@ class TranslationApp(QWidget):
break
self.logger.debug(f"웨일 브라우저를 활용한 이미지 번역 프로세스")
self.whale_translator.translate_image(url)
is_success_translated = self.whale_translator.translate_image(url)
self.logger.debug(f"paste_image_in_chrome - 이미지 붙여넣기")
self.browser_controller.paste_image_in_chrome(self.clipboardImageManager, url)
if is_success_translated:
self.logger.debug(f"paste_image_in_chrome - 이미지 붙여넣기")
self.browser_controller.paste_image_in_chrome(self.clipboardImageManager, url)
else:
self.logger.debug(f"{url} 이미지 번역 실패")
self.logger.debug(f"Progress Update")
self.update_detail_progress(i,total_images)

View File

@ -25,6 +25,8 @@ class LocatorManager:
'option_count_text_locator': self.config.get('PriceLocators', 'option_count_text_locator').strip("'"),
'product_cost_locator': self.config.get('PriceLocators', 'product_cost_locator').strip("'"),
'standard_selling_price_locator': self.config.get('PriceLocators', 'standard_selling_price_locator').strip("'"),
'product_cost_for_single_locator': self.config.get('PriceLocators', 'product_cost_for_single_locator').strip("'"),
'standard_selling_price_for_single_locator': self.config.get('PriceLocators', 'standard_selling_price_for_single_locator').strip("'"),
}
# BrowserControl 섹션

View File

@ -13,6 +13,8 @@ class OptionHandler:
self.debug_flag = debug_flag
self.vertexAItranslator = vertexAI
self.whale_translator = whale_translator
self.is_percenty_success = False
self.is_vertext_success = False
self.init_option_info()
# 선택자 로드
@ -120,10 +122,14 @@ class OptionHandler:
async def store_selected_options(self):
"""현재 페이지에서 선택된 옵션을 수집하여, 가격 낮은 순으로 정렬한 후 클래스 변수에 저장"""
try:
selected_translated_options = []
selected_translated_options = {}
total_options_count = len(self.option_info['original_names'])
# checked_states 딕셔너리에서 값이 True인 항목의 개수를 계산
checked_count = sum(value is True for value in self.option_info['checked_states'].values())
self.logger.debug(f"체크된 옵션 수: {checked_count}")
await self.low_order_click()
for i in range(1, total_options_count + 1):
@ -135,10 +141,7 @@ class OptionHandler:
option_input_element = await self.page.query_selector(option_input_selector)
if option_input_element:
option_name_value = (await option_input_element.get_attribute('value')).strip()
selected_translated_options.append(
# (option_name_value, self.option_info['prices'].get(option_name_value, {}).get('low_price', 0))
(option_name_value)
)
selected_translated_options[option_name_value] = self.option_info['prices'].get(option_name_value, {}).get('low_price', 0)
self.option_info['selected_translated_options'] = selected_translated_options
self.logger.debug(f"선택된 옵션 저장 완료: {selected_translated_options}")
@ -182,7 +185,7 @@ class OptionHandler:
# 2. 전체 옵션 체크박스 상태 확인
click_to_check_to_all = True
self.logger.debug(f"일부 체크된 옵션상품에 대한 처리 방법 : {"전체체크에서 시작" if click_to_check_to_all else "일부 체크시 수정완료로 판단"}")
self.logger.debug(f"일부 체크된 옵션상품에 대한 처리 방법 : {'전체체크에서 시작' if click_to_check_to_all else '일부 체크시 수정완료로 판단'}")
if click_to_check_to_all:
self.logger.debug("옵션이 일부만 체크된 상태입니다. 전체 체크로 바꿉니다.")
@ -202,15 +205,13 @@ class OptionHandler:
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
self.option_info = await self.collect_options_info()
translation_success = False # 성공/실패 플래그
try:
# Vertex AI를 통한 번역 시도
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
self.logger.debug(f"번역된 옵션 입력")
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
translation_success = True # 번역 성공
self.is_vertext_success = True # 번역 성공
except ValueError as ve:
# 안전 필터 예외 처리
@ -220,7 +221,8 @@ class OptionHandler:
await self.page.click(self.ai_option_btn_selector)
self.logger.debug("번역을 위한 5초간 대기")
await asyncio.sleep(5)
translation_success = False # 번역 실패
self.is_vertext_success = False
self.is_percenty_success = True
# except google.api_core.exceptions.ResourceExhausted as re:
# # 할당량 초과 예외 처리
@ -238,10 +240,11 @@ class OptionHandler:
await self.page.click(self.ai_option_btn_selector)
self.logger.debug("번역을 위한 5초간 대기")
await asyncio.sleep(5)
translation_success = False # 번역 실패
self.is_vertext_success = False # 번역 실패
self.is_percenty_success = True
# 번역 성공 여부에 따른 로그
self.logger.debug(f"[{'VertexAI' if translation_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
self.logger.debug(f"[{'VertexAI' if self.is_vertext_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
except Exception as e:
# 옵션 처리 중 오류 발생 시 전체 로그 출력
@ -252,9 +255,10 @@ class OptionHandler:
self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")
await self.filter_and_adjust_options(max_option_count)
# 6. 선택된 옵션 재수집
self.logger.debug(f"옵션 필터링 및 조정")
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
# 6. 선택된 옵션정보 재수집
if self.is_percenty_success:
self.logger.debug(f"퍼센티 옵션번역으로 인해 선택된 옵션정보 재수집")
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
# 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
if toggle_states['optionIMGTrans']:
@ -378,6 +382,12 @@ class OptionHandler:
elements = await asyncio.gather(*tasks)
original_name_element, edit_field_element, checkbox_element, image_element, price_element = elements
self.logger.debug(f"{i}번째 original_name_element : {original_name_element}")
self.logger.debug(f"{i}번째 edit_field_element : {edit_field_element}")
self.logger.debug(f"{i}번째 checkbox_element : {checkbox_element}")
self.logger.debug(f"{i}번째 image_element : {image_element}")
self.logger.debug(f"{i}번째 price_element : {price_element}")
if original_name_element:
original_name = await original_name_element.inner_text()
self.option_info['original_names'][f'origin_option_{i}'] = original_name
@ -386,6 +396,7 @@ class OptionHandler:
# 체크박스 상태 수집 (체크 여부는 클래스 이름으로 판단)
checkbox_state = None
if checkbox_element:
checkbox_classes = await checkbox_element.get_attribute('class')
if 'ant-checkbox-checked' in checkbox_classes:

View File

@ -7,9 +7,10 @@ import math , re, time
from playwright.async_api import TimeoutError
class PriceHandler:
def __init__(self, locator_manager, browser_controller, logger, vertexAI, cmb_diag, debug_flag=False):
def __init__(self, locator_manager, browser_controller, logger, optionHandler,vertexAI, cmb_diag, debug_flag=False):
self.locator_manager = locator_manager
self.browser_controller = browser_controller
self.optionHandler = optionHandler
self.page = self.browser_controller.page
self.logger = logger
self.debug_flag = debug_flag
@ -32,6 +33,8 @@ class PriceHandler:
# Locator들을 미리 로드하여 초기화 - locator_template:동적 로딩, locator:고정로딩
self.product_cost_locator_template = self.locator_manager.get_locator('PriceLocators', 'product_cost_locator')
self.standard_selling_price_locator_template = self.locator_manager.get_locator('PriceLocators', 'standard_selling_price_locator')
self.product_cost_for_single_locator = self.locator_manager.get_locator('PriceLocators', 'product_cost_for_single_locator')
self.standard_selling_price_for_single_locator = self.locator_manager.get_locator('PriceLocators', 'standard_selling_price_for_single_locator')
self.plus_margin_locator = self.locator_manager.get_locator('PriceLocators', 'plus_margin_locator')
self.oversea_shipping_locator = self.locator_manager.get_locator('PriceLocators', 'oversea_shipping_locator')
self.return_fee_input_locator = self.locator_manager.get_locator('PriceLocators', 'return_fee_input_locator')
@ -87,7 +90,10 @@ class PriceHandler:
self.logger.debug(f"더하기마진값{initial_plusmargin}을 팔린가격{sold_price}으로 간주")
self.logger.debug("옵션 가격 정보를 수집합니다.")
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = await self.collect_product_costs_and_prices() # 수집된 옵션정보를 반환
is_single = self.optionHandler.option_info['is_single_option']
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = await self.collect_product_costs_and_prices(is_single) # 수집된 옵션정보를 반환
if option_data is None:
self.logger.error("상품 옵션 정보를 수집하지 못했습니다.", exc_info=True)
return
@ -533,7 +539,7 @@ class PriceHandler:
try:
# 요소를 기다림
option_count_text_element = await self.page.wait_for_selector(self.option_count_text_locator)
option_count_text_element = await self.page.wait_for_selector(self.option_count_text_locator, timeout=5000)
# 텍스트에서 숫자만 추출
option_text = await option_count_text_element.text_content()
@ -556,7 +562,7 @@ class PriceHandler:
return 1 # 예외 발생 시 기본값 1 반환
async def collect_product_costs_and_prices(self):
async def collect_product_costs_and_prices(self, is_single):
"""
상품 원가와 판매가를 수집하여 반환합니다. 단위는 위안화
@ -572,15 +578,25 @@ class PriceHandler:
"""
try:
# 옵션 개수 가져오기 (두 가지 방법 중 하나 사용)
total_options = await self.get_option_count_from_text()
if is_single:
total_options = 1
else:
total_options = await self.get_option_count_from_text()
self.product_costs = [] # 모든 옵션의 상품원가를 저장할 리스트
self.option_data = [] # 각 옵션의 상품원가와 기준판매가를 저장할 리스트
self.logger.debug(f"collect_product_costs_and_prices 단일옵션 : {is_single}")
# 옵션 개수만큼 순회하면서 값을 수집
for i in range(1, total_options + 1):
product_cost_locator = self.product_cost_locator_template.format(index=i+1)
standard_selling_price_locator = self.standard_selling_price_locator_template.format(index=i+1)
if is_single:
product_cost_locator = self.product_cost_for_single_locator
standard_selling_price_locator = self.standard_selling_price_for_single_locator
else:
product_cost_locator = self.product_cost_locator_template.format(index=i+1)
standard_selling_price_locator = self.standard_selling_price_locator_template.format(index=i+1)
# 각 선택자를 사용하여 요소 찾기
product_cost_element = await self.page.wait_for_selector(product_cost_locator)

41
test/html_test.py Normal file

File diff suppressed because one or more lines are too long

View File

@ -42,6 +42,8 @@ class WhaleTranslator:
pyautogui.typewrite(url)
pyautogui.press('enter')
time.sleep(2)
size = self.get_whale_window_title()
# # 페이지 로딩 완료 대기
# self.wait_for_loading_icon_to_disappear()
@ -144,6 +146,37 @@ class WhaleTranslator:
self.whale_rect = win32gui.GetWindowRect(self.whale_hwnd)
self.logger.debug(f"웨일 창 크기 및 위치 저장: {self.whale_rect}")
def get_whale_window_title(self):
"""
현재 활성화된 웨일 창의 이름을 가져옵니다.
제목에서 이미지의 해상도를 확인한 , 일정 크기 이하인 경우 작업을 패스하도록 처리합니다.
"""
# hwnd = self.find_whale_window() # 웨일 창 핸들 가져오기
if self.whale_hwnd:
window_title = win32gui.GetWindowText(self.whale_hwnd)
self.logger.debug(f"현재 웨일 창의 제목: {window_title}")
# 해상도를 추출하기 위해 제목에서 괄호 안의 (숫자×숫자) 부분을 찾음
import re
match = re.search(r"\((\d+)×(\d+)\)", window_title)
if match:
width = int(match.group(1))
height = int(match.group(2))
self.logger.debug(f"이미지 해상도: {width}×{height}")
# 해상도가 기준보다 작은 경우 패스
if width < 300 or height < 200:
self.logger.debug(f"이미지 해상도가 너무 작습니다. ({width}×{height}), 작업을 패스합니다.")
return False # 작업을 수행하지 않음
return True # 작업을 계속 진행
else:
self.logger.error("이미지 해상도를 가져오지 못했습니다.")
return False
self.logger.error("웨일 창을 찾을 수 없습니다.")
return False
def find_whale_window(self):
"""웨일 창을 제목을 기준으로 찾는 메서드"""
def callback(hwnd, extra):
@ -174,6 +207,8 @@ class WhaleTranslator:
if self.whale_rect:
center_x = (self.whale_rect[0] + self.whale_rect[2]) // 2 # 가로 중앙 계산
center_y = (self.whale_rect[1] + self.whale_rect[3]) // 2 # 세로 중앙 계산
center_y = center_y + 50
pyautogui.moveTo(center_x, center_y)
self.logger.debug(f"마우스 커서를 창 중앙으로 이동: ({center_x}, {center_y})")
else:
@ -255,6 +290,7 @@ logger = logging.getLogger(__name__)
translator = WhaleTranslator(logger, ['fail_translated1.png', 'fail_translated2.png'])
# 브라우저 실행 후 URL로 이동 및 번역 성공 여부 확인
translator.start_whale_browser('https://img.alicdn.com/imgextra/i3/350475995/O1CN01uFwQ9v1u9kwOuU78C-350475995.png_Q75.jpg')
translator.start_whale_browser("https://file.percenty.co.kr/public/652bed8e865b1f32ea62bf1f/products/66ff967773994c46d388bb36/82d07178-ae60-49f7-a489-e02801ff7b06.jpg")
translator.start_whale_browser('https://img.alicdn.com/imgextra/i4/735691568/O1CN01sRUYqb1NSBuefMBlw_!!735691568.jpg_Q75.jpg')
translator.start_whale_browser('https://img.alicdn.com/imgextra/i4/1773313923/O1CN01VMRs1Z1eqmfYSXQDu_!!1773313923.jpg_Q75.jpg')

View File

@ -4,7 +4,7 @@ import time
import win32gui, win32con, win32process
from pyvda import VirtualDesktop, get_virtual_desktops
import subprocess
import asyncio
import pyscreeze
import KO_EN
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
from PIL import ImageGrab
@ -16,6 +16,7 @@ class WhaleTranslator:
self.vd_mode = vd_mode
self.newtab = "about:newtab"
self.whale_pid = None
self.whale_rect = None
isSecret = secret_mode
self.pixel_check_interval = pixel_check_interval
@ -30,6 +31,7 @@ class WhaleTranslator:
error_image_filenames = ['fail_translated1.png', 'fail_translated2.png']
self.error_image_paths = [os.path.join(img_dir, filename) for filename in error_image_filenames]
self.page_loading_icon_path = os.path.join(img_dir, 'page_loading.png')
self.translating_image_path = os.path.join(img_dir, 'translating.png')
self.translation_success_flag = False # 번역 성공 플래그
self.failure_count = 0 # 실패 횟수
@ -75,7 +77,8 @@ class WhaleTranslator:
# win32gui.SetWindowPos(self.whale_hwnd, None, 0, 0, 1920, 1080, win32con.SWP_NOZORDER)
# self.logger.debug("Whale 창 크기 조절 완료")
self.set_window_position(self.whale_hwnd, 100, 100, 1280, 720) # 위치 (100, 100), 크기 (1280x720)
self.set_window_position(self.whale_hwnd, 1, 1, 1280, 720) # 위치 (1, 1), 크기 (1280x720)
self.update_whale_rect()
# 주소창으로 이동 후 URL 입력
pyautogui.hotkey('ctrl', 'l')
@ -132,10 +135,29 @@ class WhaleTranslator:
return False
def find_whale_window(self):
"""웨일 창 핸들을 찾는 메서드"""
if not self.whale_hwnd:
self.whale_hwnd = self.find_window_by_title(self.whale_window_name)
return self.whale_hwnd
"""웨일 창을 제목을 기준으로 찾는 메서드"""
def callback(hwnd, extra):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
if self.whale_window_name in title:
extra.append(hwnd)
hwnd_list = []
win32gui.EnumWindows(callback, hwnd_list)
if hwnd_list:
self.whale_hwnd = hwnd_list[0]
self.logger.debug(f"웨일 창을 찾았습니다: {self.whale_hwnd}")
self.update_whale_rect()
return self.whale_hwnd
else:
self.logger.debug("웨일 창을 찾지 못했습니다.")
return None
def update_whale_rect(self):
"""웨일 창의 위치 및 크기 rect를 업데이트"""
if self.whale_hwnd:
self.whale_rect = win32gui.GetWindowRect(self.whale_hwnd)
self.logger.debug(f"웨일 창 크기 및 위치 저장: {self.whale_rect}")
def find_window_by_title(self, window_name):
def enum_windows_callback(hwnd, result):
@ -253,15 +275,24 @@ class WhaleTranslator:
self.logger.debug(f"이미지 URL 주소 입력")
self.enter_url(url)
pyautogui.press('enter')
time.sleep(1) # 페이지 로딩 대기
time.sleep(2) # 페이지 로딩 대기
# 현재 웨일 창의 해상도를 확인하여 기준 이하일 경우 패스
min_width = 200
min_height = 150
if not self.get_whale_window_title(min_width=min_width,min_height=min_height):
self.logger.debug("해상도가 기준보다 낮아 작업을 패스합니다.")
return False # 해상도 기준 미달로 작업 종료
# 페이지 로딩 완료 대기
self.logger.debug(f"페이지 로딩완료 대기")
self.wait_for_loading_icon_to_disappear()
# self.logger.debug(f"페이지 로딩완료 대기")
# self.wait_for_loading_icon_to_disappear()
self.logger.debug(f"페이지 로딩 완료 후 웨일 창의 가운데로 마우스 커서 이동")
# pyautogui.moveTo(960,580) # 마우스 센터로 이동
self.move_mouse_to_center(self.whale_hwnd)
self.move_mouse_to_center()
time.sleep(0.5) # 마우스 이동 후 대기
original_color = self.get_mouse_position_color() # 현재 색상 가져오기
@ -271,10 +302,10 @@ class WhaleTranslator:
self.logger.debug("번역 작업을 위한 마우스 오른쪽 클릭 및 R 전송")
if not self.right_click_and_send_key('r'):
self.logger.error("번역 작업이 대화상자 문제로 중단되었습니다.")
return
return False
# 번역 성공 여부 확인
result = self.check_translation_by_color_change(original_color)
result = self.check_translation_by_color_change()
if result == "success":
self.logger.debug("번역이 성공적으로 완료되었습니다!")
elif result == "error":
@ -290,7 +321,7 @@ class WhaleTranslator:
self.logger.debug("이미지 복사를 위한 마우스 오른쪽 클릭 및 C 전송")
if not self.right_click_and_send_key('c'):
self.logger.error("복사 작업이 대화상자 문제로 중단되었습니다.")
return
return False
self.logger.debug(f"클립보드에 번역된이미지 복사 대기 1s")
time.sleep(1) # 클립보드 업데이트 대기
@ -316,11 +347,15 @@ class WhaleTranslator:
pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드
# path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨
return True
except Exception as e:
self.logger.error(f"번역 중 오류 발생: {e}", exc_info=True)
return False
# self.handle_translation_failure()
else:
self.logger.debug('웨일 창을 찾을 수 없습니다.')
return False
# self.handle_translation_failure()
def detect_unexpected_dialog(self):
@ -482,68 +517,180 @@ class WhaleTranslator:
except Exception as e:
self.logger.debug(f"가상 데스크톱 종료 중 오류 발생: {e}", exc_info=True)
def wait_for_loading_icon_to_disappear(self, max_wait=10):
"""
로딩 아이콘이 화면에서 사라질 때까지 대기합니다.
max_wait: 최대 대기 시간 ()
"""
start_time = time.time()
while time.time() - start_time < max_wait:
try:
# 화면에서 아이콘 위치 확인 시도
icon_location = pyautogui.locateOnScreen(self.page_loading_icon_path, confidence=0.9)
if icon_location:
self.logger.debug("페이지 로딩 중...")
time.sleep(0.5) # 간격을 두고 다시 확인
# def wait_for_loading_icon_to_disappear(self, max_wait=10):
# """
# 로딩 아이콘이 화면에서 사라질 때까지 대기합니다.
# max_wait: 최대 대기 시간 (초)
# """
# start_time = time.time()
# while time.time() - start_time < max_wait:
# try:
# # 화면에서 아이콘 위치 확인 시도
# icon_location = pyautogui.locateOnScreen(self.page_loading_icon_path, confidence=0.9)
# if icon_location:
# self.logger.debug("페이지 로딩 중...")
# time.sleep(0.5) # 간격을 두고 다시 확인
except pyautogui.ImageNotFoundException:
# 아이콘이 화면에 없으면 로딩 완료로 간주
self.logger.debug("페이지 로딩이 완료되었습니다.")
return True # 로딩 완료
# except pyautogui.ImageNotFoundException:
# # 아이콘이 화면에 없으면 로딩 완료로 간주
# self.logger.debug("페이지 로딩이 완료되었습니다.")
# return True # 로딩 완료
self.logger.debug("로딩 완료 대기 시간이 초과되었습니다.")
return False
# self.logger.debug("로딩 완료 대기 시간이 초과되었습니다.")
# return False
def set_window_position(self, hwnd, x, y, width, height):
"""지정된 위치와 크기로 창을 조정"""
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
win32gui.SetWindowPos(hwnd, None, x, y, width, height, win32con.SWP_NOZORDER | win32con.SWP_NOACTIVATE)
self.logger.debug(f"창 위치 및 크기 설정: 위치({x}, {y}), 크기({width}x{height})")
def get_whale_window_title(self, min_width=200, min_height=150):
"""
현재 활성화된 웨일 창의 이름을 가져옵니다.
제목에서 이미지의 해상도를 확인한 , 일정 크기 이하인 경우 작업을 패스하도록 처리합니다.
"""
# hwnd = self.find_whale_window() # 웨일 창 핸들 가져오기
if self.whale_hwnd:
window_title = win32gui.GetWindowText(self.whale_hwnd)
self.logger.debug(f"현재 웨일 창의 제목: {window_title}")
def move_mouse_to_center(self, hwnd):
# 해상도를 추출하기 위해 제목에서 괄호 안의 (숫자×숫자) 부분을 찾음
import re
match = re.search(r"\((\d+)×(\d+)\)", window_title)
if match:
width = int(match.group(1))
height = int(match.group(2))
self.logger.debug(f"이미지 해상도: {width}×{height}")
# 해상도가 기준보다 작은 경우 패스
if width < min_width or height < min_height:
self.logger.debug(f"이미지 해상도가 너무 작습니다. ({width}×{height}), 작업을 패스합니다.")
return False # 작업을 수행하지 않음
return True # 작업을 계속 진행
else:
self.logger.error("이미지 해상도를 가져오지 못했습니다.")
return False
self.logger.error("웨일 창을 찾을 수 없습니다.")
return False
def move_mouse_to_center(self):
"""웨일 브라우저 창의 중앙으로 마우스 커서를 이동"""
rect = win32gui.GetWindowRect(hwnd) # 창 위치 및 크기 가져오기
center_x = (rect[0] + rect[2]) // 2 # 가로 중앙 계산
center_y = (rect[1] + rect[3]) // 2 # 세로 중앙 계산
pyautogui.moveTo(center_x, center_y)
self.logger.debug(f"마우스 커서를 창 중앙으로 이동: ({center_x}, {center_y})")
if self.whale_rect:
center_x = (self.whale_rect[0] + self.whale_rect[2]) // 2 # 가로 중앙 계산
center_y = (self.whale_rect[1] + self.whale_rect[3]) // 2 # 세로 중앙 계산
self.logger.debug(f"마우스 커서를 추가로 50px 내림")
center_y = center_y + 50
pyautogui.moveTo(center_x, center_y)
self.logger.debug(f"마우스 커서를 창 중앙으로 이동: ({center_x}, {center_y})")
else:
self.logger.error("웨일 창의 크기를 알 수 없습니다. 먼저 창을 찾으세요.")
def check_translation_by_color_change(self, original_color):
def check_translation_by_color_change(self):
start_time = time.time()
# 번역 시작 감지 (translating.png 확인)
while time.time() - start_time < self.timeout:
current_color = self.get_mouse_position_color()
if self.colors['during'] is None: # 번역 중 첫 색상 기록
self.colors['during'] = current_color
self.logger.debug(f"현재 색상: {current_color}")
if self.is_similar_color(current_color, original_color):
# 필터가 사라져서 색상이 변했는지 확인
if self.is_color_changed(current_color):
self.colors['after'] = current_color
self.logger.debug("번역 성공 감지 (원래 색상과 유사)")
return "success"
if not self.is_similar_color(current_color, original_color):
self.logger.debug("번역 중 상태 감지 (필터 색상)")
self.logger.debug("색상 변화 감지 (필터 제거됨)")
# translating.png가 여전히 존재하는지 확인하여 번역 성공 여부 판단
result = self.find_image_with_confidence(self.translating_image_path, confidence=0.4)
if result:
return "success"
else:
return "error"
else:
# 번역 실패 이미지 확인
if self.is_translation_failed():
self.colors['after'] = current_color
return "error"
time.sleep(self.pixel_check_interval)
# 타임아웃 발생 시, 번역 성공으로 간주
# 타임아웃 발생 시 translating.png 여부로 번역 성공/실패 판단
self.colors['after'] = current_color
self.logger.debug("번역 성공으로 간주 (타임아웃)")
return "success"
result = self.find_image_with_confidence(self.translating_image_path, confidence=0.4)
if result:
self.logger.debug("번역 성공으로 간주 (타임아웃 후 translating.png 존재)")
return "success"
else:
self.logger.debug("번역 실패로 간주 (타임아웃 후 translating.png 없음)")
return "error"
def find_image_with_confidence(self, image_path, confidence=0.8):
"""이미지를 찾을 때 highest confidence 값을 로그로 출력하는 메서드"""
if self.whale_rect:
# 웨일 창의 크기를 region으로 설정
region = (self.whale_rect[0], self.whale_rect[1],
self.whale_rect[2] - self.whale_rect[0],
self.whale_rect[3] - self.whale_rect[1])
self.logger.debug(f"이미지를 찾을 영역: {region}")
try:
# locateOnScreen 시도
result = pyscreeze.locateOnScreen(image_path, confidence=confidence, region=region)
if result:
self.logger.debug(f"이미지를 찾았습니다: {image_path}")
return result
else:
# locateOnScreen 함수가 이미지와 충분히 유사하다고 판단하지 않은 경우
highest_confidence = pyscreeze._locateAll_opencv(image_path, region)[0][1] # 최고 유사도 값 가져오기
self.logger.debug(f"최고 유사도 값: {highest_confidence}")
if highest_confidence >= confidence:
self.logger.debug(f"유사한 이미지를 찾았습니다. 최고 유사도 값: {highest_confidence}")
return True # 유사도는 충분하므로 True 반환
else:
self.logger.error(f"{image_path} 이미지를 찾지 못했습니다. 유사도: {highest_confidence}")
return None # 유사도가 충분하지 않으면 None 반환
except pyscreeze.ImageNotFoundException as e:
self.logger.error(f"{image_path} 이미지를 찾지 못했습니다. 예외 발생: {e}")
return None
# def check_translation_by_color_change_ori(self, original_color):
# start_time = time.time()
# while time.time() - start_time < self.timeout:
# current_color = self.get_mouse_position_color()
# if self.colors['during'] is None: # 번역 중 첫 색상 기록
# self.colors['during'] = current_color
# self.logger.debug(f"현재 색상: {current_color}")
# if self.is_similar_color(current_color, original_color):
# self.colors['after'] = current_color
# self.logger.debug("번역 성공 감지 (원래 색상과 유사)")
# return "success"
# if not self.is_similar_color(current_color, original_color):
# self.logger.debug("번역 중 상태 감지 (필터 색상)")
# if self.is_translation_failed():
# self.colors['after'] = current_color
# return "error"
# time.sleep(self.pixel_check_interval)
# # 타임아웃 발생 시, 번역 성공으로 간주
# self.colors['after'] = current_color
# self.logger.debug("번역 성공으로 간주 (타임아웃)")
# return "success"
def is_color_changed(self, current_color):
"""현재 색상이 변화했는지 확인 (필터 제거 여부)"""
if self.colors['during'] is None:
self.colors['during'] = current_color
return not self.is_similar_color(current_color, self.colors['during'])
def is_similar_color(self, color1, color2):
"""색상이 유사한지 확인 (허용 오차 적용)"""
@ -560,7 +707,8 @@ class WhaleTranslator:
"""번역 실패 이미지 확인"""
for image_path in self.error_image_paths:
try:
if pyautogui.locateOnScreen(image_path, confidence=0.8):
# if pyautogui.locateOnScreen(image_path, confidence=0.8):
if self.find_image_with_confidence(self.translating_image_path, confidence=0.8):
self.logger.error(f"번역 실패: '{os.path.basename(image_path)}' 메시지가 감지되었습니다.")
return True
except pyautogui.ImageNotFoundException: