diff --git a/modules/automatch_tao.py b/modules/automatch_tao.py index d354f6c..cd50ba3 100644 --- a/modules/automatch_tao.py +++ b/modules/automatch_tao.py @@ -95,7 +95,6 @@ def automatch(db, item_count, message_controller, sort_order, progress_callback= for i, product in enumerate(products): logger.debug(f"{i}번째 product 정보 DB 업데이트 준비") itemUrl, itemID, imageUrl, item_name, price, sales_volume = product - # DB에 상품 정보 업데이트 update_query = """ INSERT INTO Taobao (keyword_id, itemUrl, itemID, imageUrl, item_name, price, sales_volume) @@ -105,7 +104,7 @@ def automatch(db, item_count, message_controller, sort_order, progress_callback= cursor.execute(update_query, (keyword_id, itemUrl, itemID, imageUrl, item_name, price, sales_volume)) logger.debug(f"TaoBao 테이블에 [{product}/{len(products)}] 내용 업데이트 실행") except Exception as e: - logger.debug(f"product 정보 DB 업데이트 중 오류 발생 : {e}") + logger.debug(f"product 정보 DB 업데이트 중 오류 발생 : {e}", exc_info=True) logger.debug(f"{len(products)}개의 product 정보 DB 업데이트 완료") @@ -120,7 +119,19 @@ def automatch(db, item_count, message_controller, sort_order, progress_callback= # for index, (row, product) in enumerate(zip(group.iterrows(), products[:product_count])): for index, (row, product) in enumerate(zip(group.itertuples(index=False), products[:product_count])): - itemUrl, itemID, imageUrl, item_name, price, sales_volume = product + # itemUrl, itemID, imageUrl, item_name, price, sales_volume = product # 튜플일때 + + # 상품 정보 추출 #리스트일때 + itemUrl = product['Product URL'] + itemID = product['Tao_itemID'] + imageUrl = product['Image URL'] + item_name = product['Product Name'] + price = product['Price'] + sales_volume = product['Sales Volume'] + + if price == '' and itemID =='': + continue + if keyword_id != row.keyword_id: continue @@ -154,7 +165,7 @@ def automatch(db, item_count, message_controller, sort_order, progress_callback= # PC 주소 생성 match = re.search(r'taobao.com/i(\d{10,12})', itemUrl) pc_url = f"https://item.taobao.com/item.htm?id={match.group(1)}" if match else "" - + # NaverShopping 테이블에 매칭 정보 업데이트 update_ns_query = """ UPDATE NaverShopping diff --git a/modules/cookie_manager.py b/modules/cookie_manager.py index 9c49e37..c666b96 100644 --- a/modules/cookie_manager.py +++ b/modules/cookie_manager.py @@ -37,30 +37,25 @@ def load_cookies(driver): logger.debug(f"쿠키 로드 중 기타 에러 발생 : {e}") return False -def check_login_status_ori(driver): - # 로그인되지 않은 상태의 XPath - not_logged_in_xpath = "//li[@id='J_SiteNavLogin']/div/div/a" - # 로그인된 상태의 XPath - # logged_in_xpath = "//li[@id='J_SiteNavLogin']/div/div[2]/a" +def check_login_status_by_iframe(driver): logged_in_xpath = "/html/body/div[1]/div/ul[1]/li[2]/div[1]/div[2]/a" - # # 로그인되지 않은 상태 확인 - # if WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.XPATH, not_logged_in_xpath))): - # logger.debug("로그인되지 않았습니다. 로그인이 필요합니다.") - # return False - logger.debug("로그인된 상태 확인") - # 로그인된 상태 확인 - if WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.XPATH, logged_in_xpath))): - user_id_text = driver.find_element(By.XPATH, logged_in_xpath).text - logger.debug(f"로그인된 상태입니다. 로그인된 ID: {user_id_text}") - return True - # 로그인되지 않은 상태 확인 - elif WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.XPATH, not_logged_in_xpath))): - logger.debug("로그인되지 않았습니다. 로그인이 필요합니다.") - return False - else: - logger.debug("로그인 상태를 확인할 수 없습니다.") - return False + while True: + try: + logger.debug("로그인된 상태 확인 중...") + # 로그인된 상태 확인 + if driver.find_element(By.XPATH, logged_in_xpath): + user_id_text = driver.find_element(By.XPATH, logged_in_xpath).text + logger.debug(f"로그인된 상태입니다. 로그인된 ID: {user_id_text}") + return True + except: + # 로그인되지 않은 상태 확인 + try: + logger.debug("로그인되지 않았습니다. 로그인이 필요합니다.") + time.sleep(5) # 5초 후 다시 체크 + except: + logger.debug("로그인 상태를 확인할 수 없습니다. 잠시 후 다시 시도합니다.") + time.sleep(5) # 예외 발생 시 5초 대기 후 재시도 def check_login_status(driver): diff --git a/modules/tao_parser.py b/modules/tao_parser.py index 5de9a08..97368f9 100644 --- a/modules/tao_parser.py +++ b/modules/tao_parser.py @@ -27,7 +27,7 @@ from io import BytesIO from translatepy import Translator from urllib.request import urlretrieve -from modules.cookie_manager import load_cookies, save_cookies, check_login_status +from modules.cookie_manager import * from modules.crop_n_rotate import * # from modules.compare_with_cv2 import compare_images @@ -35,6 +35,7 @@ import logging # 로거 인스턴스 가져오기 logger = logging.getLogger('default_logger') +MAX_IMAGE_MODIFICATIONS = 3 # 최대 이미지 수정 횟수 def convert_price(price_str): logger.debug(f"convert_price : {price_str}") @@ -60,28 +61,34 @@ def extract_item_id(url): match = re.search(r'taobao.com/i(\d{10,12})', url) return match.group(1) if match else None -def fetch_products(souped, item_count, products, similarity_threshold): - for i, product in enumerate(souped, start=1): - logger.debug(f"souped product {i} \n {souped}") +def fetch_products(souped, item_count, similarity_threshold): + fetched_products = [] + + if not souped: # HTML 파싱 결과가 비어 있는 경우 + logger.debug("HTML 파싱 결과가 비어 있습니다.") + return [] + + for i, souped_element in enumerate(souped, start=1): + logger.debug(f"souped product 진행률 : [{i}]/[{len(souped)}]") if i > item_count: # 설정한 아이템 갯수에 도달하면 반복 중단 break try: - product_url = product['href'] + product_url = souped_element['href'] logger.debug(f"타오바오 상품 product_url : {product_url}") Tao_itemID = extract_item_id(product_url) logger.debug(f"타오바오 상품 extract_item_id : {Tao_itemID}") - image_url = 'https:' + product.select_one("img")['src'] + image_url = 'https:' + souped_element.select_one("img")['src'] logger.debug(f"타오바오 상품 image_url : {image_url}") - product_name = product.select_one("span.mobile--summary--2mK9e7G").text + product_name = souped_element.select_one("span.mobile--summary--2mK9e7G").text logger.debug(f"타오바오 상품 product_name : {product_name}") #trans_product_name = trans(product_name) trans_product_name = translate_texts_translatepy(product_name) trans_product_name = str(trans_product_name) logger.debug(f"타오바오 상품 trans_product_name : {trans_product_name}") - price_str = product.select_one("span.mobile--price--3eMQ3ec").text + price_str = souped_element.select_one("span.mobile--price--3eMQ3ec").text logger.debug(f"타오바오 상품 price_str : {price_str}") price = convert_price(price_str) - sales_volume_str = product.select_one("span.mobile--buy--2I4hwR4").text + sales_volume_str = souped_element.select_one("span.mobile--buy--2I4hwR4").text logger.debug(f"타오바오 상품 sales_volume_str : {sales_volume_str}") sales_volume = convert_sales_volume(sales_volume_str) logger.debug(f"타오바오 상품 sales_volume : {sales_volume}") @@ -101,24 +108,25 @@ def fetch_products(souped, item_count, products, similarity_threshold): # time.sleep(wait) # 또는 더 긴 시간 # logger.debug(f"요청간 TimeSleep : {wait}") - if difference <= similarity_threshold: logger.debug(f"상품 [{Tao_itemID}]의 상품정보 추가") product_info = { - "Product Name": trans_product_name, - "Image URL": image_url, - "Price": price, - "Sales Volume": sales_volume, "Product URL": product_url, "Tao_itemID": Tao_itemID, + "Image URL": image_url, + "Product Name": trans_product_name, + "Price": price, + "Sales Volume": sales_volume, } else: - logger.debug(f"상품 [{Tao_itemID}]의 상품이미지가 일치하지 않아 제외처리") + logger.warning(f"상품 [{Tao_itemID}]의 상품이미지가 일치하지 않아 제외처리") - products.append(product_info) + fetched_products.append(product_info) except Exception as e: - logger.debug(f"상품정보 추출 오류 발생 {i}: {e}") - + logger.error(f"상품정보 추출 오류 발생 {i}: {e}", exc_info=True) + continue + + return fetched_products def translate_texts_translatepy(text, src_lang='zh-cn', dest_lang='ko'): @@ -131,7 +139,7 @@ def translate_texts_translatepy(text, src_lang='zh-cn', dest_lang='ko'): translation = str(result) return translation except Exception as e: - logger.debug(f"번역 중 오류발생 : {e}") + logger.debug(f"번역 중 오류발생 : {e}", exc_info=True) def trans(text): @@ -146,7 +154,7 @@ def trans(text): return translated.text except Exception as e: - logger.debug(f"번역 중 오류발생 : {e}") + logger.debug(f"번역 중 오류발생 : {e}", exc_info=True) def check_first_product_tao(driver): try: @@ -157,105 +165,372 @@ def check_first_product_tao(driver): except TimeoutException: logger.debug("오류 : 첫번째 상품을 찾을 수 없습니다.") return False - -def handle_captcha_for_tao(driver, message_controller): - captcha_iframe_id = 'baxia-dialog-content' + +def handle_baxiaFrame(driver, defaultFrameName, message_controller): last_message_time = time.time() - + baxia_iframe_id = 'baxia-dialog-content' + scratch_captcha_class = 'scratch-captcha-question' nocaptcha_class = 'nc_1_nocaptcha' + login_class = 'login-box loading' + + captcha_resolved = False try: - WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, captcha_iframe_id))) - logger.debug("CAPTCHA 화면 발생. 진행을 위해 해결하세요") + WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.ID, baxia_iframe_id))) + # driver.switch_to.frame(baxia_iframe_id) + logger.debug("baxia_Frame 화면 발생. 타입에 따른 해결 시도") + switch_to_frame(driver, defaultFrameName, baxia_iframe_id) + # WebDriverWait(driver, 10).until( + # EC.frame_to_be_available_and_switch_to_it(baxia_iframe_id)) + # driver.switch_to.frame(baxia_iframe_id) + logger.debug(f"baxia_Frame 에서 {baxia_iframe_id}로 전환") - # iframe으로 포커스 전환 - driver.switch_to.frame(captcha_iframe_id) - - # 캡차 유형 식별 + # 스크래치 캡차 처리 if driver.find_elements(By.CLASS_NAME, scratch_captcha_class): - message_controller.send_message("scratch_captcha 발견: 처리를 기다리고 있습니다.") - handle_scratch_captcha(driver) # Scratch captcha 처리를 위한 별도의 함수 + logger.debug("baxia_Frame 에서 [Scratch Captcha] 타입 발생 감지") + time.sleep(1) + try: + captcha_resolved = handle_scratch_captcha(driver, defaultFrameName, message_controller) + except Exception as e: + logger.error(f"handle_scratch_captcha 메서드 호출 중 에러 발생 : {e}", exc_info=True) + logger.debug(f"(baxia_Frame) [Scratch Captcha] 처리결과 : [{captcha_resolved}]") + # 슬라이더 캡차 처리 elif driver.find_elements(By.CLASS_NAME, nocaptcha_class): - handle_nocaptcha(driver, message_controller, last_message_time) # Nocaptcha 처리를 위한 별도의 함수 + logger.debug("baxia_Frame 에서 [Sliding Captcha] 타입 발생 감지") + time.sleep(1) + try: + captcha_resolved = handle_sliding_captcha(driver, defaultFrameName, message_controller, last_message_time) + except Exception as e: + logger.error(f"handle_sliding_captcha 메서드 호출 중 에러 발생 : {e}", exc_info=True) + logger.debug(f"(baxia_Frame) [Sliding Captcha] 처리결과 : [{captcha_resolved}]") + + # 로그인 상태 확인 및 처리 + elif driver.find_elements(By.CLASS_NAME, login_class): + logger.debug("baxia_Frame 에서 [로그인 요구] 타입 발생 감지") + time.sleep(1) + try: + if load_cookies(driver): + captcha_resolved = True + else: + logger.debug("사용자의 로그인 시도 대기 5초마다 체크") + check_login_status_by_iframe() # 사용자가 로그인하길 기다림 + except Exception as e: + logger.error(f"baxia_Frame 에서 [로그인 요구] 처리 중 에러 발생 : {e}", exc_info=True) except TimeoutException as e: logger.error(f"CAPTCHA 화면을 찾는 데 실패했습니다: {e}") + captcha_resolved = False + + finally: + logger.debug("finally switch_to_frame 호출") + switch_to_frame(driver, defaultFrameName, 'default') + # driver.switch_to.default_content() + logger.debug("기본 프레임으로 전환했습니다.") + return captcha_resolved + +def switch_to_frame(driver, defaultFrameName, frameName): + """ + 지정된 프레임으로 전환하는 함수입니다. + 이 함수는 현재 포커스된 프레임이 전환하려는 프레임과 다를 경우에만 프레임 전환을 시도합니다. + :param driver: WebDriver 인스턴스 + :param frameName: 전환할 프레임의 이름 또는 'default' (기본 프레임으로 전환할 경우) + """ + + # logger.debug("프레임 포커스를 전환하겠습니다.") + current_frame = driver.execute_script("return self.name") # 현재 프레임의 이름을 얻음 + logger.debug(f"[switch_to_frame]현재 프레임 포커스 : [{current_frame}]") + + logger.debug("[switch_to_frame]프레임 전환 전 기본 프레임으로 전환 시도") + if current_frame != defaultFrameName: # 현재 프레임이 기본 프레임이 아니면 전환 + driver.switch_to.default_content() + logger.debug("[switch_to_frame]현재 포커스가 기본 프레임이 아니기 때문에 기본프레임으로 전환했습니다.") + else: + logger.debug("[switch_to_frame]현재 포커스는 기본 프레임상태이므로 전환하지 않습니다.") + + if frameName == 'default': + if current_frame != defaultFrameName: # 현재 프레임이 기본 프레임이 아니면 전환 + driver.switch_to.default_content() + logger.debug("[switch_to_frame]기본 프레임으로 전환했습니다.") + else: + # frameName이 현재 프레임과 다르면 전환을 시도 + if current_frame != frameName: + try: + # 프레임 존재 확인 + WebDriverWait(driver, 10).until( + EC.frame_to_be_available_and_switch_to_it(frameName) + ) + logger.debug(f"[switch_to_frame] '{frameName}' 프레임으로 전환했습니다.") + except TimeoutException: + logger.error(f"[switch_to_frame] '{frameName}' 프레임을 찾을 수 없습니다.") + except Exception as e: + logger.error(f"[switch_to_frame] '{frameName}' 프레임으로의 전환 중 오류 발생: {e}") + else: + logger.debug(f"[switch_to_frame] '{frameName}' 프레임에 이미 포커스되어 있습니다.") + +# def handle_captcha_for_tao(driver, message_controller): +# captcha_iframe_id = 'baxia-dialog-content' +# last_message_time = time.time() + +# scratch_captcha_class = 'scratch-captcha-question' +# nocaptcha_class = 'nc_1_nocaptcha' +# login_class = 'login-box loading' + +# try: +# WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, captcha_iframe_id))) +# logger.debug("CAPTCHA 화면 발생. 진행을 위해 해결하세요") + +# # iframe으로 포커스 전환 +# driver.switch_to.frame(captcha_iframe_id) + +# # 캡차 유형 식별 +# if driver.find_elements(By.CLASS_NAME, scratch_captcha_class): +# # message_controller.send_message("scratch_captcha 발견: 처리를 기다리고 있습니다.") +# logger.debug("스크래치 캡차 발생") +# handle_scratch_captcha(driver) # Scratch captcha 처리를 위한 별도의 함수 + +# elif driver.find_elements(By.CLASS_NAME, nocaptcha_class): +# logger.debug("슬라이드 캡차 발생") +# handle_sliding_captcha(driver, message_controller, last_message_time) # Nocaptcha 처리를 위한 별도의 함수 + +# elif driver.find_elements(By.CLASS_NAME, login_class): +# logger.debug("로그인 팝업 발생") +# if load_cookies(driver): +# logger.debug("저장된 쿠키로 로그인 완료") +# else: +# logger.debug("사용자의 로그인 시도 대기") +# # message_controller.send_massage("로그인 요청") +# time.sleep(600) + +# except TimeoutException as e: +# logger.error(f"CAPTCHA 화면을 찾는 데 실패했습니다: {e}", exc_info=True) +# return False + +# finally: +# # 기본 컨텐츠로 포커스 전환 +# driver.switch_to.default_content() +# logger.debug("캡차 해결 완료") + + +def handle_scratch_captcha(driver, defaultFrameName, message_controller, max_wait_time=7200): + """ + 스크래치 캡차를 처리하는 함수 + :param driver: WebDriver 인스턴스 + :param message_controller: 메시지 컨트롤러 인스턴스 + """ + + # logger.debug("스크래치 캡차 발생") + + # 사용자에게 캡차 해결 요청 메시지 전송 + # message_controller.send_message("스크래치 캡차가 발생했습니다. 해결을 도와주세요.") + logger.debug("Please Soleve Scratch Captcha | 해결을 도와주세요.") + + # 향후 캡차 해결 서비스 도입을 위한 준비 + # TODO: 2Captcha 같은 캡차 해결 서비스를 도입하여 자동으로 처리할 예정 + # captcha_solver = TwoCaptchaSolver(api_key='YOUR_API_KEY') + # result = captcha_solver.solve_captcha(driver) + # if result: + # logger.debug("스크래치 캡차 자동 해결 완료") + # else: + # logger.error("스크래치 캡차 자동 해결 실패") + + # 캡차가 해결될 때까지 대기 + captcha_frame_selector = (By.ID, 'baxia-dialog-content') + scratch_captcha_class = 'scratch-captcha-question' + start_time = time.time() + resolved = False + + # logger.debug(f"시간 경과 : {time.time() - start_time} (Second) | 최대경과 [{max_wait_time}]") + + # 기본 프레임으로 전환 + driver.switch_to.default_content() + logger.debug("기본 프레임으로 전환했습니다.") # baxia-dialog-content 프레임이 사라질 때까지 대기 + + while time.time() - start_time < max_wait_time: + logger.debug(f"시간 경과 : {time.time() - start_time} (Second) | 최대경과 [{max_wait_time}]") + try: + WebDriverWait(driver, 5).until( + EC.invisibility_of_element_located(captcha_frame_selector) + ) + logger.debug("스크래치 캡차 해결됨, 추가 확인을 위해 5초 대기 중...") + time.sleep(5) # 해결 후 잠시 대기 + + # 대기 후 다시 프레임이 없는지 확인 + # switch_to_frame(driver, defaultFrameName, 'default') + # if not WebDriverWait(driver, 2).until( + # EC.presence_of_element_located(captcha_frame_selector)): + try: + WebDriverWait(driver, 5).until( + EC.invisibility_of_element_located(captcha_frame_selector) + ) + # if not driver.find_elements(*captcha_frame_selector): + logger.debug("@@@@@스크래치 캡차가 최종적으로 해결되었습니다.@@@@@") + return True + except TimeoutException: + logger.debug("스크래치 캡차 재발생, 다시 해결을 기다립니다.") + except TimeoutException: + logger.debug("스크래치 캡차가 여전히 존재합니다, 계속 대기합니다.") + continue # 계속해서 캡차가 존재하는지 확인 + except Exception as e: + logger.error(f"스크래치 캡차 대기 중 오류 발생: {e}") + break + + if time.time() - start_time >= max_wait_time: + logger.error("스크래치 캡차 해결 대기 시간 초과. 프로그램을 종료하거나 추가 조치가 필요합니다.") + # 메세지 보낸 후 2캅차 호출 return False - finally: - # 기본 컨텐츠로 포커스 전환 - driver.switch_to.default_content() - - -def handle_scratch_captcha(driver): - logger.debug("scratch_captcha 처리 시작") - while True: - try: - if check_first_product_tao(driver): - logger.debug("scratch_captcha에서 첫 번째 제품 확인됨: while 문 종료.") - break - time.sleep(1) - except Exception as e: - logger.error(f"scratch_captcha에서 check_first_product 처리 중 오류 발생: {e}") - break + return True -def handle_nocaptcha(driver): +def handle_sliding_captcha(driver, defaultFrameName ,message_controller, last_message_time): + # driver.switch_to.frame('baxia-dialog-content') + act = ActionChains(driver) + logger.debug("Sliding Captcha 해결 시도") offset = 258 - driver.switch_to.frame('baxia-dialog-content') - print("슬라이딩 시도") - - try: - while True: + attempt_count = 0 + max_attempts = 100 # 최대 시도 횟수 설정 + while attempt_count < max_attempts: + try: + try: + if driver.find_elements(By.CLASS_NAME, 'errloading'): + refresh_button = WebDriverWait(driver, 3).until( + EC.visibility_of_element_located((By.CLASS_NAME, 'errloading'))) + act.click(refresh_button).perform() + except: + pass # 슬라이더 캡차가 로드되기를 기다림 slider = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, 'nc_1_n1z')) # 슬라이더의 ID를 정확히 알아야 함 ) - print("슬라이더 로드") - + logger.debug("슬라이더 로드") # 슬라이더를 클릭하여 활성화 - ActionChains(driver).click_and_hold(slider).perform() - time.sleep(0.5) - print("슬라이더 클릭") + act.click_and_hold(slider).perform() + time.sleep(0.2) + logger.debug("슬라이더 클릭") # 슬라이더를 움직이는 로직 - rand_offset = randint(10,100) - ActionChains(driver).move_by_offset(rand_offset, 0).perform() # 필요한 경우 이 값을 조정 - print("슬라이더 이동") - wait_time = randint(1,3) - time.sleep(wait_time) # 결과 확인을 위한 대기 시간 - # 마우스 버튼을 놓음 - ActionChains(driver).move_by_offset(offset-rand_offset, 0).perform() # 필요한 경우 이 값을 조정 + rand_offset = randint(180,220) - ActionChains(driver).release().perform() - print("슬라이더 버튼 놓음") + # # 점진적으로 슬라이더를 이동 + # for x in range(0, rand_offset, 1): # 10픽셀 단위로 슬라이더 이동 + # act.move_by_offset(1, 0).perform() + # time.sleep(0.001) # 자연스러운 움직임을 위해 소량의 딜레이 추가 + # logger.debug(f"슬라이더 {x+10}픽셀 이동") + + act.move_by_offset(rand_offset, 0).perform() # 랜덤한 절반 이동 + logger.debug("슬라이더 이동") + + wait_time = random.uniform(0.1, 0.7) + time.sleep(wait_time) # 결과 확인을 위한 대기 시간 + + # # 점진적으로 슬라이더를 추가적으로 이동 + # for x in range(0, offset-rand_offset, 1): # 10픽셀 단위로 슬라이더 이동 + # act.move_by_offset(1, 0).perform() + # time.sleep(0.01) # 자연스러운 움직임을 위해 소량의 딜레이 추가 + # logger.debug(f"슬라이더 {x+10}픽셀 이동") + + act.move_by_offset(offset-rand_offset, 0).perform() # 랜덤한 절반 이동 + wait_time = randint(1,2) + wait_time = random.uniform(0.3, 1.4) + time.sleep(wait_time) # 결과 확인을 위한 대기 시간 + + act.release().perform()# 마우스 버튼을 놓음 + logger.debug("슬라이더 버튼 놓음") # 성공적으로 슬라이드 했는지 확인 - print("캡차 결과 확인 중...") - time.sleep(2) # 결과 확인을 위한 대기 시간 + logger.debug("캡차 결과 확인 중...") + time.sleep(0.2) # 결과 확인을 위한 대기 시간 # 캡차 해결 여부 확인 #
Oops... something's wrong. Please refresh and try again.(error:x5fZg)
try: - refresh_button = WebDriverWait(driver, 3).until( + WebDriverWait(driver, 2).until_not( EC.visibility_of_element_located((By.CLASS_NAME, 'errloading')) ) - print("CAPTCHA 해결 실패, 재시도 중...") - # refresh_button.click() - ActionChains(driver).click(refresh_button).perform() - except NoSuchElementException: - # 새로 고침 버튼이 없으면 캡차 해결 성공으로 간주 - print("CAPTCHA 해결됨.") - break - finally: - # 브라우저 닫기 - driver.switch_to.default_content() - driver.refresh() - print("슬라이드 캡차 해결 완료.") + WebDriverWait(driver, 3).until_not( + EC.visibility_of_element_located((By.ID, 'baxia-dialog-content')) + ) + # WebDriverWait(driver, 2).until_not( + # EC.visibility_of_element_located((By.ID, 'nc_1_n1z')) + # ) + logger.debug("Sliding Captcha Solved") + logger.debug("추가적인 Scratch Captcha 감지 대기 중 : 대기시간 4초") + time.sleep(4) + if check_for_scratch_captcha(driver): + logger.debug("Sliding Captcha 해결 후 scratch 캡차 발생 감지") + return handle_scratch_captcha(driver, defaultFrameName, message_controller) + logger.debug("스크래치 캡차 없음 확인") + logger.debug(f"!!! Sliding Captcha [{attempt_count+1}]/[{max_attempts}]회 만에 해결!!!") + return True + except TimeoutException: + logger.debug(f"Sliding Captcha 해결 실패, [{attempt_count+1}]/[{max_attempts}]회 재시도 중...") + refresh_button = WebDriverWait(driver, 3).until( + EC.visibility_of_element_located((By.CLASS_NAME, 'errloading'))) + act.click(refresh_button).perform() + attempt_count += 1 + + except Exception as e: + logger.error(f"Sliding Captcha 처리 중 오류 발생: {e}", exc_info=True) + return False + # finally: + # driver.switch_to.default_content() + logger.debug("최대 시도 횟수 초과, Sliding Captcha 해결 실패") + return False + +def check_element_visibility(driver, element_id, timeout=5): + try: + WebDriverWait(driver, timeout).until(EC.visibility_of_element_located((By.ID, element_id))) + return True + except TimeoutException: + return False + + # #[수정된 내 코드] + # try: + # refresh_button = WebDriverWait(driver, 3).until( + # EC.visibility_of_element_located((By.CLASS_NAME, 'errloading'))) + # print("슬라이딩 CAPTCHA 해결 실패, 재시도 중...") + # ActionChains(driver).click(refresh_button).perform() + # except NoSuchElementException: + # try: + # WebDriverWait(driver, 3).until( + # EC.visibility_of_element_located((By.ID, 'baxia-dialog-content'))) + # logger.debug("슬라이딩 캡차 재시도 필요") + # except NoSuchElementException: + # logger.debug("슬라이딩 캡차 해결됨") + # if check_for_scratch_captcha(driver): + # logger.debug("스크래치 캡차 발생 감지") + # return handle_scratch_captcha(driver, message_controller) + # return True + + # #기존에 에러가 발생했던 코드 + # if not driver.find_elements_by_class_name('errloading'): + # if not driver.find_elements_by_id('baxia-dialog-content'): + # logger.debug("슬라이딩 캡차 해결됨") + # if check_for_scratch_captcha(driver): + # logger.debug("스크래치 캡차 발생 감지") + # return handle_scratch_captcha(driver, message_controller) + # return True + # else: + # logger.debug("슬라이딩 캡차 재시도 필요") + # else: + # refresh_button = driver.find_element_by_class_name('errloading') + # logger.debug("슬라이딩 CAPTCHA 해결 실패, 재시도 중...") + # ActionChains(driver).click(refresh_button).perform() + # except Exception as e: + # logger.error(f"슬라이딩 캡차 처리 중 오류 발생: {e}", exc_info=True) + # return False + # finally: + # driver.switch_to.default_content() + +def check_for_scratch_captcha(driver): + # 스크래치 캡차 감지 로직 + logger.debug("스크래치 캡차 감지 여부 판단 중...") + scratch_captcha_class = 'scratch-captcha-question' + return bool(driver.find_elements(By.CLASS_NAME, scratch_captcha_class)) def setup_driver(): """ @@ -318,7 +593,7 @@ def setup_driver(): logger.debug(f"현재 사용 중인 User-Agent: {current_user_agent}") return driver except Exception as e: - logger.debug(f"셀레니움 드라이버 설정 중 에러 발생 : {e}") + logger.debug(f"셀레니움 드라이버 설정 중 에러 발생 : {e}", exc_info=True) logger.error(traceback.format_exc()) # traceback 모듈을 사용하여 예외 정보를 문자열로 변환하여 로그에 출력 @@ -358,447 +633,376 @@ def login_and_manage_session(driver): save_cookies(driver) +def send_image_to_clipboard(image_path): + logger.debug(f"이미지 path : {image_path}") + image = Image.open(image_path) + + output = BytesIO() + image.convert("RGB").save(output, "BMP") + data = output.getvalue()[14:] + logger.debug("이미지 BMP로 변환") + output.close() + + logger.debug("이미지를 클립보드에 복사 시도") + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data) + win32clipboard.CloseClipboard() + logger.debug("이미지를 클립보드에 복사 완료") + +def search_img_with_action(driver, imgurl, isCrop, search_attempts): + try: + if not WebDriverWait(driver, 3).until(EC.invisibility_of_element_located((By.ID, 'baxia-dialog-content'))): + logger.error("CAPTCHA 화면이 활성화되어 있어 검색을 시작할 수 없습니다.") + return False + except Exception as e: + logger.error(f"search_img_with_action에서 캡차 화면 인식 중 에러 : {e}", exc_info=True) + return False + # 이미지를 다운로드하고 클립보드에 복사 + logger.debug(f"search_img_with_action IMG_URL: {imgurl}") + + try: + response = requests.get(imgurl) + image = Image.open(BytesIO(response.content)) + # 이미지가 RGBA 모드인 경우 RGB로 변환 + if image.mode == "RGBA": + image = image.convert("RGB") + elif image.mode == "P": + image = image.convert("RGB") + + logger.debug(f"isCrop = {isCrop}") + + if isCrop >= 1 and isCrop < 7: + image = crop_by_boxline(image, 0.015) + search_attempts += 1 + logger.debug(f"1.5% 크롭 : 이미지 수정 [{search_attempts}]회 수행") + elif isCrop >= 7 and isCrop <= 9: + image = rotate_by_angle(image, 2, 'transparent') + search_attempts += 1 + logger.debug(f"2% 회전 : 이미지 수정 [{search_attempts}]회 수행") + elif isCrop == 10: + image = mirror_by_image(image) + search_attempts += 1 + logger.debug(f"거울반사 : 이미지 수정 [{search_attempts}]회 수행") + elif isCrop == 0: + logger.debug("이미지수정 없음") + search_attempts += 1 + + temp_path = "./temp_image.jpg" + image.save(temp_path) + send_image_to_clipboard(temp_path) + except Exception as e: + logger.debug(f"이미지 URL에서 다운로드 후 클립보드 복사 과정에서 예외 발생: {e}", exc_info=True) + return False + # 이미지 검색 영역을 클릭하기 위한 액션 체인 생성 + try: + search_area_selector = ".rax-textinput" + search_area = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, search_area_selector)) + ) + # ActionChains(driver).click(search_area).perform() + ActionChains(driver).click_and_hold(search_area).perform() + # 랜덤한 시간을 생성 + random_sleep_time = random.uniform(0.2, 1.7) + time.sleep(random_sleep_time) + ActionChains(driver).release().perform() + logger.debug("이미지검색영역 클릭") + + # 클립보드의 이미지를 붙여넣기 + ActionChains(driver).key_down(Keys.CONTROL).send_keys('v').key_up(Keys.CONTROL).perform() + logger.debug("클립보드의 이미지를 붙여넣기") + # 랜덤한 시간을 생성 + random_sleep_time = random.uniform(0.6, 2.4) + time.sleep(random_sleep_time) + + # 검색 버튼 클릭 + search_button_selector = ".component-preview-button" + search_button = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, search_button_selector)) + ) + # ActionChains(driver).click(search_button).perform() + ActionChains(driver).click_and_hold(search_button).perform() + random_sleep_time = random.uniform(1.2, 2.4) + time.sleep(random_sleep_time) + ActionChains(driver).release().perform() + + logger.debug("검색버튼 클릭") + # 랜덤한 시간을 생성 + random_sleep_time = random.uniform(0.5, 1.6) + time.sleep(random_sleep_time) + + return True + except Exception as e: + logger.debug(f"search_img_with_action 과정 중 에러 발생 : {e}", exc_info=True) + return False + +def check_first_product(driver): + try: + first_product_CSS = ".rax-view-v2:nth-child(1) > .rax-view-v2 > .mobile--class-1--2Vz4bM4" + WebDriverWait(driver, 3).until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, first_product_CSS))) + logger.debug("첫번째 상품을 찾았습니다.") + return True + except TimeoutException: + logger.debug("첫번째 상품을 찾을 수 없습니다.") + return False + +def handle_sorry_message(driver): + sorry_message_xpath = "//span[contains(.,'Sorry,没有找到相关的宝贝!!')]" + try: + WebDriverWait(driver, 3).until(EC.visibility_of_element_located((By.XPATH, sorry_message_xpath))) + logger.debug("'Sorry' 발생. 페이지를 새로고침 합니다.") + driver.refresh() + time.sleep(2) + return True + except TimeoutException: + return False + def fetch_and_save_taobao_products(driver, message_controller, imgurl, cursor, db, item_count=5, sort_order=1): """ 타오바오의 상품을 검색하고 결과를 파싱하는 함수 """ - - # 이미지 URL로부터 pHash 값을 계산하는 함수 - def calculate_phash(image_url): - try: - # 캡차요청 회피를 위한 헤더 재설정 - 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", # https로의 업그레이드를 요청 - "Cache-Control": "max-age=0", # 캐시된 콘텐츠를 재사용하지 않도록 요청 - } - response = requests.get(image_url, headers=headers) - # response = requests.get(image_url) - # 이미지 데이터 검증을 위한 임시 파일 저장 - if response.status_code == 200: - with open('temp_image', 'wb') as f: - f.write(response.content) - - if response.status_code == 200 and 'image' in response.headers['Content-Type']: - img = Image.open(BytesIO(response.content)) - # phash = imagehash.phash(img) - # return phash - return 1 - else: - logger.debug("이미지 로드 실패 또는 잘못된 콘텐츠 타입") - logger.debug(response.status_code) - logger.debug(response.headers) - logger.debug(response.text[:500]) # 본문의 처음 500자 출력 - return None # 이미지 처리에 실패하면 None 반환 - except Exception as e: - logger.debug(f"이미지 처리 중 오류 발생: {e}") - return None # 예외 발생 시 None 반환 - - - # 두 이미지 URL의 pHash 값의 차이를 계산하는 함수 - def compare_images_phash(imgurl, product_imgurl): - hash1 = calculate_phash(imgurl) - hash2 = calculate_phash(product_imgurl) - if hash1 is not None and hash2 is not None: - difference = hash1 - hash2 - return difference - else: - return None # 해시 계산에 실패한 경우 None 반환 - - def convert_price(price_str): - logger.debug(f"convert_price : {price_str}") - - try: - return int(float(price_str)) - except ValueError: - return 0 - - def convert_sales_volume(sales_str): - logger.debug(f"convert_sales_volume : {sales_str}") - match = re.search(r'(\d+)(万)?\+?', sales_str) - if match: - num = int(match.group(1)) - if match.group(2): # '万'이 포함되어 있다면 - num *= 10000 # 万은 10,000을 의미 - return num - else: - return 0 - - def extract_item_id(url): - logger.debug(f"extract_item_id : {url}") - match = re.search(r'taobao.com/i(\d{10,12})', url) - return match.group(1) if match else None - - def send_image_to_clipboard(image_path): - logger.debug(f"이미지 path : {image_path}") - image = Image.open(image_path) - - output = BytesIO() - image.convert("RGB").save(output, "BMP") - data = output.getvalue()[14:] - logger.debug("이미지 BMP로 변환") - output.close() - - logger.debug("이미지를 클립보드에 복사 시도") - win32clipboard.OpenClipboard() - win32clipboard.EmptyClipboard() - win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data) - win32clipboard.CloseClipboard() - logger.debug("이미지를 클립보드에 복사 완료") - - def search_img_with_action(imgurl, isCrop, cropCount): - # 이미지를 다운로드하고 클립보드에 복사 - logger.debug(f"search_img_with_action IMG_URL: {imgurl}") - try: - response = requests.get(imgurl) - image = Image.open(BytesIO(response.content)) - # 이미지가 RGBA 모드인 경우 RGB로 변환 - if image.mode == "RGBA": - image = image.convert("RGB") - - logger.debug(f"isCrop = {isCrop}") - - if isCrop < 7: - image = crop_by_boxline(image, 0.015) - cropCount += 1 - elif isCrop >= 7 and isCrop <= 9: - image = rotate_by_angle(image, 1, 'transparent') - cropCount += 1 - elif isCrop == 10: - image = mirror_by_image(image) - cropCount += 1 - elif isCrop == 0: - logger.debug("[isCrop = 0] so pass crop or rotate iamge") - - temp_path = "./temp_image.jpg" - image.save(temp_path) - send_image_to_clipboard(temp_path) - except Exception as e: - logger.debug(f"이미지 URL에서 다운로드 후 클립보드 복사 과정에서 예외 발생: {e}") - - # 이미지 검색 영역을 클릭하기 위한 액션 체인 생성 - try: - search_area_selector = ".rax-textinput" - search_area = WebDriverWait(driver, 10).until( - EC.element_to_be_clickable((By.CSS_SELECTOR, search_area_selector)) - ) - # ActionChains(driver).click(search_area).perform() - ActionChains(driver).click_and_hold(search_area).perform() - # 랜덤한 시간을 생성 - random_sleep_time = random.uniform(0.2, 1.7) - time.sleep(random_sleep_time) - ActionChains(driver).release().perform() - logger.debug("이미지검색영역 클릭") - - # 클립보드의 이미지를 붙여넣기 - ActionChains(driver).key_down(Keys.CONTROL).send_keys('v').key_up(Keys.CONTROL).perform() - logger.debug("클립보드의 이미지를 붙여넣기") - # 랜덤한 시간을 생성 - random_sleep_time = random.uniform(0.6, 2.4) - time.sleep(random_sleep_time) - - # 검색 버튼 클릭 - search_button_selector = ".component-preview-button" - search_button = WebDriverWait(driver, 10).until( - EC.element_to_be_clickable((By.CSS_SELECTOR, search_button_selector)) - ) - # ActionChains(driver).click(search_button).perform() - ActionChains(driver).click_and_hold(search_button).perform() - random_sleep_time = random.uniform(1.2, 2.4) - time.sleep(random_sleep_time) - ActionChains(driver).release().perform() - - logger.debug("검색버튼 클릭") - # 랜덤한 시간을 생성 - random_sleep_time = random.uniform(0.5, 1.6) - time.sleep(random_sleep_time) - - except Exception as e: - logger.debug(f"과정 중 에러 발생 : {e}") - - # def search_img(imgurl): - # # imgurl에서 이미지를 로컬에 저장 - # local_image_path = "./img/temp_image.jpg" - # if not os.path.exists("./img"): - # os.makedirs("./img") - # urlretrieve(imgurl, local_image_path) # 주어진 imgurl 사용 - - # # JavaScript를 사용하여 이미지 검색 버튼 클릭 - # search_button_selector = ".component-search-icon-active" - # driver.execute_script(f"document.querySelector('{search_button_selector}').click();") - # logger.debug("이미지검색버튼 클릭") - - # # 파일 업로드 처리 - # file_input = WebDriverWait(driver, 60).until( - # EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']")) - # ) - # file_input.send_keys(os.path.abspath(local_image_path)) - - def check_first_product(driver): - try: - first_product_CSS = ".rax-view-v2:nth-child(1) > .rax-view-v2 > .mobile--class-1--2Vz4bM4" - WebDriverWait(driver, 10).until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, first_product_CSS))) - logger.debug("첫번째 상품을 찾았습니다.") - return True - except TimeoutException: - logger.debug("오류 : 첫번째 상품을 찾을 수 없습니다.") - return False - - def handle_sorry_message(driver): - sorry_message_xpath = "//span[contains(.,'Sorry,没有找到相关的宝贝!!')]" - try: - WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.XPATH, sorry_message_xpath))) - logger.debug("'Sorry' 발생. 페이지를 새로고침 합니다.") - driver.refresh() - time.sleep(33) - return True - except TimeoutException: - return False - - def handle_captcha(driver, message_controller): - captcha_iframe_id = 'baxia-dialog-content' - last_message_time = time.time() - - scratch_captcha_class = 'scratch-captcha-question' - nocaptcha_class = 'nc_1_nocaptcha' - - try: - WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, captcha_iframe_id))) - logger.debug("CAPTCHA 화면 발생. 진행을 위해 해결하세요") - - # iframe으로 포커스 전환 - driver.switch_to.frame(captcha_iframe_id) - - # 캡차 유형 식별 - if driver.find_elements(By.CLASS_NAME, scratch_captcha_class): - message_controller.send_message("scratch_captcha 발견: 처리를 기다리고 있습니다.") - logger.debug("scratch_captcha 발견: 처리를 기다리고 있습니다.") - while True: - try: - if check_first_product(driver): - logger.debug("scratch_captcha에서 첫 번째 제품 확인됨: while 문 종료.") - break # check_first_product가 True일 때 반복 종료 - time.sleep(1) # 1초 간격으로 확인 - except Exception as e: - logger.error(f"scratch_captcha에서 check_first_product 처리 중 오류 발생: {e}") - break # 예외 발생 시 반복 종료 - - elif driver.find_elements(By.CLASS_NAME, nocaptcha_class): - try: - # 사용자에게 초기 캡차 발생 알림 보내기 - # message_controller.send_message("CAPTCHA가 발생했습니다. 해결해 주세요.") - print("최초 캡챠발생 : 사용자에게 알람 발송") - print("nocaptcha 처리 중") - handle_nocaptcha(driver) - - while True: - time.sleep(5) # 30초마다 캡차 상태 검사 - current_time = time.time() - # if check_this_page(driver, found_first_product, message_controller): - if handle_sorry_message(driver): - driver.refresh() - if check_first_product(driver): # 캡차가 해결되었는지 체크 - logger.debug("CAPTCHA가 해결되었습니다. 첫 번째 상품이 로드되었습니다.") - driver.switch_to.default_content() - return True - - if current_time - last_message_time >= 600: # 10분마다 사용자에게 메시지 재전송 - # message_controller.send_message("CAPTCHA 해결이 여전히 필요합니다. 확인해 주세요.") - print("CAPTCHA 해결이 여전히 필요합니다. 확인해 주세요. : 사용자에게 알람 발송") - last_message_time = current_time # 메시지 전송 시간 업데이트 - - except Exception as e: - print(f"nocaptcha 처리 중 에러 발생: {str(e)}") - return False - - except TimeoutException: - print("CAPTCHA 화면을 찾는 데 실패했습니다.") - return False - - finally: - # 기본 컨텐츠로 포커스 전환 - driver.switch_to.default_content() - - while True: # 무한 루프를 시작하여 조건에 따라 재시도를 관리 - search_attempts = 0 - max_search_attempts = 5 # 상품 검색을 최대 몇 번까지 재시도할지 설정 - logger.debug(f"상품검색 최대횟수 : {max_search_attempts}회") - found_first_product = False - isCrop = 0 - cropCount = 0 - - while search_attempts < max_search_attempts and not found_first_product: - #logger.debug(f"상품 검색 시작 : {found_first_product} 상품") - #search_img(imgurl) # 상품 검색 시작 - - logger.debug(f"액션체인으로 상품 검색 시작 : {found_first_product} 상품") - search_img_with_action(imgurl, isCrop, cropCount) # 액션체인으로 상품 검색 시작 - - max_refresh_attempts = 5 - refresh_attempts = 0 - - while not found_first_product: - if check_first_product(driver): - # 첫 번째 상품이 로드되면 HTML 파싱 수행 - logger.debug("첫 번째 상품을 성공적으로 찾았습니다. HTML 파싱을 시작합니다.") - found_first_product = True - break # 첫 번째 상품을 찾았으니 내부 while 루프 탈출 - else: - # if handle_captcha(driver, message_controller): - if handle_captcha_for_tao(driver, message_controller): - logger.debug("캡차 화면입니다. 캡차를 해결합니다.") - # handle_captcha(driver) - elif handle_sorry_message(driver) and refresh_attempts < max_refresh_attempts: - logger.debug("Sorry 화면입니다. 페이지를 새로고침합니다.") - # driver.refresh() - # time.sleep(3) - refresh_attempts += 1 - # elif check_login_status(driver): - # logger.debug("로그인이 필요한 화면입니다. 로그인을 확인합니다.") - # # check_login_status(driver) - else: - logger.debug("알 수 없는 상태입니다. 상품 검색을 다시 시도합니다.") - break # 알 수 없는 상태이므로 내부 while 루프를 탈출하여 상품 검색을 재시도 - - if refresh_attempts >= max_refresh_attempts: - logger.debug("최대 새로고침 시도 횟수를 초과했습니다. 상품 검색을 다시 시도합니다.") - break # 최대 새로고침 횟수를 초과하면 내부 while 루프를 탈출하여 상품 검색을 재시도 - - if found_first_product: - break # 첫 번째 상품을 찾았으므로 전체 while 루프 탈출 - else: - search_attempts += 1 - isCrop = randint(1,10) - logger.debug(f"상품 검색 재시도 {search_attempts}/{max_search_attempts}") - - if found_first_product: - # 성공적으로 첫 번째 상품을 찾은 후의 처리 로직을 여기에 작성 - break # 성공적으로 처리가 완료되면 무한 루프 탈출 - else: - logger.debug("상품 검색 최대 재시도 횟수를 초과했습니다. 프로세스를 처음부터 다시 시작합니다.") - # 필요한 경우, 여기에서 추가적인 초기화 작업을 수행할 수 있습니다. - - try: - # 페이지 로딩 대기 - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "body")) - ) - wait=randint(2,4) - time.sleep(wait) # 또는 더 긴 시간 - - # 페이지의 HTML을 가져옴 - page_source = driver.page_source - # BeautifulSoup 객체 생성 - soup = BeautifulSoup(page_source, 'html.parser') - logger.debug("html파싱") - - souped = soup.select('a.mobile--class-1--2Vz4bM4') - except Exception as e: - logger.debug(f"타오바오 상품정보 html 파싱 중 에러발생 : {e}") # 상품 정보를 저장할 리스트 초기화 - products = [] # 상품 정보를 저장할 리스트 + products = [] + search_attempts = 0 + max_search_attempts = 15 # 상품 검색을 최대 몇 번까지 재시도할지 설정 + + refresh_attempts = 0 + max_refresh_attempts = 5 # 새로고침 최대 횟수 + similarity_threshold = 40 # 유사도 판단 기준 (해밍 거리) - - - fetch_products(souped, item_count, products, similarity_threshold) - - # # 상품 정보 추출 - # for i, product in enumerate(souped, start=1): - # logger.debug(f"souped product {i} \n {souped}") - # if i > item_count: # 설정한 아이템 갯수에 도달하면 반복 중단 - # break - # try: - # product_url = product['href'] - # logger.debug(f"타오바오 상품 product_url : {product_url}") - # Tao_itemID = extract_item_id(product_url) - # logger.debug(f"타오바오 상품 extract_item_id : {Tao_itemID}") - # image_url = 'https:' + product.select_one("img")['src'] - # logger.debug(f"타오바오 상품 image_url : {image_url}") - # product_name = product.select_one("span.mobile--summary--2mK9e7G").text - # logger.debug(f"타오바오 상품 product_name : {product_name}") - # #trans_product_name = trans(product_name) - # trans_product_name = translate_texts_translatepy(product_name) - # trans_product_name = str(trans_product_name) - # logger.debug(f"타오바오 상품 trans_product_name : {trans_product_name}") - # price_str = product.select_one("span.mobile--price--3eMQ3ec").text - # logger.debug(f"타오바오 상품 price_str : {price_str}") - # price = convert_price(price_str) - # sales_volume_str = product.select_one("span.mobile--buy--2I4hwR4").text - # logger.debug(f"타오바오 상품 sales_volume_str : {sales_volume_str}") - # sales_volume = convert_sales_volume(sales_volume_str) - # logger.debug(f"타오바오 상품 sales_volume : {sales_volume}") - - # # ============================= - # # 이미지 유사도 검사부분 개선필요. 현재는 삭제 - # # 이미지 유사도 검사 - # # logger.debug("이미지 유사도 검사 실행") - # # logger.debug(f"원본이미지 : {imgurl}") - # # logger.debug(f"타켓이미지 : {image_url}") - # # difference = compare_images_phash(imgurl, image_url) - # # logger.debug(f"이미지 유사도 difference = {difference}/{similarity_threshold}") - # # ============================== - - # difference = 30 - # # wait=randint(1,4) - # # time.sleep(wait) # 또는 더 긴 시간 - # # logger.debug(f"요청간 TimeSleep : {wait}") - - - # if difference <= similarity_threshold: - # logger.debug(f"상품 [{Tao_itemID}]의 상품정보 추가") - # product_info = { - # "Product Name": trans_product_name, - # "Image URL": image_url, - # "Price": price, - # "Sales Volume": sales_volume, - # "Product URL": product_url, - # "Tao_itemID": Tao_itemID, - # } - # else: - # logger.debug(f"상품 [{Tao_itemID}]의 상품이미지가 일치하지 않아 제외처리") - - # products.append(product_info) - # except Exception as e: - # logger.debug(f"상품정보 추출 오류 발생 {i}: {e}") - - - # # 정렬 로직 (가격순, 판매량순 정렬) - # if sort_order == 2: # 가격순 정렬 - # products.sort(key=lambda x: float(x['Price'].strip('¥')) if isinstance(x['Price'], str) else float(x['Price'])) - # elif sort_order == 3: # 판매량순 정렬 - # products.sort(key=lambda x: int(x['Sales Volume'].strip('已售').strip('件')), reverse=True) - - - # # 정렬 로직 (가격순, 판매량순 정렬) - # if sort_order == 2: # 가격순 정렬 - # products.sort(key=lambda x: float(x['Price'].strip('¥'))) - # elif sort_order == 3: # 판매량순 정렬 - # products.sort(key=lambda x: int(x['Sales Volume'].strip('已售').strip('件')), reverse=True) - - # 셀레니움 드라이버 종료 - # driver.quit() + product_found = False + isCrop = 0 + try: + defaultFrameName = driver.execute_script("return self.name") + logger.debug(f"기본 프레임 이름 : {defaultFrameName}") + except Exception as e: + logger.error(f"기본 프레임 이름을 얻는 중 에러 발생 : {e}", exc_info=True) - - # def img_save(image_url): - # image_save_folder = f"{db}_tao_save_images" + while search_attempts < max_search_attempts: + # 이미지 검색 및 수정 (검색 시도마다 다른 수정 방식 적용) + logger.debug(f"검색 시도 {search_attempts + 1}/{max_search_attempts}") + is_Success_search_action = search_img_with_action(driver, imgurl, isCrop, search_attempts) # 액션체인으로 상품 검색 시작 - # # 폴더가 없으면 생성 - # if not os.path.exists(image_save_folder): - # os.makedirs(image_save_folder) - # # 이미지 저장 스레드 실행 - # self.tao_image_save_thread = TaobaoImageSaveThread(self.conndb, image_save_folder) + if not is_Success_search_action: + if not handle_baxiaFrame(driver, defaultFrameName, message_controller): + logger.error("CAPTCHA 처리 실패, 다음 시도로 넘어갑니다.") + search_attempts += 1 + continue + + if is_Success_search_action: + logger.debug("Success : 상품검색 시도 성공") + logger.debug(f"첫번째 상품 존재 체크 : {product_found}") + product_found = check_first_product(driver) # 첫 번째 상품 확인 + else: + logger.debug("Failed : 상품검색 시도 실패") + + if product_found: + logger.debug("첫번째 상품 발견") + + try: + # 페이지 로딩 대기 + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, "body")) + ) + wait=randint(1,3) + time.sleep(wait) # 또는 더 긴 시간 + page_source = driver.page_source # 페이지의 HTML을 가져옴 + soup = BeautifulSoup(page_source, 'html.parser') # BeautifulSoup 객체 생성 + logger.debug("html파싱") + souped = soup.select('a.mobile--class-1--2Vz4bM4') + except Exception as e: + logger.debug(f"타오바오 상품정보 html 파싱 중 에러발생 : {e}", exc_info=True) + try: + # products.extend(fetch_products(souped, item_count, products, similarity_threshold)) + products = fetch_products(souped, item_count, similarity_threshold) + logger.debug("상품정보 반환") + logger.debug(f"상품정보 {products}") + return products + # return [(product['Product URL'], product['Tao_itemID'], product['Image URL'], product['Product Name'], product['Price'], product['Sales Volume']) for product in products] + except Exception as e: + logger.debug("상품정보 반환 중 에러발생") + logger.debug(f"기본값(빈값)을 반환합니다. : {e}", exc_info=True) + products = [{"Product URL": "", "Tao_itemID": "", "Image URL": "", "Product Name": "", "Price": "", "Sales Volume": ""} for _ in range(item_count)] + return products + + if handle_baxiaFrame(driver, defaultFrameName, message_controller): + logger.debug("handle_baxiaFrame 해결. 첫번째 상품확인") + product_found = check_first_product(driver) # 첫 번째 상품 확인 + continue + + if handle_sorry_message(driver): + refresh_attempts += 1 + logger.debug(f"Sorry 화면 : 새로고침 [{refresh_attempts}]/[{max_refresh_attempts}]") + if refresh_attempts > max_refresh_attempts: + logger.debug("최대 새로고침횟수 도달 : 이미지 수정 시도") + refresh_attempts = 0 + isCrop = randint(1, 10) # 이미지 수정 방법을 무작위로 선택 + product_found = check_first_product(driver) # 첫 번째 상품 확인 + continue - - # img_save(image_url) - - # 상품 정보 반환 - return [(product['Product URL'], product['Tao_itemID'], product['Image URL'], product['Product Name'], product['Price'], product['Sales Volume']) for product in products] + if not products: # 상품을 찾지 못한 경우 빈 상품 정보 채워 반환 + logger.debug(f"상품 검색 횟수 초과 : [{max_search_attempts}]회 실패 => 빈 상품 정보 반환") + products = [{"Product URL": "", "Tao_itemID": "", "Image URL": "", "Product Name": "", "Price": "", "Sales Volume": ""} for _ in range(item_count)] + return products + # while True: # 무한 루프를 시작하여 조건에 따라 재시도를 관리 + # logger.debug(f"상품검색 최대횟수 : {max_search_attempts}회") + # found_first_product = False + # isCrop = 0 + # cropCount = 0 + + # # while search_attempts < max_search_attempts and not found_first_product: + # while search_attempts < max_search_attempts: + # #logger.debug(f"상품 검색 시작 : {found_first_product} 상품") + # #search_img(imgurl) # 상품 검색 시작 + # image_modification_count = 0 # 이미지 수정 횟수 초기화 + # found_first_product = False + + # logger.debug(f"액션체인으로 상품 검색 시작 : {found_first_product} 상품") + # search_img_with_action(driver, imgurl, isCrop, cropCount) # 액션체인으로 상품 검색 시작 + + # max_refresh_attempts = 5 + # refresh_attempts = 0 + + # # while not found_first_product: + # while not found_first_product and image_modification_count < MAX_IMAGE_MODIFICATIONS: + + # if check_first_product(driver): + # # 첫 번째 상품이 로드되면 HTML 파싱 수행 + # logger.debug("첫 번째 상품을 성공적으로 찾았습니다. HTML 파싱을 시작합니다.") + # found_first_product = True + # break # 첫 번째 상품을 찾았으니 내부 while 루프 탈출 + # else: + # # if handle_captcha(driver, message_controller): + # if handle_captcha_for_tao(driver, message_controller): + # logger.debug("캡차 화면입니다. 캡차를 해결합니다.") + # # handle_captcha(driver) + # elif handle_sorry_message(driver) and refresh_attempts < max_refresh_attempts: + # logger.debug("Sorry 화면입니다. 페이지를 새로고침합니다.") + # # driver.refresh() + # # time.sleep(3) + # refresh_attempts += 1 + # # elif check_login_status(driver): + # # logger.debug("로그인이 필요한 화면입니다. 로그인을 확인합니다.") + # # # check_login_status(driver) + # else: + # logger.debug("알 수 없는 상태입니다. 상품 검색을 다시 시도합니다.") + # break # 알 수 없는 상태이므로 내부 while 루프를 탈출하여 상품 검색을 재시도 + + # if refresh_attempts >= max_refresh_attempts: + # logger.debug("최대 새로고침 시도 횟수를 초과했습니다. 상품 검색을 다시 시도합니다.") + # break # 최대 새로고침 횟수를 초과하면 내부 while 루프를 탈출하여 상품 검색을 재시도 + + # if found_first_product: + # break # 첫 번째 상품을 찾았으므로 전체 while 루프 탈출 + # else: + # search_attempts += 1 + # isCrop = randint(1,10) + # logger.debug(f"상품 검색 재시도 {search_attempts}/{max_search_attempts}") + + # if found_first_product: + # # 성공적으로 첫 번째 상품을 찾은 후의 처리 로직을 여기에 작성 + # break # 성공적으로 처리가 완료되면 무한 루프 탈출 + # else: + # logger.debug("상품 검색 최대 재시도 횟수를 초과했습니다. 프로세스를 처음부터 다시 시작합니다.") + # # 필요한 경우, 여기에서 추가적인 초기화 작업을 수행할 수 있습니다. + + # try: + # # 페이지 로딩 대기 + # WebDriverWait(driver, 10).until( + # EC.presence_of_element_located((By.CSS_SELECTOR, "body")) + # ) + # wait=randint(2,4) + # time.sleep(wait) # 또는 더 긴 시간 + + # # 페이지의 HTML을 가져옴 + # page_source = driver.page_source + # # BeautifulSoup 객체 생성 + # soup = BeautifulSoup(page_source, 'html.parser') + # logger.debug("html파싱") + + # souped = soup.select('a.mobile--class-1--2Vz4bM4') + # except Exception as e: + # logger.debug(f"타오바오 상품정보 html 파싱 중 에러발생 : {e}") + + # fetch_products(souped, item_count, products, similarity_threshold) + + + # # 상품 정보 반환 + # return [(product['Product URL'], product['Tao_itemID'], product['Image URL'], product['Product Name'], product['Price'], product['Sales Volume']) for product in products] + + + # # # 이미지 URL로부터 pHash 값을 계산하는 함수 + # # def calculate_phash(image_url): + # # try: + # # # 캡차요청 회피를 위한 헤더 재설정 + # # 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", # https로의 업그레이드를 요청 + # # "Cache-Control": "max-age=0", # 캐시된 콘텐츠를 재사용하지 않도록 요청 + # # } + # # response = requests.get(image_url, headers=headers) + # # # response = requests.get(image_url) + # # # 이미지 데이터 검증을 위한 임시 파일 저장 + # # if response.status_code == 200: + # # with open('temp_image', 'wb') as f: + # # f.write(response.content) + + # # if response.status_code == 200 and 'image' in response.headers['Content-Type']: + # # img = Image.open(BytesIO(response.content)) + # # # phash = imagehash.phash(img) + # # # return phash + # # return 1 + # # else: + # # logger.debug("이미지 로드 실패 또는 잘못된 콘텐츠 타입") + # # logger.debug(response.status_code) + # # logger.debug(response.headers) + # # logger.debug(response.text[:500]) # 본문의 처음 500자 출력 + + # # return None # 이미지 처리에 실패하면 None 반환 + # # except Exception as e: + # # logger.debug(f"이미지 처리 중 오류 발생: {e}") + # # return None # 예외 발생 시 None 반환 + + + # # # 두 이미지 URL의 pHash 값의 차이를 계산하는 함수 + # # def compare_images_phash(imgurl, product_imgurl): + # # hash1 = calculate_phash(imgurl) + # # hash2 = calculate_phash(product_imgurl) + # # if hash1 is not None and hash2 is not None: + # # difference = hash1 - hash2 + # # return difference + # # else: + # # return None # 해시 계산에 실패한 경우 None 반환 + + # # def convert_price(price_str): + # # logger.debug(f"convert_price : {price_str}") + + # # try: + # # return int(float(price_str)) + # # except ValueError: + # # return 0 + + # # def convert_sales_volume(sales_str): + # # logger.debug(f"convert_sales_volume : {sales_str}") + # # match = re.search(r'(\d+)(万)?\+?', sales_str) + # # if match: + # # num = int(match.group(1)) + # # if match.group(2): # '万'이 포함되어 있다면 + # # num *= 10000 # 万은 10,000을 의미 + # # return num + # # else: + # # return 0 + + # # def extract_item_id(url): + # # logger.debug(f"extract_item_id : {url}") + # # match = re.search(r'taobao.com/i(\d{10,12})', url) + # # return match.group(1) if match else None diff --git a/tao_scraping_thread.py b/tao_scraping_thread.py index 9114d73..9b03617 100644 --- a/tao_scraping_thread.py +++ b/tao_scraping_thread.py @@ -32,6 +32,6 @@ class TaoScrapingThread(QThread): logger.debug("Automatch 작업 완료") except Exception as e: - logger.error(f"Automatch 스레드에서 오류 발생: {e}") + logger.error(f"Automatch 스레드에서 오류 발생: {e}", exc_info=True) finally: self.finished.emit() \ No newline at end of file diff --git a/taoseller.py b/taoseller.py index a0c474a..5a503d3 100644 --- a/taoseller.py +++ b/taoseller.py @@ -81,7 +81,7 @@ def update_db(db_name, query, params): conn.commit() except Exception as e: conn.rollback() - print(f"Failed to update database: {e}") + print(f"Failed to update database: {e}", exc_info=True) return False return True @@ -174,7 +174,7 @@ class PandasModel(QAbstractTableModel): self._data.reset_index(drop=True, inplace=True) self.layoutChanged.emit() except Exception as e: - print(f"Error sorting data: {e}") + print(f"Error sorting data: {e}", exc_info=True) def setData(self, index, value, role=Qt.EditRole): if not index.isValid(): @@ -233,7 +233,7 @@ class PandasModel(QAbstractTableModel): connection.commit() except Exception as e: connection.rollback() - print(f"Failed to update database: {e}") + print(f"Failed to update database: {e}", exc_info=True) finally: connection.close() @@ -567,7 +567,7 @@ class Ui_Dialog(QtWidgets.QDialog): self.automatch_thread.finished.connect(self.on_automatch_finished) # automatch 작업 완료 후 처리 self.automatch_thread.start() except Exception as e: - logger.debug(f"타오바오 자동매칭 스레드 실행 중 에러 : {e}") + logger.debug(f"타오바오 자동매칭 스레드 실행 중 에러 : {e}", exc_info=True) def on_automatch_finished(self): # automatch 작업 완료 후 처리 @@ -841,7 +841,7 @@ class Ui_Dialog(QtWidgets.QDialog): conn.close() return count except Exception as e: - logger.debug(f"Error: {e}") + logger.debug(f"Error: {e}", exc_info=True) return 0 # 에러가 발생한 경우 0 반환 @@ -1032,35 +1032,46 @@ class Ui_Dialog(QtWidgets.QDialog): def save_to_excel_with_xlwings_by_branch(self, db_name, file_name): - # 로그 다이얼로그 생성 및 표시 - log_dialog = LogDialog() - log_dialog.show() - self.saved_files = [] - - logger.debug("엑셀 저장 로그기록 시작") - message = "엑셀 저장 로그기록 시작" - log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 + try: + # 로그 다이얼로그 생성 및 표시 + log_dialog = LogDialog() + log_dialog.show() + self.saved_files = [] + + logger.debug("엑셀 저장 로그기록 시작") + message = "엑셀 저장 로그기록 시작" + log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 - # 로그 설정 - logger.debug("엑셀 저장 로그기록 파일 생성") - - # 데이터베이스에서 필요한 데이터 로드 - conn = sqlite3.connect(db_name) - query = "SELECT MatchingUrl, keyword, MatchingCat, delvFee, packingFee, plusFee, manuTag, merged_price FROM NaverShopping WHERE MatchingUrl IS NOT NULL" - df = pd.read_sql_query(query, conn) - conn.close() - logger.debug("DB 읽기 완료") - message = "DB 읽기 완료" - log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 - # 'manuTag' 필드에서 '오늘' 단어 제거 - df['manuTag'] = df['manuTag'].apply(lambda x: ','.join([word for word in str(x).split(',') if '오늘' not in word.strip()])) - logger.debug("태그 필터링") - message = "태그 필터링" - log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 - - # Excel 애플리케이션을 백그라운드에서 실행 - app = xw.App(visible=False) - logger.debug("xlwings 시작") + # 로그 설정 + logger.debug("엑셀 저장 로그기록 파일 생성") + + # 데이터베이스에서 필요한 데이터 로드 + conn = sqlite3.connect(db_name) + query = "SELECT MatchingUrl, keyword, MatchingCat, delvFee, packingFee, plusFee, manuTag, merged_price FROM NaverShopping WHERE MatchingUrl IS NOT NULL" + query2 = "SELECT MatchingUrl, keyword, MatchingCat, delvFee, packingFee, plusFee, manuTag, price FROM NaverShopping WHERE MatchingUrl IS NOT NULL" + try: + df = pd.read_sql_query(query, conn) + except: + df = pd.read_sql_query(query2, conn) + conn.close() + logger.debug("DB 읽기 완료") + message = "DB 읽기 완료" + log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 + # 'manuTag' 필드에서 '오늘' 단어 제거 + df['manuTag'] = df['manuTag'].apply(lambda x: ','.join([word for word in str(x).split(',') if '오늘' not in word.strip()])) + logger.debug("태그 필터링") + message = "태그 필터링" + log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 + + # Excel 애플리케이션을 백그라운드에서 실행 + app = xw.App(visible=False) + logger.debug("xlwings 시작") + except Exception as e: + logger.debug(e) + # 예외를 로그에 기록 + logging.error(f"파일 저장 준비 중 예외 발생: {str(e)}", exc_info=True) + message = f"파일 저장 준비 중 예외 발생: {str(e)}" + log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 try: # 50개 행씩 나누어 파일 저장 @@ -1079,16 +1090,17 @@ class Ui_Dialog(QtWidgets.QDialog): final_delv = 0 - if 'sourcingman' in self.db_name or 'copyman' in self.db_name: - final_price = math.ceil((row['price'])*0.98 / 100) * 100 # 2%가격을 낮춘 후 100원단위 올림 - - else: - # final_delv = row['delvFee'] + row['packingFee'] - # final_price = row['plusFee'] - final_price = row['murged_price'] # 데이터 삽입 for index, row in df_subset.iterrows(): + if 'sourcingman' in self.db_name or 'copyman' in self.db_name: + final_price = math.ceil((row['price'])*0.98 / 100) * 100 # 2%가격을 낮춘 후 100원단위 올림 + else: + # final_delv = row['delvFee'] + row['packingFee'] + if not row['murged_price']: + final_price = row['plusFee'] + final_price = row['murged_price'] + row_num = 4 + (index % 50) ws.range(f'B{row_num}').value = row['MatchingUrl'] ws.range(f'C{row_num}').value = row['keyword'] @@ -1117,7 +1129,7 @@ class Ui_Dialog(QtWidgets.QDialog): except Exception as e: logger.debug(e) # 예외를 로그에 기록 - logging.error(f"파일 저장 중 예외 발생: {str(e)}") + logging.error(f"파일 저장 중 예외 발생: {str(e)}", exc_info=True) message = f"파일 저장 중 예외 발생: {str(e)}" log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 finally: @@ -1228,7 +1240,7 @@ class Ui_Dialog(QtWidgets.QDialog): except Exception as e: logger.debug(e) # 예외를 로그에 기록 - logging.error(f"파일 저장 중 예외 발생: {str(e)}") + logging.error(f"파일 저장 중 예외 발생: {str(e)}", exc_info=True) message = f"파일 저장 중 예외 발생: {str(e)}" log_dialog.add_log_message(message) # 로그 다이얼로그에 메시지 추가 finally: @@ -1371,7 +1383,7 @@ class Ui_Dialog(QtWidgets.QDialog): self.addmargin_spbox.setValue(int(additional_margin)) # 올바른 방법으로 값을 설정 except Exception as e: - logger.debug(f"가격 오류 : {e}") + logger.debug(f"가격 오류 : {e}", exc_info=True) try: cat1 = index.sibling(index.row(), 9).data() @@ -1400,7 +1412,7 @@ class Ui_Dialog(QtWidgets.QDialog): self.catbox4.setText(cat4) self.catcodebox.setText(cat_code) except Exception as e: - logger.debug(f"카테고리 오류 : {e}") + logger.debug(f"카테고리 오류 : {e}", exc_info=True) # 카테고리 코드 박스가 비어있을 경우 붉은색으로 경고표시 self.check_and_set_color() @@ -1431,7 +1443,7 @@ class Ui_Dialog(QtWidgets.QDialog): logger.debug(f"이미지 로드 실패: {localImagePath}") except Exception as e: - logger.debug(f"이미지 클립보드 복사 오류: {e}") + logger.debug(f"이미지 클립보드 복사 오류: {e}", exc_info=True) # 선택된 행의 ID 가져오기 (ID가 첫 번째 열에 있다고 가정) self.selected_row_id = index.sibling(index.row(), 14).data() @@ -1565,10 +1577,10 @@ class Ui_Dialog(QtWidgets.QDialog): #getattr(self, f'sel_itembox{i+1}2').setText(manuTags[i]) except sqlite3.OperationalError as e: - logger.debug(f"SQL 오류: {e}") + logger.debug(f"SQL 오류: {e}", exc_info=True) QtWidgets.QMessageBox.warning(self, "오류", "DB에서 데이터를 가져오는 중 오류가 발생했습니다.") except Exception as e: - logger.debug(f"일반 오류: {e}") + logger.debug(f"일반 오류: {e}", exc_info=True) QtWidgets.QMessageBox.warning(self, "오류", "알 수 없는 오류가 발생했습니다.") self.update_match_count() @@ -1695,7 +1707,7 @@ class Ui_Dialog(QtWidgets.QDialog): logger.debug(f"Failed to download image: {url}") return None except Exception as e: - logger.debug(f"Error downloading image {url}: {e}") + logger.debug(f"Error downloading image {url}: {e}", exc_info=True) return None def load_images_by_keyword_id(self, keyword_id): @@ -2066,7 +2078,7 @@ class Ui_Dialog(QtWidgets.QDialog): self.scraping_thread.finished.connect(self.start_image_save_thread) # 스크래핑 완료 후 이미지 저장 스레드 시작 self.scraping_thread.start() except Exception as e: - logger.debug(f"에러발생 : {e}") + logger.debug(f"에러발생 : {e}", exc_info=True) def click_image_save_btn(self): # QMessageBox.information(self, "알림", "네이버 이미지 저장 시작!") @@ -2483,6 +2495,10 @@ def send_exit_message(): if __name__ == "__main__": + # 프로그램 종료 시 호출될 함수 등록 + atexit.register(restore_sleep_mode) + atexit.register(record_logout_time) + QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) app = QApplication(sys.argv) diff --git a/temp_image.jpg b/temp_image.jpg index 63d9a98..d9f20db 100644 Binary files a/temp_image.jpg and b/temp_image.jpg differ