# SCP Auctions 拍卖数据爬虫 抓取 **SCP Auctions**([catalogs.scpauctions.com](https://catalogs.scpauctions.com/auctions/past))历史拍卖会的已售/流拍拍品数据,含拍卖会信息、拍品列表与拍品详情。提供 **历史全量** 与 **每周新增** 两个任务。 - URL:https://catalogs.scpauctions.com/auctions/past - 平台:**Bidsquare** 托管(Apache + 服务端渲染 SSR,无 Cloudflare) - 语言 / 环境:Python 3.12.10 - 请求 / 解析:`curl_cffi`(浏览器指纹)+ `parsel` --- ## 1. 目标数据 | 层级 | 来源页 | 抓取字段 | |---|---|---| | 拍卖会 | `/auctions/past` | 标题、类型、状态、开始/结束时间、主页链接、图录链接、封面图、**event_id(去重)** | | 拍品列表 | 图录页 `catalog` | Lot 号、标题、详情链接、缩略图、出价数、成交状态、成交价、**item_id(去重)** | | 拍品详情 | `/online-auctions/...` | 标题、成交价、成交状态、出价数、Collection、Sport、Type、Athlete、Team、多图 | --- ## 2. 站点分析结论(为什么这么抓) > 用户要求「优先从接口抓取」。经浏览器抓包确认:**本站为 Bidsquare 纯 SSR 页面,数据全部内嵌在 HTML 中,无独立数据 API**(XHR 仅为 GA/埋点)。因此采用 `curl_cffi` 直接 GET 页面 + `parsel` 解析,无 Cloudflare 挑战,响应 200 纯 HTML。 - **去重键** - 拍卖会:`event_id`(拍卖会卡片 `data-event_id`,如 `21706`,也出现在 URL 尾部) - 拍品:`item_id`(lot 卡片 `data-item_id`,如 `9317607`) - **分页** - 拍卖会列表:`/auctions/past?page=N`,每页 **24** 场,翻到空页为止 - 图录(lot 列表):`{catalog_url}?page=N&limit=120`,`limit` 上限 **120**,翻到空页为止 - **详情页属性区字段数不固定**:`.item-attributes li`(``)可能是 2~5 个,Athlete/Team 常缺失 → 先建 `label→value` 字典,再按需取值,缺失置 `None`。 - **多图归一化**:缩略图 `s1.img.bidsquare.com/item/s/...` 与大图 `/item/l/...` 并存,统一替换成 `/item/l/`(large)后去重,逗号拼接入库。 - **成交状态**:列表卡片底部 `9 BidsSold for$90,000` / `24 BidsUnsold`;详情页 `.bidding-price` 有金额为 `sold`,为空为 `unsold`。 --- ## 3. 文件结构 ``` 2026-07-15(scp_spider)/ ├── application.yml # MySQL 配置(mysql_pool 从运行目录读取) ├── create_table.sql # 建表:scp_auction(场次)+ scp_lot(拍品) ├── scp_core.py # 公用模块:HTTP/代理、列表解析、分页抓取、详情解析、入库 ├── scp_history.py # 任务一:历史全量(一次性) ├── scp_spider.py # 任务二:每周新增(周调度常驻) └── logs/ # 运行日志(loguru 按天切分,保留 7 天) ``` ### 数据表 - **`scp_auction`**:一条 = 一场拍卖会,`event_id` 唯一索引。 - **`scp_lot`**:一条 = 一个拍品,`item_id` 唯一索引;字段分两阶段写入。 两阶段设计: - **阶段一(列表)**:抓拍卖会 + lot 列表入库,`scp_lot.state=0`(含成交价/状态/出价,列表页即有)。 - **阶段二(详情)**:扫库 `state != 1` 的 lot,逐条进详情页补 Collection/Sport/Type/Athlete/Team/多图,成功置 `state=1`,失败置 `2`。 --- ## 4. 使用方式 ### 4.1 前置 1. 安装依赖:`curl_cffi`、`parsel`、`loguru`、`tenacity`、`schedule`,以及全局公共库 `charley-utils`(提供 `mysql_pool`)。 2. 建表:执行 `create_table.sql`(DDL 写操作,需人工确认执行)。 3. 配置 `application.yml` 中的 MySQL 连接。 4. **代理**:本地开了 VPN,从大陆直连站点不通,请求必须走代理。见下方「代理配置」。 ### 4.2 运行 ```bash # 任务一:历史全量(初始化数据库时跑一次) python scp_history.py # 任务二:每周新增(常驻,先立即跑一次,之后每周一 05:00) python scp_spider.py ``` - 每周新增逻辑:GET `/auctions/past` 首页 → 与库中 `event_id` 求差集 → **仅新增拍卖会才入库查询**;随后补跑阶段二,兜底上一轮失败/中断的详情。 --- ## 5. 代理配置 代理集中在 `scp_core.py` 的 `get_proxys()`(带重试)。默认使用住宅代理(北美出口,与 SCP 面向的美区一致): ```python http_proxy = "http://账号:密码@proxy.123proxy.cn:36927" # 若改用本地 VPN 客户端 HTTP 端口(如 Clash),改成: # http_proxy = https_proxy = "http://127.0.0.1:7890" ``` 切换本地 VPN 端口时,把 `get_proxys()` 内两行替换为对应地址即可,其余代码无需改动。 --- ## 6. 反爬与稳健性 - **无 Cloudflare / 无 JS 挑战**:`curl_cffi` 随机浏览器指纹(`impersonate`)+ 通用请求头即可,未写死 UA(避免与 TLS 指纹矛盾)。 - **重试**:页面 GET、代理获取均由 `tenacity` 包裹重试;周调度主函数失败按小时重试。 - **幂等**:`insert_many(ignore=True)` 依赖唯一索引去重,重复跑不会产生脏数据。 - **限频**:站点为静态页,当前未加显式 sleep;如遇风控可在翻页/详情循环处补 `time.sleep`。 --- ## 7. 踩坑记录 - **优先接口落空**:抓包确认本站无数据接口,全部 SSR,最终走 HTML 解析。 - **详情属性字段数量不一致**(2~5 个、可能缺 Athlete/Team):改为按 `label` 建字典按需取,避免按固定顺序取值错位。 - **多图有 small/large 两套**:统一归一化到 large 再去重,避免同图重复入库。 - **时间带时区名**(`EDT`/`EST`):解析时剥离时区名,保留东部墙钟时间入 `datetime` 字段,原文另存 `*_raw` 字段。 - **控制台中文/en-dash 乱码**:Windows GBK 终端显示问题,实际数据 UTF-8 正确,入 `utf8mb4` 库无碍。 - **下架 lot 的 404 死循环**(2026/07/28 修复):个别拍品会被撤下,详情页返回 HTTP 404(页面标题「Nothing Found」)。原先当普通错误重试 5×3 次、置 `state=2`,而补抓查询 `where state != 1` 会把 `state=2` 每轮重新选中 → 该 404 永久循环、阻塞其他待抓 lot。修复:新增 `LotNotFound` 异常,`_get_selector` 遇 404/410/软404 直接抛出且 `retry_if_not_exception_type` 不重试;置终态 `state=3`;补抓查询改为 `where state in (0, 2)`,跳过已成功(1)与永久不存在(3)。 --- ## 8. 举一反三 - 本套「拍卖会 → 图录列表 → 拍品详情」三级 + 两阶段(列表/详情)+ `event_id`/`item_id` 去重 + 首页差集增量,是 Bidsquare 系托管拍卖站的通用范式,换其他 Bidsquare 站点只需调整域名与少量选择器。 - 若某站改为前端渲染(数据走 XHR/JSON),优先复用「浏览器抓包定位接口 → 直接请求接口」路径,比解析 HTML 更稳。