|
|
1 týždeň pred | |
|---|---|---|
| .. | ||
| README.md | 1 týždeň pred | |
| YamlLoader.py | 1 týždeň pred | |
| application.yml | 1 týždeň pred | |
| mysql_pool.py | 1 týždeň pred | |
| rea_core.py | 1 týždeň pred | |
| rea_history.py | 1 týždeň pred | |
| rea_spider.py | 1 týždeň pred | |
| requirements.txt | 1 týždeň pred | |
rea-image-archive.nyc3.cdn.digitaloceanspaces.com)curl_cffi(带 impersonate 浏览器指纹,过 Cloudflare 的 TLS 指纹校验)parsel.Selector(CSS + XPath)schedule;日志:loguru;重试:tenacity;连接池:charley-utils 的 MySQLConnectionPool/search 场次列表 → /archives/{year}/{month}/ 场次内 lot 列表 → lot 详情页
rea_record,见 create_table.sql)| 字段 | 阶段 | 说明 |
|---|---|---|
auction_key |
一 | 场次唯一标识,如 2025/spring(由 URL 派生,增量差集依据) |
auction_name |
一 | 场次展示名,如 Spring 2025 |
lot_id |
一 | 站内 lot 唯一 id(列表卡片的数字 wire:key) |
lot_number |
一 | lot 编号(详情 URL 中的段) |
slug |
一 | 详情 URL 末段 slug |
detail_url |
一 | 详情页绝对 URL(唯一索引,幂等去重) |
title |
二 | 拍品标题(详情页 h1) |
sold_for |
二 | 成交价,纯数字串如 336000;未成交为空 |
year |
二 | 拍品年份,如 1916 |
auction |
二 | 详情页场次名,如 2025 Spring |
lot_no |
二 | 详情页 Lot 编号(应与 lot_number 一致,可交叉校验) |
category |
二 | 分类,如 Prewar Baseball Cards (1900-1941) |
imgs |
二 | 详情多图 URL 逗号拼接 |
state |
二 | 详情抓取状态:0/null 待抓,1 已抓,2 失败 |
阶段一(
crawl_one_auction)只产出前 6 个字段;title~imgs、state由阶段二(update_details_for_pending)写入。这是主公在 img_1.png 里圈定的「标题 + 5 行明细 + 多图」。
| 接口 | 方法 | 用途 |
|---|---|---|
/search |
GET | 场次列表页。三个 tab 面板:RECENT / AUCTION ARCHIVE / PAST CATALOG |
/archives/{year}/{month}/?page=N&pageSize=M |
GET | 某场次的 lot 列表,用查询参数翻页 |
/archives/{year}/{Season}/{lotNumber}/{slug} |
GET | lot 详情页,取标题 / 5 字段 / 多图 |
/search 页是 Alpine.js 的 tab 组件。注意 curl_cffi 拿到的是 Alpine 未执行的原始 HTML,
面板不能靠 Alpine 解析后的 aria-labelledby="tab-1-1" 区分,只能按 role="tabpanel" 的文档顺序:
| 面板顺序 | tab | 内容 | 数量 | 本爬虫 |
|---|---|---|---|---|
| 第 1 个含链接面板 | RECENT AUCTIONS(最近) | Swiper 封面卡 | 16 | 增量只抓这个 |
| 第 2 个含链接面板 | AUCTION ARCHIVE(历史) | 按年份分组文字链 | 68 | 全量抓这个 |
| 第 3 个面板 | PAST CATALOG | <select> 往期图录(/catalog/...) |
9 | 不在范围内 |
原始 HTML 最外层还有一个「包裹用」的
<section role="tabpanel">嵌套了全部面板, 解析时用 XPath//*[@role="tabpanel" and not(.//*[@role="tabpanel"])]只取叶子面板,避免误取到外壳。 RECENT + ARCHIVE 合计 84 场即全量。
场次页 /archives/{year}/{month}/ 是一个 Livewire 搜索组件,预置过滤了该场次
(Auction=['2025-Spring']),默认 pageSize=12、sortBy=Price:desc。
翻页无需逆向 Livewire 协议:Livewire WithPagination 默认把页码同步到 URL 查询串,
实测 ?page=N&pageSize=M、?sortBy=Price:asc|Price:desc、?Category[]=<分类名> 都直接对
SSR 页面生效(pageSize 最高 1000)。超出末页服务端返回空列表 → 作为翻页停止条件。
⚠️ 1000 条硬上限(关键坑):搜索结果有 1000 条总量窗口——任何排序下深翻页最多只能取到
前 1000 条(实测 pageSize=1000 第 2 页即返回 0)。而单场次动辄数千件(如 2025 Spring 约 3653 件),
直接整场翻页会静默丢失 1000 之后的全部数据。
应对(fetch_auction_lots 已实现):改为逐个 Category 分类过滤抓取——
parse_category_facet,来自 input[name="Category[]"] + 相邻 <label> 的 (N))。?Category[]=<分类名> 过滤翻页(各分类天然只属该场次,且绝大多数 <1000,可整取)。sortBy=Price:asc(取前 1000)+ sortBy=Price:desc(取前 1000)并按 detail_url 去重,可覆盖到 2000。实测 2025 Spring:17 个分类合计 3653 件全部取到(其中 Postwar 分类 1108 件 = asc 1000 + desc 108,去重后正好 1108)。
列表卡片信息极简:只有数字 wire:key(=lot_id)、详情链接(含 lot_number 与 slug)。
筛选器控件的 wire:key 是 item-... / Category 等非数字,天然与 lot 卡片区分;
数字 wire:key 与详情 <a> 按文档顺序 1:1 对应,逐条配对回填 lot_id。
h1,其一为 "xxx - Item detail" 面包屑,取另一条真实标题。Sold For / Year / Auction / Lot # / Category 在 <dl> 的 dt/dd 对里。rea-image-archive CDN 上的 -N.jpg,去重并保序拼接。| 文件 | 角色 |
|---|---|
rea_core.py |
公用核心:HTTP/指纹/代理/重试、场次列表解析、lot 分页抓取、详情解析、两阶段入库 |
rea_history.py |
一次性全量:抓「最近 + 历史归档」全部 84 场,初始化数据库时跑一次 |
rea_spider.py |
日调度增量:只看「最近」板块,查库 auction_key 差集,仅抓新增场次 |
create_table.sql |
建表脚本(表 rea_record) |
application.yml |
数据库配置(MySQLConnectionPool 读运行目录下这份) |
列表抓取与详情抓取完全分离:列表先入库(state 默认 0),详情阶段再扫库 state != 1 的记录逐条补齐。
好处是列表抓取快、可断点续补,详情失败(state=2)不影响列表、下轮自动重试。
阶段一 · 列表(crawl_one_auction → fetch_auction_lots)
GET /search → parse_auction_list 取场次(only_recent 决定取「最近」还是全部)。GET .../?Category[]=<分类>&page=N&pageSize=200
(超 1000 的分类再加 &sortBy=Price:asc|desc),parse_lot_cards 解析卡片,
以 detail_url 全局去重,空页即停。详见上文「绕过 1000 条上限」。sql_pool 不为 None 时 insert_many(table="rea_record", ignore=True) 入库。阶段二 · 详情(update_details_for_pending → get_details → fetch_lot_detail)
select id, detail_url from rea_record where state != 1 or state is null 取待补记录。GET 详情页 → parse_lot_detail 解析标题 + 5 字段 + 多图。title~imgs、state=1;整条失败则 state=2。rea_spider.py 的 get_existing_auction_keys 会 select distinct auction_key from rea_record,
与「最近」面板解析出的场次做差集,仅抓新增场次。REA 场次一旦结束即定型,新场次会出现在「最近」板块顶部,
老场次早已在库,故增量只需盯「最近」。sql_pool=None 时视作库内无任何场次 = 全量重抓最近板块。
charley-utils(全局 editable 安装,提供 mysql_pool / YamlLoader)
pip install curl_cffi parsel loguru tenacity schedule
先按 create_table.sql 建表,并确认 application.yml 的 mysql 配置正确:
mysql -u<user> -p <db> < create_table.sql
# 全量历史(初始化用,跑一次;84 场 × 数百~千余 lot,较慢)
python rea_history.py
# 日调度增量(生产用,常驻)
python rea_spider.py
rea_spider.py 默认:先即时跑一次,之后每天 05:00 跑一次增量。只想手动跑一次时,
把 schedule_task() 注释掉、保留 rea_main(log=logger) 即可。
rea_history.py 顶部 DEBUG_AUCTION_LIMIT:设 1 只抓前 1 个场次,设 None 全抓。sql_pool 临时改为 None,crawl_one_auction 会跳过入库、print 样本(阶段二自动跳过)。requests 直连易被拦;统一用 curl_cffi + 随机 impersonate 浏览器指纹直连即可。若后续被限流,在 rea_core.get_proxys 填入代理(函数已带 tenacity 重试)。curl_cffi 看到的是 Alpine 未执行的原始 HTML,所有选择器均按原始 HTML 校准;调试时若用浏览器 outerHTML(Alpine 已执行)会有出入,切记区分。fetch_auction_lots 靠「空页」停止,并以 detail_url 去重兜底分页异常。若站点未来改分页机制,重点核对 ?page=/?pageSize= 是否仍生效。Sold For 明细,sold_for 会为空,属正常。insert_many(ignore=True) + detail_url 唯一索引,重复跑不会产生重复记录;增量重跑安全。WithPagination 的列表页,多数支持 ?page=N 甚至 ?pageSize=M 直接翻页(页码同步到 URL),不必逆向 /livewire/update 的 snapshot/checksum 协议——先试查询参数,成本最低。x-show 切换显隐;用 role="tabpanel" 文档顺序 + 叶子节点过滤来区分,比依赖 Alpine 运行后的属性稳。-1.jpg、-2.jpg…同分辨率;部分同类站有 _lg/_med/_sml 分级,按需保留。