|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
|
@ -0,0 +1,280 @@
|
|||
import sys
|
||||
import os
|
||||
import random
|
||||
import asyncio
|
||||
import uuid
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
|
||||
from PySide6.QtGui import QPixmap
|
||||
from playwright.async_api import async_playwright
|
||||
import qasync
|
||||
|
||||
# ============================================================
|
||||
# 1. PySide6 GUI: QR 이미지 표시 창
|
||||
# ============================================================
|
||||
class QRCodeWindow(QMainWindow):
|
||||
def __init__(self, img_bytes):
|
||||
super().__init__()
|
||||
self.setWindowTitle("타오바오 QR 코드 로그인")
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
layout = QVBoxLayout(central_widget)
|
||||
|
||||
# QR 이미지를 표시할 QLabel
|
||||
self.label = QLabel()
|
||||
layout.addWidget(self.label)
|
||||
|
||||
pixmap = QPixmap()
|
||||
if not pixmap.loadFromData(img_bytes):
|
||||
self.label.setText("이미지를 로드할 수 없습니다.")
|
||||
else:
|
||||
self.label.setPixmap(pixmap)
|
||||
# 이미지 크기에 따라 창 크기 조절
|
||||
self.resize(pixmap.width(), pixmap.height())
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2. 로그인 페이지 접속 함수 (브라우저 실행 및 로그인 페이지 열기)
|
||||
# ============================================================
|
||||
async def open_login_page(p):
|
||||
"""
|
||||
제공해주신 브라우저 설정 코드를 사용하여 타오바오 로그인 페이지에 접속합니다.
|
||||
반환값: browser, page, login_url
|
||||
"""
|
||||
# 브라우저 경로 설정
|
||||
if getattr(sys, 'frozen', False):
|
||||
browser_path = os.path.join(os.path.dirname(sys.executable), 'src', 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
|
||||
else:
|
||||
browser_path = os.path.join(os.path.dirname(__file__), 'src', 'browsers', 'chromium-1112', 'chrome-win', 'chrome.exe')
|
||||
|
||||
# 사용자 에이전트 설정
|
||||
user_agent = random.choice([
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.0.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/85.0.0.0",
|
||||
])
|
||||
|
||||
browser = await p.chromium.launch(
|
||||
headless=False, # 디버깅을 위해 브라우저 창을 표시
|
||||
executable_path=browser_path,
|
||||
args=[
|
||||
'--disable-popup-blocking',
|
||||
'--start-maximized',
|
||||
'--window-size=1920,1080'
|
||||
]
|
||||
)
|
||||
|
||||
# 시크릿 브라우저 컨텍스트 생성
|
||||
context = await browser.new_context(
|
||||
user_agent=user_agent,
|
||||
geolocation={"latitude": 37.5665, "longitude": 126.9780},
|
||||
locale="ko-KR",
|
||||
permissions=["geolocation", "notifications"]
|
||||
)
|
||||
|
||||
# 페이지 열기
|
||||
page = await context.new_page()
|
||||
login_url = "https://login.taobao.com/member/login.jhtml"
|
||||
await page.goto(login_url)
|
||||
return browser, page, login_url
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3. QR 스크린샷 캡쳐 함수 (QR 이미지를 리턴)
|
||||
# ============================================================
|
||||
async def capture_qr_screenshot(page):
|
||||
"""
|
||||
페이지에서 'div#qrcode-img canvas' 요소의 스크린샷을 찍어 이미지 바이트 데이터를 반환합니다.
|
||||
"""
|
||||
await page.wait_for_selector("div#qrcode-img canvas", timeout=10000)
|
||||
await asyncio.sleep(2) # QR 코드가 완전히 렌더링될 때까지 딜레이
|
||||
qr_canvas = await page.query_selector("div#qrcode-img canvas")
|
||||
if not qr_canvas:
|
||||
print("QR 코드 캔버스를 찾을 수 없습니다.")
|
||||
return None
|
||||
img_bytes = await qr_canvas.screenshot()
|
||||
return img_bytes
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 4. 로그인 완료 감시 및 검색 페이지 이동 함수
|
||||
# ============================================================
|
||||
async def monitor_login(page, login_url, search_url):
|
||||
"""
|
||||
1초마다 현재 페이지 URL을 출력하다가 로그인 완료(로그인 페이지 URL과 달라짐)를 감지하면
|
||||
지정한 검색 URL로 이동합니다.
|
||||
"""
|
||||
print("로그인 완료 감시 시작...")
|
||||
while True:
|
||||
current_url = page.url
|
||||
print("현재 URL:", current_url)
|
||||
if current_url != login_url:
|
||||
print("로그인 완료 감지됨. 현재 URL:", current_url)
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 지정한 검색 페이지로 이동
|
||||
await page.goto(search_url)
|
||||
print("검색 페이지로 이동했습니다:", search_url)
|
||||
|
||||
# 상품 카드가 나타날 때까지 대기
|
||||
try:
|
||||
await page.wait_for_selector(".doubleCard--gO3Bz6bu", timeout=15000)
|
||||
print("상품 카드 요소가 나타났습니다.")
|
||||
except Exception as e:
|
||||
print("상품 카드 요소를 찾지 못했습니다.", e)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 5. 상품정보 추출 함수 (현재 페이지의 모든 상품 카드 처리)
|
||||
# ============================================================
|
||||
async def extract_products_from_current_page(page, card_or_img="card"):
|
||||
"""
|
||||
현재 페이지의 모든 상품 카드(.doubleCard--gO3Bz6bu)를 순회하며,
|
||||
상품 이미지 URL, 상품명, 가격, 구매량 정보를 추출하고,
|
||||
각 상품 카드의 스크린샷을 찍어 로컬 파일에 저장한 후, 파일 경로를 함께 반환합니다.
|
||||
"""
|
||||
await page.wait_for_selector(".doubleCard--gO3Bz6bu", timeout=10000)
|
||||
cards = await page.query_selector_all(".doubleCard--gO3Bz6bu")
|
||||
products = []
|
||||
|
||||
# 로컬에 상품 이미지 저장 폴더 생성
|
||||
images_folder = "product_images"
|
||||
os.makedirs(images_folder, exist_ok=True)
|
||||
|
||||
for card in cards:
|
||||
# 상품 이미지 URL 추출
|
||||
img_elem = await card.query_selector("img.mainPic--Ds3X7I8z")
|
||||
img_src = await img_elem.get_attribute("src") if img_elem else None
|
||||
|
||||
# 상품명 추출
|
||||
name_elem = await card.query_selector("div.title--qJ7Xg_90 span")
|
||||
name_text = (await name_elem.inner_text()).strip() if name_elem else ""
|
||||
|
||||
# 가격 추출 (정수 부분)
|
||||
price_elem = await card.query_selector("span.priceInt--yqqZMJ5a")
|
||||
price_text = (await price_elem.inner_text()).strip() if price_elem else ""
|
||||
|
||||
# 구매량 추출
|
||||
sales_elem = await card.query_selector("span.realSales--XZJiepmt")
|
||||
sales_text = (await sales_elem.inner_text()).strip() if sales_elem else ""
|
||||
|
||||
# 각 상품 카드의 스크린샷을 찍어서 로컬 파일로 저장
|
||||
unique_filename = f"product_{uuid.uuid4().hex}.png"
|
||||
local_path = os.path.join(images_folder, unique_filename)
|
||||
try:
|
||||
if card_or_img == "card":
|
||||
await card.screenshot(path=local_path)
|
||||
elif card_or_img == "img":
|
||||
if img_elem:
|
||||
await img_elem.screenshot(path=local_path)
|
||||
else:
|
||||
await card.screenshot(path=local_path)
|
||||
print(f"상품 스크린샷 저장됨: {local_path}")
|
||||
except Exception as e:
|
||||
print("상품 스크린샷 저장 실패:", e)
|
||||
local_path = None
|
||||
|
||||
product = {
|
||||
"image_url": img_src,
|
||||
"local_image": local_path,
|
||||
"name": name_text,
|
||||
"price": price_text,
|
||||
"sales": sales_text
|
||||
}
|
||||
products.append(product)
|
||||
return products
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 6. 여러 페이지에서 상품정보 수집 함수
|
||||
# ============================================================
|
||||
async def collect_products(page, pages_to_collect):
|
||||
"""
|
||||
사용자가 지정한 페이지 수만큼 각 페이지의 상품 정보를 추출합니다.
|
||||
각 페이지 당 최대 45개 상품이 있다고 가정하며, 페이지 이동 버튼을 클릭하여 이동합니다.
|
||||
"""
|
||||
all_products = []
|
||||
for i in range(pages_to_collect):
|
||||
print(f"\n--- Page {i+1} 상품정보 추출 시작 ---")
|
||||
products = await extract_products_from_current_page(page)
|
||||
print(f"페이지 {i+1}에서 {len(products)}개의 상품 정보 추출됨.")
|
||||
all_products.extend(products)
|
||||
|
||||
# 마지막 페이지가 아니라면 다음 페이지로 이동
|
||||
if i < pages_to_collect - 1:
|
||||
try:
|
||||
# 페이지 이동 버튼 영역 대기
|
||||
await page.wait_for_selector("div.next-pagination-list", timeout=10000)
|
||||
next_page = i + 2 # 현재 페이지가 i+1이면, 다음 페이지는 i+2
|
||||
# 페이지 이동 버튼 클릭 (버튼 내 텍스트가 페이지 번호와 일치)
|
||||
btn_selector = f"button.next-pagination-item:has-text('{next_page}')"
|
||||
await page.click(btn_selector)
|
||||
print(f"페이지 {next_page}로 이동 버튼 클릭됨.")
|
||||
# 페이지 이동 후 로드 대기
|
||||
await page.wait_for_load_state("networkidle")
|
||||
await asyncio.sleep(2)
|
||||
except Exception as e:
|
||||
print(f"페이지 {next_page}로 이동 실패: {e}")
|
||||
break
|
||||
return all_products
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 7. 전체 동작을 통합하는 메인 함수 (qasync 사용)
|
||||
# ============================================================
|
||||
async def main(pages_to_collect):
|
||||
async with async_playwright() as p:
|
||||
# 1) 로그인 페이지 접속
|
||||
browser, page, login_url = await open_login_page(p)
|
||||
|
||||
# 2) QR 스크린샷 캡쳐 (GUI에 표시할 이미지)
|
||||
img_bytes = await capture_qr_screenshot(page)
|
||||
if img_bytes is None:
|
||||
print("QR 코드 이미지를 가져오지 못했습니다.")
|
||||
await browser.close()
|
||||
return
|
||||
|
||||
# 3) 검색 페이지 URL (로그인 완료 후 이동)
|
||||
search_url = ("https://s.taobao.com/search?commend=all&ie=utf8page=1&"
|
||||
"q=%E5%A5%B3%E5%A3%AB%E8%A5%BF%E6%9C%8D&search_type=item")
|
||||
|
||||
# 4) GUI 창 생성 (QR 이미지 표시)
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
app = QApplication(sys.argv)
|
||||
window = QRCodeWindow(img_bytes)
|
||||
window.show()
|
||||
|
||||
# 5) 로그인 완료 감시 및 검색 페이지 이동
|
||||
await monitor_login(page, login_url, search_url)
|
||||
|
||||
# 6) 사용자가 지정한 페이지 수만큼 상품 정보 수집
|
||||
products = await collect_products(page, pages_to_collect)
|
||||
print(f"\n총 {len(products)}개의 상품 정보를 수집했습니다.")
|
||||
for idx, prod in enumerate(products, start=1):
|
||||
print(f"[{idx}] {prod}")
|
||||
|
||||
# 7) 디버깅 후 브라우저 종료 및 GUI 종료 처리
|
||||
await browser.close()
|
||||
await asyncio.sleep(2)
|
||||
app.quit()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 8. qasync를 통한 이벤트 루프 실행 및 사용자 입력 처리
|
||||
# ============================================================
|
||||
if __name__ == "__main__":
|
||||
# 사용자에게 수집할 페이지 수 입력 받기 (기본값 1)
|
||||
page_count_input = input("수집할 페이지 수를 입력하세요 (기본값 1): ")
|
||||
try:
|
||||
pages_to_collect = int(page_count_input) if page_count_input.strip() else 1
|
||||
except Exception:
|
||||
pages_to_collect = 1
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
loop = qasync.QEventLoop(app)
|
||||
asyncio.set_event_loop(loop)
|
||||
with loop:
|
||||
loop.run_until_complete(main(pages_to_collect))
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import getpass
|
||||
import textwrap
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
DEFAULT_DELAY = 1
|
||||
MESSAGES_URL = 'https://messages.google.com/web/conversations/new'
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='messages-for-web-playwright',
|
||||
description='Playwright를 사용하여 Google Messages for Web에서 SMS 전송 자동화',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=textwrap.dedent('''\
|
||||
예시:
|
||||
python script.py --to 010-1234-5678 "안녕하세요, 테스트 메시지입니다."
|
||||
''')
|
||||
)
|
||||
parser.add_argument('MESSAGE', type=str, help='전송할 문자 메시지 내용 (따옴표로 감싸서 입력)')
|
||||
parser.add_argument('--to', type=str, required=True, help='받는 사람의 전화번호 (예: 010-1234-5678)')
|
||||
parser.add_argument('-d', '--delay', type=int, default=DEFAULT_DELAY, help='동작 사이의 지연 시간 (초)')
|
||||
parser.add_argument('--dry_run', action='store_true', help='실제 전송 없이 테스트 모드 실행')
|
||||
args = parser.parse_args()
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
# persistent context를 사용하여 로그인 세션을 재사용 (로그인 되어 있어야 함)
|
||||
user_data_dir = f"C:\\Users\\{getpass.getuser()}\\AppData\\Local\\ms-playwright"
|
||||
context = await p.chromium.launch_persistent_context(user_data_dir=user_data_dir, headless=False)
|
||||
page = await context.new_page()
|
||||
print("Playwright로 Google Messages for Web에 접속합니다.")
|
||||
|
||||
# 1. Google Messages for Web 새 대화 페이지로 이동
|
||||
await page.goto(MESSAGES_URL)
|
||||
print("Google Messages for Web 페이지로 이동합니다.")
|
||||
await page.wait_for_timeout(args.delay * 1000)
|
||||
print("Google Messages for Web에 접속했습니다.")
|
||||
|
||||
# 2. "이름, 전화번호 또는 이메일 입력" input에 받는 사람 정보 입력
|
||||
recipient_input_selector = 'input[placeholder="이름, 전화번호 또는 이메일 입력"]'
|
||||
await page.wait_for_selector(recipient_input_selector, timeout=45000)
|
||||
print("받는 사람 정보 입력란을 찾았습니다.")
|
||||
await page.fill(recipient_input_selector, args.to)
|
||||
print(f"받는 사람 정보를 입력했습니다: {args.to}")
|
||||
|
||||
# 3. 새 대화 요소
|
||||
new_conv_selector = "span:has-text('번으로 보내기')"
|
||||
await page.wait_for_selector(new_conv_selector, timeout=45000)
|
||||
print("새 대화 버튼을 찾았습니다.")
|
||||
await page.click(new_conv_selector)
|
||||
print("새 대화 버튼을 클릭했습니다.")
|
||||
|
||||
# 4. "문자메시지" 입력란이 나타날 때까지 기다림
|
||||
message_input_selector = 'textarea[placeholder="문자메시지"]'
|
||||
await page.wait_for_selector(message_input_selector, timeout=45000)
|
||||
await page.fill(message_input_selector, args.MESSAGE)
|
||||
await page.wait_for_timeout(args.delay * 1000)
|
||||
|
||||
if args.dry_run:
|
||||
print("Dry run: 실제 전송 없이 메시지 입력만 수행했습니다.")
|
||||
else:
|
||||
await page.click('mws-message-compose > div > mws-message-send-button > div > mw-message-send-button > button')
|
||||
print("메시지가 전송되었습니다.")
|
||||
|
||||
await page.wait_for_timeout(args.delay * 1000)
|
||||
await context.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -59,11 +59,15 @@ class PlaywrightThread(QThread):
|
|||
|
||||
# 페이지 열기
|
||||
page = await context.new_page()
|
||||
await page.goto("https://world.taobao.com")
|
||||
# await page.goto("https://world.taobao.com")
|
||||
await page.goto("https://s.taobao.com/search?commend=all&ie=utf8page=1&q=%E5%A5%B3%E5%A3%AB%E8%A5%BF%E6%9C%8D&search_type=item")
|
||||
|
||||
self.logger.log(f"접속 완료.", level=logging.INFO)
|
||||
|
||||
# 페이지 로딩 확인 및 pagedown
|
||||
await page.wait_for_selector(".tb-pick-content-item") # 상품 카드 로딩 대기
|
||||
# await page.wait_for_selector(".tb-pick-content-item") # 상품 카드 로딩 대기
|
||||
await page.wait_for_selector("doubleCard--gO3Bz6bu") # 상품 카드 로딩 대기
|
||||
|
||||
self.logger.log(f"페이지 로딩 완료", level=logging.INFO)
|
||||
await page.keyboard.press("PageDown")
|
||||
await page.wait_for_timeout(1000) # 1초 대기
|
||||
|
|
|
|||