posted by 어서와3123 2026. 3. 1. 13:34

 

 

유투브 영상 주소

https://www.youtube.com/watch?v=C8uIOyRoDFQ

 

- YouTube

 

www.youtube.com

 

지피티 프로젝트 공유 (무료 로그인 해야 사용 할 수 있습니다)

https://chatgpt.com/g/g-p-69a3b47c62208191b5f0b4cae85b3e60-beuraendeukeonegteu-ddalggag-pyo/project

 

 

naver_shop.py

import customtkinter as ctk
import threading
import time
import os
import pyperclip
import re
from tkinter import messagebox
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager

class NaverShopAutomation:
def __init__(self):
self.driver = None

def setup_driver(self):
"""브라우저를 초기화하고 드라이버를 설정합니다."""
chrome_options = Options()
chrome_options.add_argument("--disable-blink-features=AutomationControlled")
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option("useAutomationExtension", False)
chrome_options.add_argument("--window-size=1600,1024")
 
service = Service(ChromeDriverManager().install())
self.driver = webdriver.Chrome(service=service, options=chrome_options)
 
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})

def open_login_page(self):
if not self.driver:
self.setup_driver()

def is_logged_in(self):
if not self.driver:
return False
try:
cookies = self.driver.get_cookies()
return any(cookie['name'] == 'NID_SES' for cookie in cookies)
except Exception:
return False

def navigate_to(self, url):
if self.driver:
self.driver.get(url)

def is_match(self, keyword, text):
"""키워드 유연한 매칭 (공백 무시, 순서 무관, 대소문자 무시)"""
if not keyword:
return True
 
# 1. 공백 제거 및 소문자화하여 전체 포함 확인 (기본)
clean_keyword = keyword.replace(" ", "").lower()
clean_text = text.replace(" ", "").lower()
if clean_keyword in clean_text:
return True
 
# 2. 키워드를 한글/영문/숫자 단위로 쪼개서 각각의 조각이 모두 포함되어 있는지 확인
# 예: "프로4" -> ["프로", "4"]
# 예: "버즈프로" -> ["버즈", "프로"]
chunks = re.findall(r'[a-zA-Z]+|[0-9]+|[가-힣]+', keyword.lower())
if not chunks:
return False
 
return all(chunk in clean_text for chunk in chunks)

def scrape_table_data(self, keyword):
"""키워드에 맞는 데이터를 수집하고 링크를 복사합니다."""
results = []
try:
wait = WebDriverWait(self.driver, 10)
rows = wait.until(EC.presence_of_all_elements_located((By.XPATH, "//table//tr[td]")))
 
for row in rows:
try:
product_name_cell = row.find_element(By.XPATH, "./td[1]")
product_name = product_name_cell.text.strip()
 
# 스마트 매칭 적용
if not self.is_match(keyword, product_name):
continue
 
# 나머지 데이터 추출
store = row.find_element(By.XPATH, "./td[2]").text.strip()
 
# 할인가/판매가 추출 (첫 번째 줄이 실제 판매가인 경우가 많음)
price_cell = row.find_element(By.XPATH, "./td[3]")
prices = [p.strip() for p in price_cell.text.split('\n') if p.strip()]
# 실제 구매가(할인가 혹은 첫 번째 가격)를 selling_price로 저장
selling_price = prices[0] if len(prices) > 0 else "N/A"
# 원래 정가(두 번째 가격)이 있으면 저장, 없으면 "N/A"
original_price = prices[1] if len(prices) > 1 else "N/A"
 
# 수수료
fee = row.find_element(By.XPATH, "./td[4]").text.strip().replace('\n', ' ')
 
# 예상 수익
profit = row.find_element(By.XPATH, "./td[5]").text.strip()
 
# 진행 상태
status = row.find_element(By.XPATH, "./td[6]").text.strip()
 
# 링크 발급일
issue_date = row.find_element(By.XPATH, "./td[7]").text.strip()
 
# 복사 버튼 찾기
copy_btn = row.find_element(By.XPATH, ".//button[contains(., '복사')] | .//span[contains(text(), '복사')]/ancestor::button")
 
# 1. 버튼이 보이도록 스크롤 및 안정화
self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", copy_btn)
time.sleep(0.5)
 
# 2. 클립보드 비우기 (새 링크가 오는지 확인용)
pyperclip.copy("")
 
# 3. ActionChains를 사용한 실제 클릭 시뮬레이션 (클립보드 권한 확보)
from selenium.webdriver.common.action_chains import ActionChains
try:
ActionChains(self.driver).move_to_element(copy_btn).click().perform()
except:
# 차선책: JS 클릭
self.driver.execute_script("arguments[0].click();", copy_btn)
 
# 4. 클립보드에 새 URL이 들어올 때까지 최대 2초 대기
url = ""
for _ in range(10): # 0.2초씩 10번 = 2초
time.sleep(0.2)
temp_url = pyperclip.paste().strip()
if temp_url and temp_url.startswith("http") and "brandconnect" not in temp_url:
url = temp_url
break
 
# 만약 여전히 못 가져왔다면 1.5초 더 대기 후 마지막 확인
if not url:
time.sleep(1.0)
url = pyperclip.paste().strip()
# 여전히 브랜드커넥트 주소라면 실패 처리
if "brandconnect" in url:
url = "링크 복사 실패 (수동 확인 필요)"
 
# 데이터 정리
item_data = {
"product_name": product_name,
"store": store,
"selling_price": selling_price,
"original_price": original_price,
"fee": fee,
"profit": profit,
"status": status,
"issue_date": issue_date,
"url": url
}
results.append(item_data)
 
# 팝업 알림이 뜨는 경우 확인 버튼 처리 (필요시)
try:
alert = self.driver.switch_to.alert
alert.accept()
except:
pass
 
except Exception as row_e:
print(f"Row skip error: {row_e}")
continue
 
except Exception as e:
print(f"Scrape error: {e}")
 
return results

class NaverShopApp(ctk.CTk):
def __init__(self):
super().__init__()

self.title("Naver Shop Data Scraper")
self.geometry("900x750")
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")

self.automation = NaverShopAutomation()
 
self.setup_ui()

def setup_ui(self):
# 상단 타이틀
self.label_title = ctk.CTkLabel(self, text="네이버 쇼핑 데이터 추출 도구", font=ctk.CTkFont(size=24, weight="bold"))
self.label_title.pack(pady=(20, 10))

# 메인 입력 프레임
self.input_frame = ctk.CTkFrame(self)
self.input_frame.pack(pady=10, padx=30, fill="x")

# URL 입력
ctk.CTkLabel(self.input_frame, text="1. 대상 URL 입력:").grid(row=0, column=0, padx=10, pady=10, sticky="w")
self.url_entry = ctk.CTkEntry(self.input_frame, width=600, placeholder_text="링크 발급 페이지 URL을 입력하세요")
self.url_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

# 키워드 입력
ctk.CTkLabel(self.input_frame, text="2. 검색 키워드:").grid(row=1, column=0, padx=10, pady=10, sticky="w")
self.keyword_entry = ctk.CTkEntry(self.input_frame, width=600, placeholder_text="예: 원두")
self.keyword_entry.grid(row=1, column=1, padx=10, pady=10, sticky="ew")

# 버튼 프레임
self.btn_frame = ctk.CTkFrame(self, fg_color="transparent")
self.btn_frame.pack(pady=10, padx=30, fill="x")

self.btn_login = ctk.CTkButton(self.btn_frame, text="단계 1: 브라우저 열기 & 로그인", command=self.handle_step_1, height=40)
self.btn_login.pack(side="left", padx=5, expand=True, fill="x")

self.btn_start = ctk.CTkButton(self.btn_frame, text="단계 2: 데이터 수집 시작", command=self.handle_step_2, height=40, fg_color="#28a745", hover_color="#218838")
self.btn_start.pack(side="left", padx=5, expand=True, fill="x")

# 결과 출력 영역
self.result_header_frame = ctk.CTkFrame(self, fg_color="transparent")
self.result_header_frame.pack(pady=(10, 0), padx=30, fill="x")

ctk.CTkLabel(self.result_header_frame, text="추출 데이터 결과:", font=ctk.CTkFont(size=14, weight="bold")).pack(side="left")
 
self.btn_copy_all = ctk.CTkButton(
self.result_header_frame,
text="📋 전체 내용 복사하기",
width=150,
height=28,
command=self.copy_to_clipboard,
fg_color="#343a40",
hover_color="#495057"
)
self.btn_copy_all.pack(side="right")
 
self.result_text = ctk.CTkTextbox(self, height=400, font=ctk.CTkFont(family="Courier", size=12))
self.result_text.pack(pady=10, padx=30, fill="both", expand=True)

# 상태 표시 안내
self.status_label = ctk.CTkLabel(self, text="준비됨", text_color="gray")
self.status_label.pack(pady=5)

def handle_step_1(self):
url = self.url_entry.get().strip()
if not url:
messagebox.showwarning("입력 필요", "URL을 먼저 입력해주세요.")
return
 
self.status_label.configure(text="브라우저를 열고 로그인 대기 중...", text_color="#3B8ED0")
threading.Thread(target=self.run_login, daemon=True).start()

def run_login(self):
try:
self.automation.open_login_page()
# 로그인 완료 후 다시 입력바의 URL로 이동
self.status_label.configure(text="로그인을 완료한 후 '데이터 수집 시작'을 눌러주세요.", text_color="yellow")
except Exception as e:
self.status_label.configure(text=f"오류: {str(e)}", text_color="red")

def handle_step_2(self):
if not self.automation.is_logged_in():
messagebox.showwarning("로그인 필요", "먼저 로그인을 완료해주세요.")
return

keyword = self.keyword_entry.get().strip()
url = self.url_entry.get().strip()
if not keyword:
messagebox.showwarning("키워드 필요", "검색할 키워드를 입력해주세요.")
return

self.status_label.configure(text="데이터 수집 중... 잠시만 기다려주세요.", text_color="green")
self.result_text.delete("1.0", "end")
 
threading.Thread(target=self.run_scraping, args=(url, keyword), daemon=True).start()

def run_scraping(self, url, keyword):
try:
# 먼저 해당 URL로 이동
self.automation.navigate_to(url)
time.sleep(3) # 페이지 로딩 대기
 
# 데이터 수집
data = self.automation.scrape_table_data(keyword)
 
if not data:
self.after(0, lambda: self.status_label.configure(text="검색된 키워드 데이터가 없습니다.", text_color="orange"))
return

# 결과 포맷 작성
output = self.format_output(data, keyword)
 
self.after(0, lambda: self.update_result_ui(output))
 
except Exception as e:
self.after(0, lambda: self.status_label.configure(text=f"수집 중 오류: {str(e)}", text_color="red"))

def format_output(self, data, keyword):
# 최상단 키워드 출력
header = f"■ 검색 키워드: {keyword}\n"
header += f"■ 수집 일시: {time.strftime('%Y-%m-%d %H:%M:%S')}\n"
header += "=" * 60 + "\n\n"

# 테이블 1: 전체 상세
table1 = "==================== [표 1: 전체 상세 정보] ====================\n"
table1 += "상품명\t스토어\t판매가\t수수료\t예상수익\t상태\t발급일\tURL\n"
table1 += "-" * 100 + "\n"
 
# 테이블 2: 요약 정보
table2 = "\n\n==================== [표 2: 요약 정보] ====================\n"
table2 += "상품명\t스토어\t판매가\tURL\n"
table2 += "-" * 100 + "\n"
 
for item in data:
# 표 1 채우기
table1 += f"{item['product_name']}\n"
table1 += f"{item['store']}\t{item['selling_price']}\t{item['fee']}\t{item['profit']}\t{item['status']}\t{item['issue_date']}\n"
table1 += f"LINK: {item['url']}\n"
table1 += "-" * 50 + "\n"
 
# 표 2 채우기
table2 += f"{item['product_name']}\n"
table2 += f"{item['store']}\t{item['selling_price']}\t{item['url']}\n"
table2 += "-" * 50 + "\n"
 
return header + table1 + table2

def copy_to_clipboard(self):
content = self.result_text.get("1.0", "end-1c")
if content.strip():
pyperclip.copy(content)
self.status_label.configure(text="클립보드에 결과가 복사되었습니다!", text_color="green")
else:
messagebox.showinfo("복사 실패", "복사할 내용이 없습니다.")

def update_result_ui(self, output):
self.result_text.insert("1.0", output)
self.status_label.configure(text=f"수집 완료! 총 {output.count('LINK:')}건 저장됨.", text_color="green")

if __name__ == "__main__":
app = NaverShopApp()
app.mainloop()