更新 Cigarette/jinye_scraper.py
This commit is contained in:
+29
-48
@@ -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 代理
|
||||||
"User-Agent": "Mozilla/5.0",
|
return httpx.Client(
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
cookies=cookies,
|
||||||
"Referer": f"{BASE_URL}/ui/pages/list.html?funCode={FUNCODE}"
|
headers={
|
||||||
}, timeout=30)
|
"User-Agent": "Mozilla/5.0",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"Referer": f"{BASE_URL}/ui/pages/list.html?funCode={FUNCODE}"
|
||||||
|
},
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user