This commit is contained in:
9700X_PC 2024-12-09 01:46:11 +09:00
parent 2ff27a8b96
commit 623671f586
41 changed files with 969 additions and 126 deletions

27
main.py
View File

@ -3,15 +3,34 @@ import logging
from PySide6.QtWidgets import QApplication
from src.gui import TaobaoScraperApp
from src.databaseManager import DatabaseManager
from src.loggerModule import Logger
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
import ctypes
from ctypes import wintypes
# COM 초기화 (멀티스레드 모드)
def initialize_com():
COINIT_MULTITHREADED = 0x0
ctypes.windll.ole32.CoInitializeEx(None, COINIT_MULTITHREADED)
# COM 해제
def uninitialize_com():
ctypes.windll.ole32.CoUninitialize()
if __name__ == "__main__":
initialize_com() # COM 초기화
app = QApplication(sys.argv)
db_manager = DatabaseManager() # 데이터베이스 매니저 인스턴스 생성
window = TaobaoScraperApp(db_manager)
logger = Logger(log_file="Scrapper2.log", logger_name="Scrapper_Logger", level=logging.DEBUG)
db_manager = DatabaseManager(logger) # 데이터베이스 매니저 인스턴스 생성
window = TaobaoScraperApp(logger, db_manager)
window.show()
uninitialize_com() # COM 해제
sys.exit(app.exec())

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"APP_DESCRIPTION": {
"message": "NAVER Shopping Lens Whale extension application"
},
"APP_NAME": {
"message": "NAVER Shopping Lens"
}
}

View File

@ -0,0 +1,8 @@
{
"APP_DESCRIPTION": {
"message": "웨일 브라우저 네이버 쇼핑렌즈 확장앱"
},
"APP_NAME": {
"message": "네이버 쇼핑렌즈"
}
}

View File

@ -0,0 +1,297 @@
@charset "utf-8";
header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
z-index: 1;
background-color: rgba(255, 255, 255, 0.8);
}
header .logo {
display: block;
position: absolute;
top: 50%;
left: 50%;
margin-left: -70px;
margin-top: -12px;
}
header .logo img {
height: 24px;
display: block;
}
header .logo::after {
content: "Beta";
display: block;
position: absolute;
top: -1px;
right: -36px;
color: #69a2fd;
font-size: 9px;
font-weight: 600;
border: 1px solid #69a2fd;
padding: 2px 5px 1px 5px;
line-height: 1;
border-radius: 20px;
background-color: #fff;
}
header .logo::before {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
header #close {
display: block;
width: 18px;
height: 18px;
position: absolute;
top: 50%;
margin-top: -9px;
left: 14px;
background-size: 18px 18px;
background-repeat: no-repeat;
}
#lens-container {
position: fixed;
top: 50%;
left: 50%;
z-index: 99999999999;
background-color: #fff;
user-select: none;
}
#lens-background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
#lens-background img {
display: block;
}
#lens-background::after {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.64);
}
#lens-background::before {
content: "";
display: block;
position: absolute;
top: -1px;
left: -1px;
bottom: -1px;
right: -1px;
border: 1px solid rgba(0, 0, 0, 0.2);
}
#lens-bench {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
user-select: none;
cursor: crosshair;
}
#lens-bench.wait {
cursor: wait !important;
}
#lens-glass {
position: absolute;
top: -1px;
left: -1px;
width: auto;
height: auto;
border: 1px solid #00c73c;
}
#lens-glass > div {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: hidden;
}
#lens-glass img {
position: absolute;
display: block;
}
#lens-glass span {
display: none;
}
#lens-glass[data-drag="on"] {
border-color: rgba(0, 0, 0, 0.3);
}
#lens-glass[data-drag="invalid"] {
border-color: red;
}
#lens-glass[data-drag="invalid"]::before {
content: "좀 더 큰 영역을 지정해주세요";
position: absolute;
top: -14px;
left: -1px;
display: block;
font-size: 11px;
line-height: 1;
color: #fff;
white-space: nowrap;
background-color: red;
padding: 2px 2px 1px 2px;
}
#lens-glass.guide {
border-width: 0;
width: 320px;
height: 100px;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -160px;
background-color: rgba(255, 255, 255, 0.4);
}
#lens-glass.guide img {
display: none;
}
#lens-glass.guide span {
display: block;
}
#lens-glass.guide span::after {
content: "커서를 드래그하여 영역을 지정해주세요";
display: block;
font-size: 16px;
font-weight: 500;
line-height: 1;
color: rgba(0, 0, 0, 0.6);
position: absolute;
top: 50%;
left: 0;
right: 0;
text-align: center;
margin-top: -6px;
text-shadow: 1px 1px 0 #fff;
}
#lens-glass.guide span::before {
content: "";
width: 10px;
height: 10px;
background-color: rgba(0, 0, 0, 0.56);
position: absolute;
top: 18px;
left: 18px;
border-radius: 5px;
}
#lens-glass.guide::after,
#lens-glass.guide::before,
#lens-glass.guide div::after,
#lens-glass.guide div::before {
content: "";
display: block;
position: absolute;
width: 16px;
height: 16px;
top: 0;
left: 0;
border: 5px solid rgba(0, 0, 0, 0.56);
border-bottom-width: 0;
border-right-width: 0;
}
#lens-glass.guide::before {
left: auto;
right: 0;
border-left: 0;
border-right-width: 5px;
}
#lens-glass.guide div::after {
top: auto;
bottom: 0;
border-top: 0;
border-left-width: 5px;
border-bottom-width: 5px;
border-right: 0;
}
#lens-glass.guide div::before {
top: auto;
left: auto;
bottom: 0;
right: 0;
border-top: 0;
border-left: 0;
border-right-width: 5px;
border-bottom-width: 5px;
}
#cursor-position {
position: absolute;
top: 0;
left: 0;
padding: 4px;
}
#cursor-position span[data-x],
#cursor-position span[data-y] {
display: block;
white-space: nowrap;
font-size: 10px;
text-shadow: 1px 1px 0px #fff;
padding: 1px;
line-height: 1;
}
#whale-lens-upload {
position: absolute;
top: 50%;
left: 48px;
height: 24px;
margin-top: -12px;
border-left: 1px solid rgba(0, 0, 0, 0.1);
padding-left: 16px;
}
#whale-lens-upload label {
position: relative;
font-size: 0;
color: transparent;
width: 24px;
height: 24px;
display: block;
background-size: 24px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
#whale-lens-upload label::after {
content: "이미지 업로드";
display: block;
position: absolute;
top: 50%;
left: 34px;
margin-top: -7px;
white-space: nowrap;
color: #444;
font-family: "Dotum", "돋움", "Helvetica", "Apple SD Gothic Neo", sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1;
}
#whale-lens-upload input[type="file"] {
display: none;
}
#whale-lens-upload input[type="file"]:disabled + label,
#whale-lens-upload input[type="file"][disabled="disabled"] + label {
cursor: wait !important;
}
#whale-lens-upload input[type="file"]:disabled + label::after,
#whale-lens-upload input[type="file"][disabled="disabled"] + label::after {
content: "처리중입니다...";
}

View File

@ -0,0 +1,45 @@
body.whale-extension button[class^="header_btn_camera"] {
display: none;
}
body.whale-extension button[class^="header_btn_close"] {
display: none;
}
body.whale-extension div[class^="footer_u_sca"] {
display: none;
}
#whale-lens-upload {
display: none;
}
body.whale-extension #whale-lens-upload {
position: absolute;
top: 0;
left: 0;
width: 24px;
height: 24px;
overflow: hidden;
display: block;
padding: 12px 17px;
}
body.whale-extension #whale-lens-upload label {
font-size: 0;
color: transparent;
width: 24px;
height: 24px;
display: block;
background-size: 24px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
body.whale-extension #whale-lens-upload input[type="file"] {
display: none;
}
body.whale-extension #whale-lens-upload input[type="file"]:disabled + label,
body.whale-extension
#whale-lens-upload
input[type="file"][disabled="disabled"]
+ label {
cursor: wait !important;
}

View File

@ -0,0 +1,31 @@
<svg
width="50"
height="50"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
style="background: 0 0"
>
<circle
cx="50"
cy="50"
fill="none"
stroke-linecap="round"
r="42"
stroke-width="6"
stroke="#444"
stroke-dasharray="65.97344572538566 65.97344572538566"
transform="rotate(79.867 50 50)"
>
<animateTransform
attributeName="transform"
type="rotate"
calcMode="linear"
values="0 50 50;360 50 50"
keyTimes="0;1"
dur="1.5s"
begin="0s"
repeatCount="indefinite"
/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 734 B

View File

@ -0,0 +1,32 @@
<svg
width="50"
height="50"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
class="lds-dual-ring"
style="background: 0 0"
>
<circle
cx="50"
cy="50"
fill="none"
stroke-linecap="round"
r="42"
stroke-width="10"
stroke="#FFF"
stroke-dasharray="65.97344572538566 65.97344572538566"
transform="rotate(79.867 50 50)"
>
<animateTransform
attributeName="transform"
type="rotate"
calcMode="linear"
values="0 50 50;360 50 50"
keyTimes="0;1"
dur="1.5s"
begin="0s"
repeatCount="indefinite"
/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 486.3 486.3"
style="enable-background: new 0 0 486.3 486.3"
xml:space="preserve"
>
<path
fill="#444"
d="M395.5 135.8c-5.2-30.9-20.5-59.1-43.9-80.5-26-23.8-59.8-36.9-95-36.9-27.2 0-53.7 7.8-76.4 22.5-18.9 12.2-34.6 28.7-45.7 48.1-4.8-.9-9.8-1.4-14.8-1.4-42.5 0-77.1 34.6-77.1 77.1 0 5.5.6 10.8 1.6 16C16.7 200.7 0 232.9 0 267.2c0 27.7 10.3 54.6 29.1 75.9 19.3 21.8 44.8 34.7 72 36.2h86.8c7.5 0 13.5-6 13.5-13.5s-6-13.5-13.5-13.5h-85.6C61.4 349.8 27 310.9 27 267.1c0-28.3 15.2-54.7 39.7-69 5.7-3.3 8.1-10.2 5.9-16.4-2-5.4-3-11.1-3-17.2 0-27.6 22.5-50.1 50.1-50.1 5.9 0 11.7 1 17.1 3 6.6 2.4 13.9-.6 16.9-6.9 18.7-39.7 59.1-65.3 103-65.3 59 0 107.7 44.2 113.3 102.8.6 6.1 5.2 11 11.2 12 44.5 7.6 78.1 48.7 78.1 95.6 0 49.7-39.1 92.9-87.3 96.6h-73.7c-7.5 0-13.5 6-13.5 13.5s6 13.5 13.5 13.5h75.2c30.5-2.2 59-16.2 80.2-39.6 21.1-23.2 32.6-53 32.6-84-.1-56.1-38.4-106-90.8-119.8z"
/>
<path
fill="#444"
d="M324.2 280c5.3-5.3 5.3-13.8 0-19.1l-71.5-71.5c-2.5-2.5-6-4-9.5-4s-7 1.4-9.5 4l-71.5 71.5c-5.3 5.3-5.3 13.8 0 19.1 2.6 2.6 6.1 4 9.5 4s6.9-1.3 9.5-4l48.5-48.5v222.9c0 7.5 6 13.5 13.5 13.5s13.5-6 13.5-13.5V231.5l48.5 48.5c5.2 5.3 13.7 5.3 19 0z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 486.3 486.3"
style="enable-background: new 0 0 486.3 486.3"
xml:space="preserve"
>
<path
fill="#fff"
d="M395.5 135.8c-5.2-30.9-20.5-59.1-43.9-80.5-26-23.8-59.8-36.9-95-36.9-27.2 0-53.7 7.8-76.4 22.5-18.9 12.2-34.6 28.7-45.7 48.1-4.8-.9-9.8-1.4-14.8-1.4-42.5 0-77.1 34.6-77.1 77.1 0 5.5.6 10.8 1.6 16C16.7 200.7 0 232.9 0 267.2c0 27.7 10.3 54.6 29.1 75.9 19.3 21.8 44.8 34.7 72 36.2h86.8c7.5 0 13.5-6 13.5-13.5s-6-13.5-13.5-13.5h-85.6C61.4 349.8 27 310.9 27 267.1c0-28.3 15.2-54.7 39.7-69 5.7-3.3 8.1-10.2 5.9-16.4-2-5.4-3-11.1-3-17.2 0-27.6 22.5-50.1 50.1-50.1 5.9 0 11.7 1 17.1 3 6.6 2.4 13.9-.6 16.9-6.9 18.7-39.7 59.1-65.3 103-65.3 59 0 107.7 44.2 113.3 102.8.6 6.1 5.2 11 11.2 12 44.5 7.6 78.1 48.7 78.1 95.6 0 49.7-39.1 92.9-87.3 96.6h-73.7c-7.5 0-13.5 6-13.5 13.5s6 13.5 13.5 13.5h75.2c30.5-2.2 59-16.2 80.2-39.6 21.1-23.2 32.6-53 32.6-84-.1-56.1-38.4-106-90.8-119.8z"
/>
<path
fill="#fff"
d="M324.2 280c5.3-5.3 5.3-13.8 0-19.1l-71.5-71.5c-2.5-2.5-6-4-9.5-4s-7 1.4-9.5 4l-71.5 71.5c-5.3 5.3-5.3 13.8 0 19.1 2.6 2.6 6.1 4 9.5 4s6.9-1.3 9.5-4l48.5-48.5v222.9c0 7.5 6 13.5 13.5 13.5s13.5-6 13.5-13.5V231.5l48.5 48.5c5.2 5.3 13.7 5.3 19 0z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1 @@
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([,function(e,t,n){"use strict";n.r(t),n.d(t,"onClickBrowserAction",(function(){return r})),n.d(t,"initializeActionButtonState",(function(){return a})),n.d(t,"updateActionButtonState",(function(){return i})),n.d(t,"onMessageCropLayerEnded",(function(){return u})),n.d(t,"onMessageCloseLayer",(function(){return l}));const r=async({id:e})=>{const t=await new Promise(e=>{whale.tabs.captureVisibleTab({format:"jpeg"},t=>{e(t)})});whale.browserAction.disable(e),whale.tabs.sendMessage(e,{type:"openCropLayer",options:{image:t}})},o=({id:e,url:t})=>{(e=>{const t=new URL(e);return["http:","https:"].includes(t.protocol)})(t)||whale.browserAction.disable(e)},a=()=>{whale.windows.getAll({populate:!0,windowTypes:["normal"]},e=>{e.forEach(e=>{e.tabs.forEach(e=>o(e))})})},i=(e,t,n)=>{o(n)},u=(e,{tab:t})=>{whale.browserAction.enable(t.id)},l=()=>{whale.tabs.getAllInWindow(null,e=>{e.forEach(e=>{whale.tabs.sendMessage(e.id,{type:"closeLayer"})})})}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
{
"background": {
"scripts": [ "js/background.js" ]
},
"browser_action": {
"default_icon": {
"16": "img/icon16.png",
"32": "img/icon32.png",
"48": "img/icon48.png"
}
},
"content_scripts": [ {
"js": [ "js/content.script.js" ],
"matches": [ "*://*/*" ],
"run_at": "document_start"
}, {
"css": [ "css/search.result.css" ],
"include_globs": [ "*://msearch.shopping.naver.com/search/image*" ],
"js": [ "js/SearchResult/content.script.js" ],
"matches": [ "*://msearch.shopping.naver.com/*" ],
"run_at": "document_end"
} ],
"default_locale": "ko",
"description": "__MSG_APP_DESCRIPTION__",
"icons": {
"128": "img/icon128.png",
"16": "img/icon16.png"
},
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Hsd+PJV9gK5uz7MHYehqVIuG18ZP37YLAqvRaA/AFDrvp1ZjRBvYt9FwATj067uDHx9i60XtiFNvY8oQ4YtEYIf/51BbpB4nUyQK/oEuirgFfag9UpVbmocaZrTcCA24zEw1InYCO7n0n1ETn1MTlsqY6VgLp9uQ0wWNI3cj5a90gdATIJ1IQzg2LhPg0sxIBMMzrH9baNwKWLmJq1367MIoskgl1eDTN8HzgyWYkH1GQZMX1TTdfxXoJGZWR+QzpX41rxCEP+cUyVkJKyCrVsLDAkd67ZDALN9/vP6S+YaIplo0yHAX6wzlUhclhbjTMmVnswyRRWgIYeeizDIPwIDAQAB",
"manifest_version": 2,
"minimum_whale_version": "1.5.72.0",
"name": "__MSG_APP_NAME__",
"permissions": [ "activeTab", "contextMenus", "tabs", "utility", "*://api.scopic.naver.com/*" ],
"update_url": "https://store.whale.naver.com/update/whx",
"version": "1.1.11",
"web_accessible_resources": [ "js/CropLayer/window.script.js", "css/lenscrop.element.css", "img/shoppinglens.logo.png", "img/close.png", "img/button.loading.dark.svg", "img/button.loading.svg", "img/button.upload.dark.svg", "img/button.upload.svg" ],
"whale_extension": true
}

View File

@ -18,13 +18,14 @@ class CategoryManager:
for index, row in df.iterrows():
# 첫 번째 열에서 카테고리 코드와 계층 정보 추출
match = re.match(r"\[(\w{8})\]\s(.+)", row[0]) # 첫 번째 열
match = re.match(r"\[(\w{8})\]\s(.+)", row.iloc[0]) # 첫 번째 열
if match:
category_code = match.group(1)
category_hierarchy = match.group(2).split("-")
# 두 번째 열이 비어 있으면 모두 허용으로 간주
is_allowed = row[1] if pd.notna(row[1]) else True
is_allowed = row.iloc[1] if pd.notna(row.iloc[1]) else True # 두 번째 열
category_list.append({
"category_code": category_code,
@ -42,14 +43,14 @@ class CategoryManager:
f"스스 카테고리 시트 B열에 금지여부를 False 또는 True로 표시해주세요."
)
else:
print(f"예기치 않은 경고 발생: ERR {e}")
self.logger.log(f"예기치 않은 경고 발생: ERR {e}", level=logging.DEBUG, exc_info=True)
return []
except Exception as e:
print(f"엑셀에서 카테고리를 로드하는 중 오류 발생: ERR {e}")
self.logger.log(f"엑셀에서 카테고리를 로드하는 중 오류 발생: ERR {e}", level=logging.DEBUG, exc_info=True)
return []
def find_category_code_ori(self, detailed_category: List[Optional[str]]) -> Optional[str]:
def find_category_code(self, detailed_category: List[Optional[str]]) -> Optional[str]:
"""상세 카테고리 리스트를 기반으로 카테고리 코드를 검색."""
# 빈 문자열을 None으로 변환
detailed_category = [cat if cat else None for cat in detailed_category]
@ -62,7 +63,7 @@ class CategoryManager:
and category["category4Name"] == detailed_category[3]
):
# # 디버깅 로그 추가
# print(f"매칭된 카테고리: {category['category_code']}, is_allowed: {category['is_allowed']}")
# self.logger.log(f"매칭된 카테고리: {category['category_code']}, is_allowed: {category['is_allowed']}", level=logging.DEBUG)
# 허용된 카테고리만 반환
category_hierarchy = "-".join(
@ -78,7 +79,7 @@ class CategoryManager:
return None
def find_category_code(self, detailed_category: List[Optional[str]]) -> Optional[str]:
def find_category_code_inclode_ETC(self, detailed_category: List[Optional[str]]) -> Optional[str]:
"""
상세 카테고리 리스트를 기반으로 카테고리 코드를 검색.
1. 3번째 카테고리까지 일치하는 항목을 찾음.
@ -120,7 +121,7 @@ class CategoryManager:
candidate["category4Name"]
])
)
logger.debug(f"'기타' 포함된 카테고리 선택: {category_hierarchy}")
self.logger.log(f"'기타' 포함된 카테고리 선택: {category_hierarchy}", level=logging.DEBUG)
return f"[{candidate['category_code']}] {category_hierarchy}"
# 4번째 카테고리에 '기타'가 없을 경우, 가장 유사한 카테고리 선택
@ -146,11 +147,11 @@ class CategoryManager:
best_match["category4Name"]
])
)
logger.debug(f"가장 유사한 카테고리 선택: {category_hierarchy}")
self.logger.log(f"가장 유사한 카테고리 선택: {category_hierarchy}", level=logging.DEBUG)
return f"[{best_match['category_code']}] {category_hierarchy}"
# 매칭된 카테고리가 없는 경우
logger.error(f"3번째 카테고리까지 일치하는 항목을 찾을 수 없습니다: {detailed_category}")
self.logger.log(f"3번째 카테고리까지 일치하는 항목을 찾을 수 없습니다: {detailed_category}", level=logging.ERROR, exc_info=True)
return None
# def is_category_allowed(self, category: List[Optional[str]]) -> bool:
@ -163,7 +164,7 @@ class CategoryManager:
# and cat["category4Name"] == (category[3] if len(category) > 3 else None)
# ):
# # 디버깅 로그 추가
# print(f"매칭된 카테고리: [{category['category_code']}], is_allowed: {cat['is_allowed']}")
# self.logger.log(f"매칭된 카테고리: [{category['category_code']}], is_allowed: {cat['is_allowed']}", level=logging.DEBUG)
# return bool(cat["is_allowed"]) # 허용이면 True, 금지면 False
# # 카테고리를 찾지 못하면 기본적으로 금지(False)로 간주
@ -175,13 +176,13 @@ class CategoryManager:
카테고리 코드(faaabigd) 추출하여 is_allowed 값을 반환.
"""
if not category_with_hierarchy:
print("카테고리 정보가 None입니다.")
self.logger.log(f"카테고리 정보가 None입니다.", level=logging.DEBUG)
return True
# 카테고리 코드 추출 (대괄호 안의 8자리 코드)
match = re.match(r"\[(\w{8})\]", category_with_hierarchy)
if not match:
print(f"잘못된 카테고리 형식: {category_with_hierarchy}")
self.logger.log(f"잘못된 카테고리 형식: {category_with_hierarchy}", level=logging.DEBUG)
return True
category_code = match.group(1)
@ -189,11 +190,11 @@ class CategoryManager:
# 카테고리 리스트에서 코드 검색
for category in self.category_list:
if category["category_code"] == category_code:
print(f"카테고리 코드 '{category_code}'의 is_allowed: {category['is_allowed']}")
self.logger.log(f"카테고리 코드 '{category_code}'의 is_allowed: {category['is_allowed']}", level=logging.DEBUG)
return bool(category["is_allowed"])
# 카테고리를 찾지 못한 경우 기본적으로 False 반환
print(f"카테고리 코드를 찾을 수 없습니다: {category_code}")
self.logger.log(f"카테고리 코드를 찾을 수 없습니다: {category_code}", level=logging.DEBUG)
return True
def find_most_common_category(self, detailed_products: List[Dict]) -> Optional[str]:
@ -255,4 +256,4 @@ class CategoryManager:
# # 가장 빈도가 높은 카테고리 찾기
# most_common_category = category_manager.find_most_common_category(detailed_products)
# print(f"가장 빈도가 높은 카테고리: {most_common_category}")
# self.logger.log(f"가장 빈도가 높은 카테고리: {most_common_category}", level=logging.DEBUG)

View File

@ -9,11 +9,11 @@ class DatabaseManager():
self.logger = logger
self.db_path = db_name
# self.logger.log(f"DBManager 초기화 완료", level=logging.DEBUG)
self.create_table()
self.create_table(self.db_path)
def create_table(self):
def create_table(self, db_path):
try:
with sqlite3.connect(self.db_path) as conn:
with sqlite3.connect(db_path) as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
pc_url TEXT,
@ -44,14 +44,30 @@ class DatabaseManager():
except sqlite3.Error as e:
self.logger.log(f"데이터베이스 저장 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
def fetch_all(self):
# def fetch_all(self):
# try:
# with sqlite3.connect(self.db_path, check_same_thread=False) as conn:
# df = pd.read_sql_query("SELECT * FROM items", conn)
# self.logger.log(f"데이터베이스에서 데이터 로드 완료", level=logging.DEBUG)
# return df
# except sqlite3.Error as e:
# self.logger.log(f"데이터베이스 읽기 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
# return pd.DataFrame()
def fetch_all(self, db_path=None):
"""
데이터베이스에서 모든 데이터를 로드하는 메서드.
:param db_path: 사용할 데이터베이스 경로 (기본값은 self.db_path)
:return: 데이터프레임 형식으로 반환
"""
try:
with sqlite3.connect(self.db_path, check_same_thread=False) as conn:
db_path = db_path or self.db_path # 인자로 받은 db_path가 없으면 self.db_path 사용
with sqlite3.connect(db_path, check_same_thread=False) as conn:
df = pd.read_sql_query("SELECT * FROM items", conn)
self.logger.log(f"데이터베이스에서 데이터 로드 완료", level=logging.DEBUG)
self.logger.log(f"데이터베이스({db_path})에서 데이터 로드 완료", level=logging.DEBUG)
return df
except sqlite3.Error as e:
self.logger.log(f"데이터베이스 읽기 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
self.logger.log(f"데이터베이스({db_path}) 읽기 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return pd.DataFrame()
def update_item(self, product: Dict):

View File

@ -25,10 +25,18 @@ class ExcelExporter:
def save_to_excel(self, output_path="output.xlsx"):
df = self.fetch_data_from_db()
if df.empty:
logger.warning("DB에서 불러온 데이터가 없습니다.")
return False # 성공 여부 반환
# 조건에 맞는 데이터 필터링
filtered_df = df[(df['is_valid'] == 1) & (df['is_export'] == 0)]
if filtered_df.empty:
logger.warning("조건에 맞는 데이터가 없습니다.")
return False # 성공 여부 반환
app = xw.App(visible=False)
logger.debug("xlwings 시작")
@ -46,10 +54,26 @@ class ExcelExporter:
for index, row in df_subset.iterrows():
row_num = 4 + (index % 50)
logger.debug(f"{index + 1}번째 행 기록 시작: B{row_num}, C{row_num}, D{row_num}") # 셀 위치 로그 추가
logger.debug(f"{index + 1}번째 행 기록 시작: B{row_num}, C{row_num}, D{row_num}, F{row_num}, G{row_num}, H{row_num}") # 셀 위치 로그 추가
ws.range(f'B{row_num}').value = row['pc_url']
ws.range(f'C{row_num}').value = row['name']
ws.range(f'D{row_num}').value = row['price']
ws.range(f'F{row_num}').value = row['tags']
ws.range(f'G{row_num}').value = row['category_code']
ws.range(f'H{row_num}').value = row['memo']
# 데이터베이스 업데이트
self.db_manager.update_item({
'id': row['id'],
'generated_Title': row.get('generated_Title', None),
'category_code': row['category_code'],
'tags': row['tags'],
'margin_price': row.get('margin_price', None),
'memo': row['memo'],
'is_valid': row['is_valid'],
'is_export': 1 # is_export를 1로 설정
})
logger.debug(f"{index + 1}번째 행 기록 완료")
wb.save(part_file_name) # SaveCopyAs 대신 save 사용

View File

@ -56,7 +56,7 @@ class GPTClient:
return ""
try:
keyword_data = json.loads(classify_response)
keyword_data = self.parse_json_response(classify_response)
large_keywords = keyword_data.get("large_keywords", [])
medium_keywords = keyword_data.get("medium_keywords", [])
small_keywords = keyword_data.get("small_keywords", [])
@ -65,29 +65,73 @@ class GPTClient:
return ""
# 2. 상품명 생성 프롬프트 생성
product_prompt = (
"너는 상품명 편집 전문가야. 주어진 중국 원본 상품명과 키워드를 활용하여 한국에서 잘 팔릴 수 있는 상품명으로 수정해야 해.\n\n"
"### 작업 규칙:\n"
"1. 대형키워드를 제외한 소형, 중형키워드를 조합하여 작성해야 함.\n"
"2. 상품명에는 고유 상품 코드가 포함되어야 함. 단! 숫자로만 이루어진 단어는 제외해야해\n"
"3. 키워드 배치는 자연스럽고 검색에 용이하도록 작성.\n"
f"4. 반드시 필수 키워드 중 최소 2~3개를 넣어서 상품명을 작성.\n\n"
"### 입력 데이터:\n"
f"- 원본 상품명: {original_name}\n"
f"- 필수 키워드: {unique_first_two_words}\n"
f"- 중형 키워드: {medium_keywords}\n"
f"- 소형 키워드: {small_keywords}\n\n"
f"- 상품명 길이제한: 공백 포함 {max_length}자 ~ {max_length+(max_length*0.4)}자 이내\n\n"
"### 출력 형식:\n"
"{ \"product_name\": \"수정된 상품명\" }\n"
)
if not unique_first_two_words or not medium_keywords or not small_keywords: # 검색되지 않는 상품일 경우 원본상품명을 활용해 상품명 생성
product_prompt = (
"너는 상품명 편집 전문가야. 주어진 중국 원본 상품명을 단어단위로 구분하여 한국에서 잘 팔릴 수 있는 상품명으로 수정해야 해.\n\n"
"### 작업 규칙:\n"
"1. 연도, 지역, 과도한 홍보, 이벤트, 추상적인 표현등인 모두 지워줘. 지양해야한다는 얘기야.\n"
"2. 상품명에는 고유 상품 코드가 포함되어야 함. 단! 숫자로만 이루어진 단어는 제외해야해\n"
"3. 키워드 배치는 자연스럽고 검색에 용이하도록 작성.\n"
"4. 괄호나 대괄호등이 있다면 해당내용들은 모두 버리고, 12345같은 의미없는 나열은 지양해야해.\n"
f"5. 중복을 피하고 중국어가 남아있으면 안되. \n\n"
"### 입력 데이터:\n"
f"- 원본 상품명: {original_name}\n"
f"- 상품명 길이제한: 공백 포함 {max_length}자 ~ {max_length+(max_length*0.4)}자 이내\n\n"
"### 출력 형식:\n"
"{ \"product_name\": \"수정된 상품명\" }\n"
)
else:
product_prompt = (
"너는 상품명 편집 전문가야. 주어진 중국 원본 상품명과 키워드를 활용하여 한국에서 잘 팔릴 수 있는 상품명으로 수정해야 해.\n\n"
"### 작업 규칙:\n"
"1. 반드시 대형키워드는 제외한 소형키워드, 중형키워드를 조합하여 작성해야 함.\n"
"2. 상품명에는 고유 상품 코드가 포함되어야 함. 단! 숫자로만 이루어진 단어는 제외해야해\n"
"3. 키워드 배치는 자연스럽고 검색에 용이하도록 작성.\n"
"4. 괄호나 대괄호등이 있다면 해당내용들은 모두 버리고, 12345같은 의미없는 나열은 지양해야해.\n"
f"5. 반드시 필수 키워드 중 최소 2~3개를 넣어서 상품명을 작성.\n\n"
"### 입력 데이터:\n"
f"- 원본 상품명: {original_name}\n"
f"- 필수 키워드: {unique_first_two_words}\n"
f"- 중형 키워드: {medium_keywords}\n"
f"- 소형 키워드: {small_keywords}\n\n"
f"- 상품명 길이제한: 공백 포함 {max_length}자 ~ {max_length+(max_length*0.4)}자 이내\n\n"
"### 출력 형식:\n"
"{ \"product_name\": \"수정된 상품명\" }\n"
)
# GPT에게 상품명 생성 요청
product_response = self.ask(product_prompt)
try:
product_data = json.loads(product_response)
product_data = self.parse_json_response(product_response)
return product_data.get("product_name", "").strip()
except json.JSONDecodeError as e:
self.logger.log(f"Error parsing product name from GPT response: {e}", level=logging.ERROR, exc_info=True)
return ""
def parse_json_response(self, classify_response: str) -> dict:
"""
주어진 응답에서 JSON 데이터를 추출하고 파싱합니다.
"""
try:
self.logger.log(f"classify_response : {classify_response}", level=logging.DEBUG)
# 정규식을 사용하여 JSON 블록 추출
match = re.search(r"\{.*\}", classify_response, re.DOTALL)
if not match:
self.logger.log("JSON 블록을 찾을 수 없습니다.")
return {}
json_str = match.group(0) # JSON 문자열 추출
result = json.loads(json_str) # JSON 디코딩
self.logger.log(f"result : {result}", level=logging.DEBUG)
return result
except json.JSONDecodeError as e:
self.logger.log(f"파싱 오류: {e}. 응답 내용: {classify_response}", level=logging.ERROR, exc_info=True)
return {}

View File

@ -1,30 +1,38 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox
from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox, QFileDialog
from PySide6.QtCore import Slot
import os
import logging
from src.playwright_thread import PlaywrightThread
from src.excel_export import ExcelExporter
logger = logging.getLogger(__name__)
from src.post_processor import PostProcessor
from src.xlsProcessingThread import XLSProcessingThread
class TaobaoScraperApp(QWidget):
def __init__(self, db_manager):
def __init__(self, logger, db_manager):
super().__init__()
self.logger = logger
self.db_manager = db_manager
self.setWindowTitle("Taobao Scraper")
self.layout = QVBoxLayout()
self.start_button = QPushButton("시작")
self.start_button.clicked.connect(self.start_scraping)
# self.collect_button = QPushButton("수집")
# self.collect_button.clicked.connect(self.collect_data)
self.excel_button = QPushButton("엑셀출력")
self.excel_button.clicked.connect(self.save_to_excel)
self.post_db__button = QPushButton("DB로 후처리")
self.post_db__button.clicked.connect(self.post_process_by_DB)
self.post_xls_button = QPushButton("수집맨xls로 후처리")
self.post_xls_button.clicked.connect(self.post_process_by_xls)
self.close_button = QPushButton("닫기")
self.close_button.clicked.connect(self.close)
self.layout.addWidget(QLabel("Taobao Scraper"))
self.layout.addWidget(self.start_button)
# self.layout.addWidget(self.collect_button)
self.layout.addWidget(self.post_db__button)
self.layout.addWidget(self.post_xls_button)
self.layout.addWidget(self.excel_button)
self.layout.addWidget(self.close_button)
@ -32,17 +40,78 @@ class TaobaoScraperApp(QWidget):
self.playwright_thread = PlaywrightThread(self.db_manager)
self.playwright_thread.data_collected.connect(self.on_data_collected)
self.excel_exporter = ExcelExporter(self.db_manager)
self.postProcessor = PostProcessor(self.logger, self.db_manager)
@Slot()
def start_scraping(self):
logger.info("Playwright 스레드 시작")
self.logger.log(f"Playwright 스레드 시작.", level=logging.INFO)
self.playwright_thread.start()
@Slot()
def collect_data(self):
logger.info("수집 버튼 클릭됨 - 데이터 수집 시작")
self.playwright_thread.run()
def post_process_by_DB(self):
self.logger.log(f"수집된 DB로 후처리.", level=logging.INFO)
self.postProcessor.post_by_DB()
@Slot()
def post_process_by_xls(self):
self.logger.log(f"수집맨 XLS로 후처리.", level=logging.INFO)
default_folder = os.path.join(os.getcwd(), 'XLS')
selected_folder = QFileDialog.getExistingDirectory(None, "XLS 폴더 선택", default_folder)
if not selected_folder:
self.logger.warning("폴더 선택이 취소되었습니다.")
return
self.logger.info(f"선택된 폴더: {selected_folder}")
self.postProcessor.post_by_XLS(selected_folder)
def post_process_by_xls(self):
self.logger.log(f"수집맨 XLS로 후처리.", level=logging.INFO)
default_folder = os.path.join(os.getcwd(), 'XLS')
self.logger.log(f"1", level=logging.INFO)
# 폴더 선택 다이얼로그 설정 (폴더 트리 모드)
dialog = QFileDialog(self, "XLS 폴더 선택")
self.logger.log(f"2", level=logging.INFO)
dialog.setFileMode(QFileDialog.Directory)
self.logger.log(f"3", level=logging.INFO)
dialog.setOption(QFileDialog.ShowDirsOnly, True)
self.logger.log(f"4", level=logging.INFO)
dialog.setDirectory(default_folder)
self.logger.log(f"5", level=logging.INFO)
# 네이티브 다이얼로그 비활성화
dialog.setOption(QFileDialog.DontUseNativeDialog, True)
# 비차단 방식으로 다이얼로그 열기
dialog.fileSelected.connect(self.on_folder_selected) # 폴더 선택 시 슬롯 호출
dialog.open()
@Slot(str)
def on_folder_selected(self, selected_folder):
"""
폴더가 선택되었을 호출되는 슬롯
"""
self.logger.log(f"선택된 폴더: {selected_folder}", level=logging.INFO)
# 스레드를 생성하여 작업 실행
self.xls_thread = XLSProcessingThread(self.postProcessor, selected_folder)
self.xls_thread.progress.connect(self.on_xls_progress)
self.xls_thread.start()
@Slot(str)
def on_xls_progress(self, message):
"""
스레드에서 전달된 진행 상태를 처리
"""
self.logger.log(message, level=logging.INFO)
@Slot()
def save_to_excel(self):

View File

@ -157,26 +157,26 @@ class NaverParser:
# 사용 예제
from categoryManager import CategoryManager
if __name__ == "__main__":
excel_path = "baseXLS_Percenty.xlsx"
category_manager = CategoryManager(excel_path)
# # 사용 예제
# from categoryManager import CategoryManager
# if __name__ == "__main__":
# excel_path = "baseXLS_Percenty.xlsx"
# category_manager = CategoryManager(excel_path)
parser = NaverParser()
keyword = "고양이숨숨집"
result = parser.search_and_parse(keyword)
# parser = NaverParser()
# keyword = "고양이숨숨집"
# result = parser.search_and_parse(keyword)
# detailed_products에서 category_code 추가
for product in result["detailed_products"]:
category_code = category_manager.find_category_code(product["category"])
product["category_code"] = category_code
# # detailed_products에서 category_code 추가
# for product in result["detailed_products"]:
# category_code = category_manager.find_category_code(product["category"])
# product["category_code"] = category_code
most_common_category = category_manager.find_most_common_category(result["detailed_products"])
# most_common_category = category_manager.find_most_common_category(result["detailed_products"])
print(f"검색 결과: {keyword}\n{result}")
# print(f"검색 결과: {keyword}\n{result}")
print(f"cat 검색 결과: {most_common_category}")
# print(f"cat 검색 결과: {most_common_category}")
# print("검색 결과:", json.dumps(result, ensure_ascii=False, indent=4))
# # print("검색 결과:", json.dumps(result, ensure_ascii=False, indent=4))

View File

@ -1,30 +1,55 @@
import logging
import pandas as pd
from typing import Dict, List
import os, sys
from PySide6.QtWidgets import QFileDialog
from pywinauto import Application, findwindows, timings
from pywinauto.controls.hwndwrapper import HwndWrapper
import configparser
from src.shoppingLens import ShoppingLensScraper
from src.titleManager import TitleManager
from src.categoryManager import CategoryManager
from src.naver_parser import NaverParser
from src.gpt_client import GPTClient
import requests
from bs4 import BeautifulSoup
class MainProcessor:
def __init__(self, logger, db_manager, shopping_lens, title_manager, naver_parser, categoryManager, gpt, config_path="config.ini"):
class PostProcessor:
def __init__(self, logger, db_manager):
self.logger = logger
self.db_manager = db_manager
self.shopping_lens = shopping_lens
self.title_manager = title_manager
self.categoryManager = categoryManager
self.naver_parser = naver_parser
self.gpt = gpt
base_xls_path = 'baseXLS_Percenty.xlsx'
config_path = 'config.ini'
self.gpt = GPTClient(self.logger, api_key='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')
self.shopping_lens = ShoppingLensScraper(self.logger)
self.title_manager = TitleManager(self.logger, self.gpt)
self.categoryManager = CategoryManager(self.logger, base_xls_path)
self.naver_parser = NaverParser(self.logger)
# 설정 파일 로드
self.config = configparser.ConfigParser()
self.config.read(config_path)
self.read_config(config_path)
# 필터 데이터 로드
self.banned_tags = set(self.config.get("Filters", "banned_tags", fallback="").split(","))
self.banned_words = set(self.config.get("Filters", "banned_words", fallback="").split(","))
self.disallowed_words = set(self.config.get("Filters", "disallowed_words", fallback="").split(","))
def read_config(self, config_path):
try:
# 파일을 UTF-8로 열어서 ConfigParser로 읽기
with open(config_path, 'r', encoding='utf-8') as config_file:
self.config.read_file(config_file)
except UnicodeDecodeError as e:
self.logger.error(f"Config 파일 읽기 중 인코딩 오류 발생: {e}")
raise
except FileNotFoundError:
self.logger.error(f"Config 파일을 찾을 수 없습니다: {config_path}")
raise
def get_base_dir(self):
"""
실행 환경에 따라 base_dir을 설정하는 메서드.
@ -85,17 +110,154 @@ class MainProcessor:
self.logger.log(f"웨일 창 탐색 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return None
def post_by_DB(self):
# 1. DB에서 처리되지 않은 상품 가져오기
products = self.db_manager.fetch_all().query("is_export == 0").to_dict('records')
self.logger.log(f"처리 대상 상품 {len(products)}개 로드 완료", level=logging.DEBUG)
def process_products(self):
self.process_products(products)
# def post_by_XLS(self):
# default_folder = os.path.join(os.getcwd(), 'XLS')
# selected_folder = QFileDialog.getExistingDirectory(None, "XLS 폴더 선택", default_folder)
# if not selected_folder:
# self.logger.warning("폴더 선택이 취소되었습니다.")
# return
# self.logger.info(f"선택된 폴더: {selected_folder}")
# self._post_by_XLS(selected_folder)
def post_by_XLS(self, folder_path):
import openpyxl
"""
주어진 폴더 경로에서 모든 엑셀 파일을 순회하며 데이터를 수집 DB에 저장.
:param folder_path: 엑셀 파일이 위치한 폴더 경로
"""
try:
# 폴더 내 모든 엑셀 파일 가져오기
excel_files = [f for f in os.listdir(folder_path) if f.endswith('.xls') or f.endswith('.xlsx')]
if not excel_files:
self.logger.log(f"엑셀 파일이 폴더 '{folder_path}'에 없습니다.", level=logging.WARNING)
return
self.logger.log(f"{len(excel_files)}개의 엑셀 파일을 발견했습니다.", level=logging.DEBUG)
# DB 초기화
db_name = "xls_db.db"
self.db_manager.create_table(db_name)
for excel_file in excel_files:
file_path = os.path.join(folder_path, excel_file)
self.logger.log(f"엑셀 파일 처리 중: {file_path}", level=logging.DEBUG)
try:
# 엑셀 파일 열기
workbook = openpyxl.load_workbook(file_path, data_only=True)
sheet = workbook.active
# 데이터 추출 (B4~B53, C4~C53)
items = []
for row in range(4, 54): # 4번 행부터 53번 행까지
pc_url = sheet[f"B{row}"].value # PC_URL
name = sheet[f"C{row}"].value # 상품명
if not pc_url or not name:
self.logger.log(f"필수 데이터 누락: 행 {row} (PC_URL: {pc_url}, Name: {name})", level=logging.WARNING)
continue
id_value = self.parse_id_from_url(pc_url) # URL에서 ID 추출
price, image_url = self.fetch_price_and_image(pc_url) # 가격 및 이미지 URL 수집
if id_value and price and image_url:
items.append((id_value, pc_url, name, price, image_url, 0)) # sales는 0으로 설정
if items:
self.db_manager.insert_items(items)
self.logger.log(f"{file_path}의 데이터를 DB에 저장했습니다.", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"엑셀 파일 처리 중 오류 발생: {file_path}, 오류: {e}", level=logging.ERROR, exc_info=True)
# 처리되지 않은 상품 로드 및 후처리
# products = self.db_manager.fetch_all().query("is_export == 0").to_dict('records')
products = self.db_manager.fetch_all(db_path=db_name).query("is_export == 0").to_dict('records')
self.logger.log(f"{len(products)}개의 처리되지 않은 상품 로드 완료.", level=logging.DEBUG)
self.process_products(products)
except Exception as e:
self.logger.log(f"XLS 데이터 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
def parse_id_from_url(self, url):
"""
URL에서 ID 추출
"""
try:
id_value = url.split("id=")[1]
return id_value
except IndexError:
self.logger.log(f"URL에서 ID를 추출하지 못했습니다: {url}", level=logging.ERROR, exc_info=True)
return None
def fetch_price_and_image(self, url):
"""
URL로부터 가격과 이미지 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",
"Cache-Control": "max-age=0"
}
response = requests.get(url, headers=headers, stream=True)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
self.logger.log(f"soup : {soup}", level=logging.DEBUG)
# 이미지 URL 추출
image_element = soup.select_one(".mainPicWrap--Ns5WQiHr img")
image_url = image_element["src"] if image_element else None
# 가격 추출
price_element = soup.select_one(".text--Mdqy24Ex span")
price = None
if price_element:
price = price_element.text.strip()
else:
# 가격 요소가 특정 클래스로 없을 경우, "¥"가 포함된 span을 기준으로 다음 요소 찾기
span_with_yen = soup.find("span", text=lambda t: "" in t if t else False)
if span_with_yen and span_with_yen.find_next_sibling():
price = span_with_yen.find_next_sibling().text.strip()
if not image_url or not price:
self.logger.log(f"이미지 또는 가격 정보를 가져오지 못했습니다: {url}", level=logging.WARNING)
return price, image_url
except Exception as e:
self.logger.log(f"URL에서 데이터를 가져오는 중 오류 발생: {url}, 오류: {e}", level=logging.ERROR, exc_info=True)
return None, None
def process_products(self, products):
# 쇼핑렌즈를 위한 웹브라우저 준비
whale_window = self.start_whale_browser()
# time.sleep(600)
# 1. DB에서 처리되지 않은 상품 가져오기
products = self.db_manager.fetch_all().query("is_export == 0").to_dict('records')
self.logger.log(f"처리 대상 상품 {len(products)}개 로드 완료", level=logging.DEBUG)
# # 1. DB에서 처리되지 않은 상품 가져오기
# products = self.db_manager.fetch_all().query("is_export == 0").to_dict('records')
# self.logger.log(f"처리 대상 상품 {len(products)}개 로드 완료", level=logging.DEBUG)
for product in products:
try:
@ -127,11 +289,6 @@ class MainProcessor:
# 금지 카테고리 확인
if naver_data:
# isvalid_category = self.categoryManager.is_category_allowed(
# naver_data["detailed_products"][0]["category"]
# )
# self.logger.log(f"isvalid_category : {isvalid_category}", level=logging.DEBUG)
isvalid_category = self.categoryManager.is_allowed_by_category_code(most_common_category)
self.logger.log(f"isvalid_category : {isvalid_category}", level=logging.DEBUG)
@ -144,10 +301,6 @@ class MainProcessor:
# 태그 필터링 및 병합
tags = self.filter_and_merge_tags(naver_data)
# 5. 가격 계산
# base_price = product['price'] * 200 * 0.04
# margin_price = base_price * 1.24
additional_margin = self.calculate_additional_margin(scraped_data)
self.logger.log(f"더하기마진(=팔린가격) : {additional_margin}", level=logging.DEBUG)
@ -224,28 +377,13 @@ class MainProcessor:
return "메모 없음"
# 각 클래스 인스턴스 생성 및 실행
if __name__ == "__main__":
from databaseManager import DatabaseManager
from shoppingLens import ShoppingLensScraper
from titleManager import TitleManager
from categoryManager import CategoryManager
from naver_parser import NaverParser
from gpt_client import GPTClient
from loggerModule import Logger
# # 각 클래스 인스턴스 생성 및 실행
# if __name__ == "__main__":
# from databaseManager import DatabaseManager
# from loggerModule import Logger
base_xls_path = 'baseXLS_Percenty.xlsx'
config_path = 'config.ini'
# logger = Logger(log_file="post_Processor.log", logger_name="PoseProcessorLogger", level=logging.DEBUG)
# db_manager = DatabaseManager(logger)
logger = Logger(log_file="post_Processor.log", logger_name="PoseProcessorLogger", level=logging.DEBUG)
gpt = GPTClient(logger, api_key='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')
db_manager = DatabaseManager(logger)
shopping_lens = ShoppingLensScraper(logger)
title_manager = TitleManager(logger, gpt)
categoryManager = CategoryManager(logger, base_xls_path)
naver_parser = NaverParser(logger)
processor = MainProcessor(logger, db_manager, shopping_lens, title_manager, naver_parser, categoryManager, gpt, config_path)
processor.process_products()
# processor = PostProcessor(logger, db_manager)
# processor.process_products()

View File

@ -1,13 +1,13 @@
from pywinauto import Application, findwindows, timings
from pywinauto.timings import wait_until
import time, logging, os, re
from translatepy.translators.google import GoogleTranslate
from deep_translator import GoogleTranslator
from collections import Counter
import logging
class ShoppingLensScraper:
def __init__(self, logger=None):
self.logger = logger
self.gtranslator = GoogleTranslate(service_url="translate.google.cn")
self.gtranslator = GoogleTranslator(source="zh-CN", target="ko")
def translate_name(self, text: str) -> str:
"""
@ -16,16 +16,17 @@ class ShoppingLensScraper:
:return: 번역된 한국어 텍스트
"""
if not text.strip():
self.logger.log("빈 텍스트가 입력되었습니다.", level=logging.WARNING)
self.logger.log(f"빈 텍스트가 입력되었습니다.", level=logging.WARNING)
return ""
try:
# 번역 수행
result = self.gtranslator.translate(text, "Korean")
self.logger.log(f"번역 성공: {text} -> {result.result}", level=logging.INFO)
return result.result
result = self.gtranslator.translate(text)
self.logger.log(f"번역 성공: {text} -> {result}", level=logging.DEBUG)
return result
except Exception as e:
self.logger.log(f"번역 중 오류 발생: {e}", level=logging.ERROR)
self.logger.log(f"번역 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return "번역 실패"
def save_control_identifiers(self, window, output_file="debug_controls.txt"):
@ -43,7 +44,7 @@ class ShoppingLensScraper:
os.sys.stdout = original_stdout # stdout 복원
self.logger.log(f"컨트롤 식별자가 {output_file}에 저장되었습니다.", level=logging.INFO)
except Exception as e:
self.logger.log(f"컨트롤 식별자를 저장하는 중 오류 발생: {e}", level=logging.ERROR)
self.logger.log(f"컨트롤 식별자를 저장하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)

Binary file not shown.

15
src/test/rq.py Normal file
View File

@ -0,0 +1,15 @@
from requests_html import HTMLSession
def fetch_taobao_page(url):
session = HTMLSession()
response = session.get(url)
# JavaScript 렌더링
response.html.render(timeout=20) # JavaScript 실행 후 렌더링된 HTML
return response.html.html # 렌더링된 HTML 반환
# 사용 예시
url = "https://item.taobao.com/item.htm?id=848821992604"
html = fetch_taobao_page(url)
print(html)

View File

@ -0,0 +1,20 @@
from PySide6.QtCore import QThread, Signal
class XLSProcessingThread(QThread):
progress = Signal(str) # 진행 상태를 GUI에 전달할 시그널
def __init__(self, post_processor, folder_path):
super().__init__()
self.post_processor = post_processor
self.folder_path = folder_path
def run(self):
"""
스레드에서 실행할 작업
"""
try:
self.progress.emit(f"'{self.folder_path}'에서 XLS 파일 처리 시작")
self.post_processor.post_by_XLS(self.folder_path)
self.progress.emit("XLS 파일 처리 완료")
except Exception as e:
self.progress.emit(f"XLS 파일 처리 중 오류 발생: {e}")

Binary file not shown.

BIN
xls_db.db Normal file

Binary file not shown.

BIN
xlwings32-0.33.4.dll Normal file

Binary file not shown.

BIN
xlwings64-0.33.4.dll Normal file

Binary file not shown.