You've already forked pruchase_jhw
适配common-0.3.0
This commit is contained in:
+2
-2
@@ -9,8 +9,8 @@ authors = [
|
||||
{ name = "Antigravity Development Team" }
|
||||
]
|
||||
dependencies = [
|
||||
# playwright 由 surugaya-common 的 [browser] extra 提供(>=1.54.0,<2.0.0),不再直接声明。
|
||||
"surugaya-common[browser]>=0.2.0",
|
||||
# playwright / playwright-stealth 由 surugaya-common 的 [browser] extra 提供,不再直接声明。
|
||||
"surugaya-common[browser]>=0.3.0",
|
||||
"pydantic>=2.7.0",
|
||||
"pydantic-settings>=2.2.0",
|
||||
"loguru>=0.7.2",
|
||||
|
||||
+7
-17
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
from typing import Dict
|
||||
from playwright.async_api import async_playwright, Playwright, BrowserContext
|
||||
from loguru import logger
|
||||
from surugaya_common.browser_profile import DEFAULT_USER_AGENT, LOCALE, TIMEZONE_ID, stealth_launch_args
|
||||
from src.config.settings import get_settings
|
||||
from src.config.accounts import AccountConfig, load_accounts_config
|
||||
|
||||
@@ -239,17 +240,9 @@ class BrowserManager:
|
||||
logger.error(f"创建账号 '{account.account_id}' 的持久化目录失败: {e}")
|
||||
raise e
|
||||
|
||||
# 2. 配置隐身 Chromium 启动参数
|
||||
chrome_args = [
|
||||
"--disable-blink-features=AutomationControlled", # 浏览器层面移除 navigator.webdriver
|
||||
"--disable-infobars", # 隐藏「Chrome 正受到自动测试软件控制」提示条
|
||||
"--no-sandbox", # Docker/服务器环境兼容
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage", # 内存受限环境避免资源崩溃
|
||||
# 2. 配置隐身 Chromium 启动参数(共享反检测参数 + 本进程追加项)
|
||||
chrome_args = stealth_launch_args() + [
|
||||
"--start-maximized", # 启动时全屏(最大化)
|
||||
"--disable-gpu", # 服务器无头模式稳定渲染
|
||||
"--use-mock-keychain", # 避免本地密钥环弹窗(macOS/Linux)
|
||||
"--password-store=basic",
|
||||
]
|
||||
|
||||
executable_path_raw = self.settings.browser_executable_path
|
||||
@@ -265,11 +258,8 @@ class BrowserManager:
|
||||
)
|
||||
headless_mode = self.settings.browser_headless
|
||||
|
||||
# 使用较新的 Windows Chrome User-Agent
|
||||
user_agent = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
)
|
||||
# 使用与 API 服务一致的 Windows Chrome User-Agent(共享指纹配置)
|
||||
user_agent = DEFAULT_USER_AGENT
|
||||
|
||||
logger.info(f"正在为账号 {account.account_id} 启动 Playwright | 无头模式: {headless_mode}")
|
||||
|
||||
@@ -289,8 +279,8 @@ class BrowserManager:
|
||||
"args": chrome_args,
|
||||
"user_agent": user_agent,
|
||||
"no_viewport": True, # 配合 --start-maximized 使得网页视口也铺满全屏
|
||||
"locale": "ja-JP", # 区域语言一致
|
||||
"timezone_id": "Asia/Tokyo", # 日本本地时区
|
||||
"locale": LOCALE, # 区域语言一致(ja-JP)
|
||||
"timezone_id": TIMEZONE_ID, # 日本本地时区(Asia/Tokyo)
|
||||
"accept_downloads": True, # 允许自动下载(如需要)
|
||||
"proxy": proxy_config # 住宅代理等
|
||||
}
|
||||
|
||||
+10
-75
@@ -17,12 +17,13 @@ from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
||||
from notifications import NotificationEvent
|
||||
from src.tasks.base import BasePurchaseTask
|
||||
from src.config.accounts import get_account_config
|
||||
from src.utils.page_utils import format_japanese_price
|
||||
from src.task_queue.redis_manager import get_redis_client
|
||||
from surugaya_common.models import SurugayaOrderDetail
|
||||
from surugaya_common.parsers.surugaya.order_detail import SurugayaOrderDetailParser
|
||||
from surugaya_common.browser_utils import human_scroll
|
||||
from surugaya_common.browser_utils import clear_cart, human_scroll
|
||||
from surugaya_common.parse_utils import format_japanese_price
|
||||
from surugaya_common.app_utils import is_valid_str
|
||||
from surugaya_common.urls import CARGO_DETAIL_URL, MYPAGE_URL, product_detail_url, trade_detail_url
|
||||
|
||||
|
||||
class TemplateMatchResult(TypedDict):
|
||||
@@ -57,7 +58,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
|
||||
logger.info(f"[任务 {self.task_id}] 购买任务开始,商品:{item_list}")
|
||||
# 打开购物车
|
||||
await page.goto("https://www.suruga-ya.jp/cargo/detail", wait_until="domcontentloaded", timeout=30000)
|
||||
await page.goto(CARGO_DETAIL_URL, wait_until="domcontentloaded", timeout=30000)
|
||||
|
||||
# 检查登录状态
|
||||
logger.debug(f"[任务 {self.task_id}] 正在校验账号 '{self.account_id}' 的登录状态...")
|
||||
@@ -70,7 +71,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
raise RuntimeError(f"账号 '{self.account_id}' 登录失败,无法继续执行采购流程。")
|
||||
|
||||
# 重新进入购物车
|
||||
await page.goto("https://www.suruga-ya.jp/cargo/detail", wait_until="domcontentloaded", timeout=30000)
|
||||
await page.goto(CARGO_DETAIL_URL, wait_until="domcontentloaded", timeout=30000)
|
||||
|
||||
# 判断购物车是否有商品
|
||||
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
||||
@@ -79,7 +80,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
# 清空购物车
|
||||
await asyncio.sleep(random.uniform(1, 3)) # 加入随机延迟,模拟人工操作间隔
|
||||
if cart_goods_num > 0:
|
||||
await self.clear_cart(page)
|
||||
await clear_cart(page)
|
||||
|
||||
# 将待采购的商品加入购物车
|
||||
for index, item in enumerate(item_list):
|
||||
@@ -96,9 +97,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
logger.warning(f"[任务 {self.task_id}] 索引 {index} 的商品数量为 0,跳过。")
|
||||
continue
|
||||
|
||||
detail_url = f"https://www.suruga-ya.jp/product/detail/{product_id}"
|
||||
if tenpo_cd:
|
||||
detail_url += f"?tenpo_cd={tenpo_cd}"
|
||||
detail_url = product_detail_url(product_id, tenpo_cd)
|
||||
|
||||
logger.debug(
|
||||
f"[任务 {self.task_id}] 进入商品 {product_id} 详情页,购买数量: {number}, URL: {detail_url}")
|
||||
@@ -195,7 +194,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
|
||||
# 再次进入购物车,确认商品是否加入成功
|
||||
await asyncio.sleep(random.uniform(1, 3))
|
||||
await page.goto("https://www.suruga-ya.jp/cargo/detail", wait_until="domcontentloaded", timeout=30000)
|
||||
await page.goto(CARGO_DETAIL_URL, wait_until="domcontentloaded", timeout=30000)
|
||||
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
||||
logger.debug(f"[任务 {self.task_id}] 再次进入购物车页面,购物车商品数量:{cart_goods_num}")
|
||||
if cart_goods_num < 1:
|
||||
@@ -304,7 +303,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
解析交易详情页内容,提取交易详情。
|
||||
"""
|
||||
await page.goto(
|
||||
f"https://www.suruga-ya.jp/pcmypage/action_sell_search/detail?trade_code={trade_code}",
|
||||
trade_detail_url(trade_code),
|
||||
wait_until="domcontentloaded",
|
||||
timeout=30000,
|
||||
)
|
||||
@@ -345,7 +344,7 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
password = account_config.password
|
||||
|
||||
logger.info(f"[任务 {self.task_id}] 正在初始化骏河屋登录流程...")
|
||||
await page.goto("https://www.suruga-ya.jp/pcmypage", wait_until="domcontentloaded", timeout=30000)
|
||||
await page.goto(MYPAGE_URL, wait_until="domcontentloaded", timeout=30000)
|
||||
|
||||
# 1. 确保表单和输入框在画面上完全可见
|
||||
# 骏河屋的账号框 id 是 'edit-mail'
|
||||
@@ -390,70 +389,6 @@ class SurugayaPurchaseTask(BasePurchaseTask):
|
||||
await self.take_screenshot(scene="surugaya_login_failed")
|
||||
raise e
|
||||
|
||||
async def clear_cart(self, page: Page) -> None:
|
||||
"""
|
||||
清空购物车
|
||||
"""
|
||||
logger.debug(f"[任务 {self.task_id}] 正在清空购物车...")
|
||||
cart_goods_num = await page.locator("table.item_box tbody tr.item").count()
|
||||
if cart_goods_num < 1:
|
||||
return
|
||||
|
||||
is_delete = 0
|
||||
# 购物车商品数量超过3个后会出现"全削除"按钮
|
||||
if cart_goods_num > 3:
|
||||
try:
|
||||
delete_button = page.locator("#total_box .delbtn-all").get_by_role("button", name="全削除")
|
||||
# 3. 显式等待按钮在页面上可见(容错处理:比如给它最多 3000 毫秒的加载时间)
|
||||
# 如果按钮是一开始就在静态 HTML 里的,这一步也可以省略,直接用下面的 is_visible()
|
||||
await delete_button.wait_for(state="visible", timeout=3000)
|
||||
if await delete_button.is_visible():
|
||||
logger.debug("检测到‘全削除’按钮存在,正在执行点击...")
|
||||
await delete_button.click()
|
||||
is_delete = 1
|
||||
except Exception:
|
||||
logger.debug("未检测到‘全削除’按钮。")
|
||||
|
||||
if is_delete == 1:
|
||||
return
|
||||
|
||||
# 遍历删除
|
||||
await page.wait_for_timeout(random.randint(1000, 3000))
|
||||
item_selector = "table.item_box tbody tr.item"
|
||||
remove_selector = "table.item_box a.remove_product"
|
||||
|
||||
while True:
|
||||
cart_goods_num = await page.locator(item_selector).count()
|
||||
if cart_goods_num == 0:
|
||||
break
|
||||
|
||||
remove_link = page.locator(remove_selector).first
|
||||
href = await remove_link.get_attribute("href")
|
||||
# logger.debug(f"[任务 {self.task_id}] 删除购物车商品:{href or '(no href)'},当前购物车数量:{cart_goods_num}")
|
||||
|
||||
try:
|
||||
await remove_link.click()
|
||||
# 删除按钮不会触发弹窗,直接等待购物车条目数减少即可。
|
||||
await page.wait_for_function(
|
||||
"""({selector, expectedCount}) => {
|
||||
return document.querySelectorAll(selector).length < expectedCount;
|
||||
}""",
|
||||
arg={"selector": item_selector, "expectedCount": cart_goods_num},
|
||||
timeout=15_000,
|
||||
)
|
||||
await page.wait_for_timeout(random.randint(800, 1500))
|
||||
except PlaywrightTimeoutError:
|
||||
latest_count = await page.locator(item_selector).count()
|
||||
if latest_count < cart_goods_num:
|
||||
logger.debug(f"[任务 {self.task_id}] 购物车商品已删除,当前购物车数量:{latest_count}")
|
||||
continue
|
||||
|
||||
logger.warning(f"[任务 {self.task_id}] 删除商品后数量未变化,等待后重试")
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
cart_goods_num = await page.locator(item_selector).count()
|
||||
# logger.debug(f"[任务 {self.task_id}] 购物车已清空。")
|
||||
|
||||
async def get_show_payment_methods(self, page: Page) -> list:
|
||||
"""
|
||||
获取所有显示的支付方式
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
from loguru import logger
|
||||
|
||||
from surugaya_common.parsers.surugaya import SurugayaOrderDetailParser
|
||||
from surugaya_common.urls import trade_detail_url
|
||||
from src.tasks.base import BasePurchaseTask
|
||||
from src.task_queue.redis_manager import get_redis_client
|
||||
|
||||
@@ -37,7 +38,7 @@ class SurugayaTradeMonitorTask(BasePurchaseTask):
|
||||
|
||||
page = self.page
|
||||
|
||||
await page.goto(f"https://www.suruga-ya.jp/pcmypage/action_sell_search/detail?trade_code={trade_code}",
|
||||
await page.goto(trade_detail_url(trade_code),
|
||||
wait_until="domcontentloaded", timeout=30000)
|
||||
|
||||
order_detail_model = await SurugayaOrderDetailParser.parse_page(page)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import re
|
||||
|
||||
|
||||
def format_japanese_price(price_str: str | None) -> int:
|
||||
"""
|
||||
将日本电商网站的价格字符串格式化为标准整数
|
||||
支持的格式示例: "600円 (税込)", "1,250円", "¥ 3,000", "300"
|
||||
"""
|
||||
if not price_str:
|
||||
return 0
|
||||
|
||||
digits = re.findall(r'\d', price_str)
|
||||
if not digits:
|
||||
return 0
|
||||
|
||||
return int("".join(digits))
|
||||
@@ -187,7 +187,7 @@ requires-dist = [
|
||||
{ name = "pyscreeze", specifier = ">=1.0.1" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.1" },
|
||||
{ name = "redis", specifier = "==8.0.0" },
|
||||
{ name = "surugaya-common", extras = ["browser"], specifier = ">=0.2.0", index = "https://git.jerryyan.net/api/packages/jp/pypi/simple" },
|
||||
{ name = "surugaya-common", extras = ["browser"], specifier = ">=0.3.0", index = "https://git.jerryyan.net/api/packages/jp/pypi/simple" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -381,6 +381,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "playwright-stealth"
|
||||
version = "2.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "playwright" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/db/6ade5d539c7d151b9defc78fafa8b65aa52352617d0e7699b47008bd801f/playwright_stealth-2.0.3.tar.gz", hash = "sha256:1d8e488fbdd8f190f1269ea8cf5d57d14df3a9f1af1001c41ee3588b2aac3133", size = 25751, upload-time = "2026-04-04T02:50:33.88Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl", hash = "sha256:1887ade423ab7ff8ae16d363a30a38de0b5817e1e4a29d47b74bf3a0e3dbfcb4", size = 34385, upload-time = "2026-04-04T02:50:35.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyautogui"
|
||||
version = "0.9.54"
|
||||
@@ -685,20 +697,21 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "surugaya-common"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = { registry = "https://git.jerryyan.net/api/packages/jp/pypi/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "redis" },
|
||||
]
|
||||
sdist = { url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.2.0/surugaya_common-0.2.0.tar.gz", hash = "sha256:168a5810672f5ead82957a9491611b64bf439f489b4eeaa287b302206712e2ab" }
|
||||
sdist = { url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.3.0/surugaya_common-0.3.0.tar.gz", hash = "sha256:578757409e8e062fd9bac32244775f4db76ba1dd8d22cb5fe4eaeab8f068c85f" }
|
||||
wheels = [
|
||||
{ url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.2.0/surugaya_common-0.2.0-py3-none-any.whl", hash = "sha256:140587df6b60abf1f1fc3b592d0668a096ddd42f5216ee16d4b83a95783b58cd" },
|
||||
{ url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.3.0/surugaya_common-0.3.0-py3-none-any.whl", hash = "sha256:281ae05a4996e4100d40abd6e5c6ffe0953cec5142745d287e1b21082c2bad1f" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
browser = [
|
||||
{ name = "playwright" },
|
||||
{ name = "playwright-stealth" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user