186 lines
6.2 KiB
Python
186 lines
6.2 KiB
Python
"""骏河屋页面解析工具函数"""
|
|
|
|
import re
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
|
|
def extract_low_price(text: str) -> int | None:
|
|
"""从价格范围文本中提取最低价格
|
|
|
|
例如 "¥4,700 ~ ¥6,200" 会返回 4700
|
|
"""
|
|
prices = re.findall(r'(\d[0-9,.]*)', text)
|
|
if prices:
|
|
low_price_str = prices[0].replace(',', '')
|
|
return int(float(low_price_str))
|
|
return None
|
|
|
|
|
|
def format_price(price_text: str) -> int | None:
|
|
"""格式化商品价格文字为整数(日元)
|
|
|
|
支持的格式:
|
|
- 单价:¥4,700 → 4700
|
|
- 范围:¥4,700 ~ ¥6,200 → 4700(取最低价)
|
|
- 円后缀:4700円 → 4700
|
|
"""
|
|
if not price_text:
|
|
return None
|
|
|
|
# 价格范围,取最低价
|
|
if "~" in price_text:
|
|
return extract_low_price(price_text)
|
|
|
|
match = re.search(r"[¥¥]\s*([0-9][0-9,]*)", price_text)
|
|
if not match:
|
|
match = re.search(r"([0-9][0-9,]*)\s*円", price_text)
|
|
if not match:
|
|
return None
|
|
try:
|
|
return int(match.group(1).replace(",", ""))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def normalize_goods_id(goods_id: str) -> str:
|
|
"""
|
|
高效处理商品ID:如果包含字母则转小写。
|
|
Python 的 lower() 会处理整个字符串,但在全是数字的情况下开销极小。
|
|
"""
|
|
# 如果 ID 已经是全数字,直接返回原对象,避免额外的处理
|
|
if goods_id.isdigit():
|
|
return goods_id
|
|
return goods_id.lower()
|
|
|
|
|
|
def surugaya_photo_url_to_cdn_m(photo_url: str | None = None, item_id: str | None = None) -> str:
|
|
"""将骏河屋商品照片 URL 转换为 CDN URL(webp 格式)
|
|
|
|
示例:
|
|
输入: https://www.suruga-ya.jp/database/photo.php?shinaban=220035970&size=m
|
|
输出: https://cdn.suruga-ya.jp/pics_webp/boxart_m/220035970m.jpg.webp
|
|
|
|
当 photo_url 无法解析时,回退使用 item_id 拼接 CDN 地址;
|
|
两者都为空时返回空字符串。
|
|
"""
|
|
# 如果无法从 photo_url 解析出商品编号,则用 item_id 直接拼接
|
|
if not photo_url:
|
|
if item_id:
|
|
return f"https://cdn.suruga-ya.jp/pics_webp/boxart_m/{normalize_goods_id(item_id)}m.jpg.webp"
|
|
return ""
|
|
|
|
parsed = urlparse(photo_url)
|
|
qs = parse_qs(parsed.query)
|
|
|
|
shinaban = (qs.get("shinaban") or [None])[0]
|
|
if not shinaban:
|
|
# photo_url 中没有 shinaban 参数,回退到 item_id
|
|
if item_id:
|
|
return f"https://cdn.suruga-ya.jp/pics_webp/boxart_m/{normalize_goods_id(item_id)}m.jpg.webp"
|
|
return photo_url
|
|
|
|
size = (qs.get("size") or ["m"])[0] or "m"
|
|
size = str(size).strip().lower()
|
|
|
|
folder = f"boxart_{size}"
|
|
filename = f"{normalize_goods_id(shinaban)}{size}"
|
|
return f"https://cdn.suruga-ya.jp/pics_webp/{folder}/{filename}.jpg.webp"
|
|
|
|
|
|
def surugaya_photo_url_to_cdn(photo_url: str | None = None, item_id: str | None = None) -> str:
|
|
"""将骏河屋商品照片 URL 转换为 CDN URL(webp 格式)
|
|
|
|
示例:
|
|
输入: https://www.suruga-ya.jp/database/photo.php?shinaban=220035970&size=m
|
|
输出: https://cdn.suruga-ya.jp/database/pics_webp/game/220035970.jpg.webp
|
|
|
|
优先使用 item_id 拼接 CDN 地址;
|
|
两者都为空时返回空字符串。
|
|
"""
|
|
if item_id:
|
|
return f"https://cdn.suruga-ya.jp/database/pics_webp/game/{normalize_goods_id(item_id)}.jpg.webp"
|
|
|
|
if photo_url:
|
|
parsed = urlparse(photo_url)
|
|
qs = parse_qs(parsed.query)
|
|
shinaban = (qs.get("shinaban") or [None])[0]
|
|
if not shinaban:
|
|
return photo_url
|
|
|
|
return f"https://cdn.suruga-ya.jp/database/pics_webp/game/{normalize_goods_id(shinaban)}.jpg.webp"
|
|
|
|
return ""
|
|
|
|
def extract_product_id(url: str | None = None) -> str:
|
|
"""从各种商品URL或路径中精准提取商品ID"""
|
|
if not url:
|
|
return ""
|
|
# 核心逻辑:匹配 /detail/ 后面紧跟的非斜杠、非问号、非点号的连续字符
|
|
match = re.search(r"/detail/([^/?.]+)", url)
|
|
if match:
|
|
return match.group(1)
|
|
return ""
|
|
|
|
|
|
# --- 官方店铺日本境内运费规则 ---
|
|
# amount < OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD: OFFICIAL_SHIPPING_FEE_LOW
|
|
# OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD <= amount < OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD: OFFICIAL_SHIPPING_FEE_MID
|
|
# amount >= OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD: 免运费(0)
|
|
OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD = 1000
|
|
OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD = 1500
|
|
OFFICIAL_SHIPPING_FEE_LOW = 440
|
|
OFFICIAL_SHIPPING_FEE_MID = 385
|
|
|
|
# --- 加盟店日本境内运费规则 ---
|
|
# 每 1 家加盟店,无论订单数量与金额,统一收 FRANCHISE_SHIPPING_FEE_PER_STORE 日元
|
|
FRANCHISE_SHIPPING_FEE_PER_STORE = 800
|
|
|
|
# --- 通贩手续费规则 ---
|
|
# amount < HANDLE_FEE_FREE_THRESHOLD: HANDLE_FEE_BELOW
|
|
# amount >= HANDLE_FEE_FREE_THRESHOLD: 免手续费(0)
|
|
HANDLE_FEE_FREE_THRESHOLD = 5000
|
|
HANDLE_FEE_BELOW = 240
|
|
|
|
|
|
def get_shipping_fee(amount: int) -> int:
|
|
"""计算官方店铺日本境内运费(本州/四国/九州统一价)。
|
|
|
|
规则:
|
|
- amount < 1000 日元: 440 日元
|
|
- 1000 <= amount < 1500 日元: 385 日元
|
|
- amount >= 1500 日元: 0(免运费)
|
|
|
|
备注: 北海道/冲绳/离岛可能需额外支付;此处按本州口径计算。
|
|
加盟店运费另由 get_franchise_shipping_fee 计算。
|
|
"""
|
|
if amount >= OFFICIAL_SHIPPING_FEE_HIGH_THRESHOLD:
|
|
return 0
|
|
if amount >= OFFICIAL_SHIPPING_FEE_LOW_THRESHOLD:
|
|
return OFFICIAL_SHIPPING_FEE_MID
|
|
return OFFICIAL_SHIPPING_FEE_LOW
|
|
|
|
|
|
def get_franchise_shipping_fee(franchise_store_count: int) -> int:
|
|
"""计算加盟店日本境内运费。
|
|
|
|
每 1 家加盟店统一收 800 日元,与订单数量/金额无关。
|
|
franchise_store_count <= 0 时返回 0。
|
|
"""
|
|
if franchise_store_count <= 0:
|
|
return 0
|
|
return franchise_store_count * FRANCHISE_SHIPPING_FEE_PER_STORE
|
|
|
|
|
|
def get_handle_fee(amount: int) -> int:
|
|
"""计算通贩手续费(本州/四国/九州口径)。
|
|
|
|
规则:
|
|
- amount < 5000 日元: 240 日元
|
|
- amount >= 5000 日元: 0(免手续费)
|
|
|
|
参考: https://www.suruga-ya.jp/man/qa/hanbai_qa/shiharai.html#shiharai_1_2
|
|
"""
|
|
if amount >= HANDLE_FEE_FREE_THRESHOLD:
|
|
return 0
|
|
return HANDLE_FEE_BELOW
|