更新 Cigarette/jinye_scraper.py

This commit is contained in:
2026-08-01 15:45:15 +08:00
parent d559a9514a
commit 4e48e04b49
+26 -45
View File
@@ -1,21 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
卷烟平台数据采集脚本 v4跨平台 + 数据/图片合并 卷烟平台数据采集脚本 v4通用格式 + 强制禁用代理
- 数字列格式设为 'General'G/通用格式)
- Cookie 从环境变量 JY_CK 读取 - 强制忽略系统代理(HTTP_PROXY/HTTPS_PROXY
- 以产品名称为主键,增量更新模板 - 修复调货价(第20列)转数字
- 图片用原始文件名,可并发下载 - 从第一个空白行开始写入
- 特定列自动设置数字格式和公式
- 每 20 条自动保存
- 跨平台:Windows / Linux (青龙) 均可运行
用法:
python jinye_scraper.py # 使用脚本同目录下的模板
python jinye_scraper.py --template ./模板.xlsx # 指定模板
python jinye_scraper.py --no-images # 只采集数据,不下图片
python jinye_scraper.py --images-only # 只下载图片(不处理数据)
python jinye_scraper.py --skip-existing # 跳过已存在产品(加速)
python jinye_scraper.py --delay 0.1 # API 请求间隔
""" """
import os, sys, json, time, re, argparse import os, sys, json, time, re, argparse
@@ -35,9 +24,9 @@ FUNCODE = "MOHD0418"
SAVE_INTERVAL = 20 SAVE_INTERVAL = 20
# 格式列编号 # 格式列编号
INT_COLS = {4, 14} # D 小盒条码, N 条装条码 → '0' INT_COLS = {4, 14} # D 小盒条码, N 条装条码 → 整数
DEC_COLS = {6, 7, 8, 9, 16, 17, 19} # 数值列 → '#,##0.##' DEC_COLS = {6, 7, 8, 9, 16, 17, 19, 20} # 数值列 → 通用格式
DATE_COL = 30 # ^ 批复日期 → 'yyyy-mm-dd' DATE_COL = 30 # 批复日期 → 'yyyy-mm-dd'
FORMULA_COL = 10 # J 烟支分类公式 FORMULA_COL = 10 # J 烟支分类公式
# 列号 → API 字段路径 # 列号 → API 字段路径
@@ -68,7 +57,6 @@ COL_FIELD = {
# ── 工具函数 ────────────────────────────────────────── # ── 工具函数 ──────────────────────────────────────────
def safe_product_dir(name: str) -> str: def safe_product_dir(name: str) -> str:
"""产品名 → 安全文件夹名"""
for ch in r'/\:*?"<>|': for ch in r'/\:*?"<>|':
name = name.replace(ch, " ") name = name.replace(ch, " ")
return name return name
@@ -79,7 +67,6 @@ def extract_filename(url: str) -> str:
return unquote(raw) if raw else f"img{int(time.time())}.jpg" return unquote(raw) if raw else f"img{int(time.time())}.jpg"
def _g(detail, path): def _g(detail, path):
"""从嵌套字典取值"""
if detail is None: return "" if detail is None: return ""
val = detail val = detail
for p in path.split("."): for p in path.split("."):
@@ -98,11 +85,17 @@ def parse_cookies(s: str) -> dict:
return c return c
def make_client(cookies: dict) -> httpx.Client: def make_client(cookies: dict) -> httpx.Client:
return httpx.Client(cookies=cookies, headers={ # 强制禁用代理,避免走 SOCKS/HTTP 代理
return httpx.Client(
cookies=cookies,
headers={
"User-Agent": "Mozilla/5.0", "User-Agent": "Mozilla/5.0",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Referer": f"{BASE_URL}/ui/pages/list.html?funCode={FUNCODE}" "Referer": f"{BASE_URL}/ui/pages/list.html?funCode={FUNCODE}"
}, timeout=30) },
timeout=30,
proxies=None # ← 关键修改:忽略系统代理
)
# ── API ─────────────────────────────────────────────── # ── API ───────────────────────────────────────────────
@@ -161,7 +154,7 @@ def write_cell(ws, row: int, col: int, value):
if s: if s:
try: cell.value = int(float(s)) try: cell.value = int(float(s))
except ValueError: cell.value = s except ValueError: cell.value = s
cell.number_format = '0' cell.number_format = '0' # 整数格式
return return
if col in DEC_COLS: if col in DEC_COLS:
@@ -169,7 +162,7 @@ def write_cell(ws, row: int, col: int, value):
if s: if s:
try: cell.value = float(s) try: cell.value = float(s)
except ValueError: cell.value = s except ValueError: cell.value = s
cell.number_format = '#,##0.##' cell.number_format = 'General' # G/通用格式
return return
cell.value = str(value).strip() if value else "" cell.value = str(value).strip() if value else ""
@@ -189,9 +182,7 @@ def download_single(url: str, folder: Path) -> bool:
return False return False
def download_images_for_product(pk, name, images_root: Path, cookies: dict): def download_images_for_product(pk, name, images_root: Path, cookies: dict):
"""为单个产品下载图片(并发单元)"""
folder = images_root / safe_product_dir(name) folder = images_root / safe_product_dir(name)
# 检查是否已有非编号图片
existing = [f for f in os.listdir(str(folder)) if not re.match(r'^\d{2,3}\.jpg$', f)] if folder.exists() else [] existing = [f for f in os.listdir(str(folder)) if not re.match(r'^\d{2,3}\.jpg$', f)] if folder.exists() else []
if existing: if existing:
return name, 0, len(existing) return name, 0, len(existing)
@@ -214,7 +205,7 @@ def download_images_for_product(pk, name, images_root: Path, cookies: dict):
# ── 主流程 ──────────────────────────────────────────── # ── 主流程 ────────────────────────────────────────────
def main(): def main():
parser = argparse.ArgumentParser(description="卷烟平台数据采集 v4") parser = argparse.ArgumentParser(description="卷烟平台数据采集 v4(通用格式版)")
parser.add_argument("--template", default="", help="模板路径,默认脚本同目录") parser.add_argument("--template", default="", help="模板路径,默认脚本同目录")
parser.add_argument("--output", default="", help="输出路径,默认覆盖模板") parser.add_argument("--output", default="", help="输出路径,默认覆盖模板")
parser.add_argument("--page-size", type=int, default=20) parser.add_argument("--page-size", type=int, default=20)
@@ -225,7 +216,6 @@ def main():
parser.add_argument("--img-workers", type=int, default=5, help="图片下载并发数") parser.add_argument("--img-workers", type=int, default=5, help="图片下载并发数")
args = parser.parse_args() args = parser.parse_args()
# 模板路径
script_dir = Path(__file__).parent script_dir = Path(__file__).parent
if not args.template: if not args.template:
args.template = str(script_dir / "卷烟产品数据系统_v1.xlsx") args.template = str(script_dir / "卷烟产品数据系统_v1.xlsx")
@@ -241,15 +231,12 @@ def main():
if not cookie_str: print("错误: JY_CK 未设置"); sys.exit(1) if not cookie_str: print("错误: JY_CK 未设置"); sys.exit(1)
cookies = parse_cookies(cookie_str) cookies = parse_cookies(cookie_str)
# ── 仅图片模式 ─────────────────────────────────────
if args.images_only: if args.images_only:
run_images_only(template_path, images_root, cookies, args) run_images_only(template_path, images_root, cookies, args)
return return
# ── 正常模式:数据 + 图片 ──────────────────────────
client = make_client(cookies) client = make_client(cookies)
# 清理旧编号图片
if images_root.exists(): if images_root.exists():
for d in os.listdir(str(images_root)): for d in os.listdir(str(images_root)):
dp = images_root / d dp = images_root / d
@@ -259,11 +246,9 @@ def main():
try: (dp / f).unlink() try: (dp / f).unlink()
except: pass except: pass
# 打开模板
wb = openpyxl.load_workbook(str(template_path)) wb = openpyxl.load_workbook(str(template_path))
ws = wb.active ws = wb.active
# 构建已有产品索引
existing = {} existing = {}
for row in range(2, ws.max_row + 1): for row in range(2, ws.max_row + 1):
n = ws.cell(row, 3).value n = ws.cell(row, 3).value
@@ -275,20 +260,21 @@ def main():
existing[name_key] = row existing[name_key] = row
print(f"模板已有 {len(existing)} 条数据") print(f"模板已有 {len(existing)} 条数据")
# 获取全量列表
print("获取产品列表...") print("获取产品列表...")
all_items = fetch_all(client, args.page_size, args.delay) all_items = fetch_all(client, args.page_size, args.delay)
print(f"金叶网共 {len(all_items)} 条产品") print(f"金叶网共 {len(all_items)} 条产品")
new_cnt = upd_cnt = skip_cnt = 0 new_cnt = upd_cnt = skip_cnt = 0
next_row = ws.max_row + 1
fmt_cols = INT_COLS | DEC_COLS | {DATE_COL} fmt_cols = INT_COLS | DEC_COLS | {DATE_COL}
img_tasks = [] # 待下载图片的产品
# 从第一个空行开始写入
next_row = 2
while next_row <= ws.max_row and ws.cell(next_row, 3).value is not None:
next_row += 1
for i, item in enumerate(all_items, 1): for i, item in enumerate(all_items, 1):
pk, name = item["pk_cigarette"], item["vcgtname"] pk, name = item["pk_cigarette"], item["vcgtname"]
# ── 已存在产品:补缺失字段 ──
if name in existing: if name in existing:
if args.skip_existing: if args.skip_existing:
continue continue
@@ -318,13 +304,13 @@ def main():
if upd_cnt > 0: wb.save(output_path) if upd_cnt > 0: wb.save(output_path)
continue continue
# ── 新产品:完整填充 ──
detail = fetch_detail(client, pk) detail = fetch_detail(client, pk)
if detail is None: if detail is None:
print(f"[{i}/{len(all_items)}] {name} 详情失败,跳过") print(f"[{i}/{len(all_items)}] {name} 详情失败,跳过")
skip_cnt += 1; continue skip_cnt += 1; continue
target_row = next_row; next_row += 1 target_row = next_row
next_row += 1
existing[name] = target_row existing[name] = target_row
new_cnt += 1 new_cnt += 1
@@ -341,10 +327,8 @@ def main():
fills += 1 fills += 1
write_cell(ws, target_row, FORMULA_COL, None) write_cell(ws, target_row, FORMULA_COL, None)
# C 列:先写产品名,图片下载后再替换为 HYPERLINK
ws.cell(target_row, 3).value = name ws.cell(target_row, 3).value = name
# 图片:记录任务(或同步下载)
img_urls = detail.get("imgPaths") or [] img_urls = detail.get("imgPaths") or []
if img_urls and not args.no_images: if img_urls and not args.no_images:
safe_name = safe_product_dir(name) safe_name = safe_product_dir(name)
@@ -374,7 +358,6 @@ def main():
# ── 仅图片模式 ──────────────────────────────────────── # ── 仅图片模式 ────────────────────────────────────────
def run_images_only(template_path, images_root, cookies, args): def run_images_only(template_path, images_root, cookies, args):
"""独立图片下载模式"""
import openpyxl as _xl import openpyxl as _xl
wb = _xl.load_workbook(str(template_path)) wb = _xl.load_workbook(str(template_path))
ws = wb.active ws = wb.active
@@ -410,7 +393,6 @@ def run_images_only(template_path, images_root, cookies, args):
tasks = [(name_to_pk[n], n) for n in products if n in name_to_pk] tasks = [(name_to_pk[n], n) for n in products if n in name_to_pk]
print(f"待处理: {len(tasks)} 个产品,{args.img_workers} 线程并发") print(f"待处理: {len(tasks)} 个产品,{args.img_workers} 线程并发")
# 清理旧编号图片
if images_root.exists(): if images_root.exists():
for d in os.listdir(str(images_root)): for d in os.listdir(str(images_root)):
dp = images_root / d dp = images_root / d
@@ -431,7 +413,6 @@ def run_images_only(template_path, images_root, cookies, args):
print(f"\n图片完成: 下载 {total_dl} | 已有跳过 {skipped}") print(f"\n图片完成: 下载 {total_dl} | 已有跳过 {skipped}")
# 更新 HYPERLINK
print("更新超链接...") print("更新超链接...")
wb = _xl.load_workbook(str(template_path)) wb = _xl.load_workbook(str(template_path))
ws = wb.active ws = wb.active