test4
27
main.py
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
BIN
requirements.txt
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"APP_DESCRIPTION": {
|
||||
"message": "NAVER Shopping Lens Whale extension application"
|
||||
},
|
||||
"APP_NAME": {
|
||||
"message": "NAVER Shopping Lens"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"APP_DESCRIPTION": {
|
||||
"message": "웨일 브라우저 네이버 쇼핑렌즈 확장앱"
|
||||
},
|
||||
"APP_NAME": {
|
||||
"message": "네이버 쇼핑렌즈"
|
||||
}
|
||||
}
|
||||
|
|
@ -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: "처리중입니다...";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 901 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 887 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -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"})})})}}]);
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 사용
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
91
src/gui.py
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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}")
|
||||