4. 拍卖数据获取:API接口、爬虫与数据清洗
数据获取,是整个量化分析的起点。没有数据,再牛的模型也是空中楼阁。我刚开始做艺术品量化时,最头疼的就是数据源。拍卖行官网、艺术数据库、画廊记录……信息散落在各个角落。今天我们就来聊聊,怎么把这些数据「抓」到手,并且整理成能用的样子。
核心逻辑:API接口(官方通道)→ Scrapy爬虫(补充采集)→ 数据清洗(去杂质)→ 存储(CSV/数据库)。
4.1 API接口:官方数据通道
API,说白了就是官方给你开的后门。你不用去网页上复制粘贴,直接发个请求,数据就整整齐齐地回来了。我个人习惯优先用API,因为稳定、规范、不容易被封。
艺术品拍卖领域,常见的API有这些:
- Artnet API:覆盖全球主要拍卖行的成交记录,数据质量高。不过需要申请密钥,而且有调用次数限制。
- Artsy API:偏向当代艺术,数据更新快。我曾在项目里用它抓取过近5年的当代艺术拍卖数据,效果不错。
- Christie's / Sotheby's 官方API:这两家顶级拍卖行有自己的数据接口,但通常只对合作机构开放。
- MutualArt API:聚合了多家拍卖行的数据,适合做横向对比。
小技巧:很多API有沙箱环境(sandbox),可以先在那里测试,别一上来就怼生产环境。我曾经在Artsy的沙箱里调试了三天,省了不少调用配额。
API调用的基本流程,其实就三步:
- 获取密钥:去官网注册应用,拿到API Key或Token。
- 构造请求:用Python的
requests库,带上参数发GET或POST请求。 - 解析响应:返回的数据通常是JSON格式,用
json.loads()解析成字典。
举个简单的例子,调用Artsy API获取某位艺术家的拍卖记录:
import requests
# 你的API密钥
API_KEY = "your_artsy_api_key_here"
headers = {"X-XAPP-Token": API_KEY}
# 查询艺术家ID为"pablo-picasso"的拍卖作品
url = "https://api.artsy.net/api/artists/pablo-picasso/artworks"
params = {"size": 10, "sort": "-created_at"}
response = requests.get(url, headers=headers, params=params)
data = response.json()
# 提取作品名称和成交价
for artwork in data["_embedded"]["artworks"]:
title = artwork["title"]
price = artwork.get("sale_price", "N/A")
print(f"{title}: {price}")
注意:API调用频率别太快。很多接口限制每秒1-2次请求。我见过有人写了个死循环,5分钟就把月配额用光了……嗯,那个人就是我。
4.2 Scrapy爬虫:网页数据采集
API虽好,但很多中小拍卖行根本没有API。这时候就得靠爬虫了。Scrapy是我最常用的爬虫框架,它比单纯的requests+BeautifulSoup要强大得多。
为什么选Scrapy?三个理由:
- 异步并发:默认支持多线程,爬取速度比单线程快10倍以上。
- 内置中间件:处理Cookie、代理、重定向都很方便。
- 数据管道:爬到的数据可以直接清洗、存储,一条龙服务。
创建一个Scrapy爬虫项目,大概是这样:
# 终端执行
scrapy startproject auction_scraper
cd auction_scraper
scrapy genspider christie_lots "www.christies.com"
然后编辑spiders/christie_lots.py文件:
import scrapy
class ChristieLotsSpider(scrapy.Spider):
name = "christie_lots"
start_urls = ["https://www.christies.com/en/lot/lot-123456"]
def parse(self, response):
# 用XPath提取拍品信息
yield {
"title": response.xpath('//h1[@class="lot-title"]/text()').get(),
"artist": response.xpath('//span[@class="artist-name"]/text()').get(),
"estimate": response.xpath('//span[@class="estimate"]/text()').get(),
"hammer_price": response.xpath('//span[@class="hammer-price"]/text()').get(),
"currency": response.xpath('//span[@class="currency"]/text()').get(),
}
避坑指南:我曾经爬一个欧洲拍卖行网站,发现返回的HTML结构和浏览器里看到的不一样。后来才意识到,网站用了JavaScript动态加载数据。解决办法是:要么用Selenium模拟浏览器,要么直接找XHR请求里的JSON接口。我个人更推荐后者,效率高得多。
运行爬虫:
scrapy crawl christie_lots -o lots.json
数据就会乖乖地存到lots.json文件里。你也可以指定输出为CSV格式:
scrapy crawl christie_lots -o lots.csv
4.3 数据清洗与预处理
数据拿到手,别急着分析。你想想看,爬下来的数据有多脏?
- 价格字段可能是字符串,比如"$1,200,000"或"EUR 800,000"
- 日期格式五花八门:"2023-05-12"、"12 May 2023"、"05/12/23"
- 艺术家名字可能有拼写错误或多余空格
- 有些字段直接是空的
我一般用Pandas来做清洗,它处理表格数据太方便了。核心步骤就这几步:
| 步骤 | 操作 | 代码示例 |
|---|---|---|
| 1. 加载数据 | 读取CSV或JSON | df = pd.read_csv("lots.csv") |
| 2. 去重 | 删除完全重复的行 | df.drop_duplicates(inplace=True) |
| 3. 处理缺失值 | 填充或删除空值 | df.fillna("未知", inplace=True) |
| 4. 格式化价格 | 转为数值型 | df["price"] = df["price"].str.replace("$","").str.replace(",","").astype(float) |
| 5. 统一日期 | 转为标准格式 | df["date"] = pd.to_datetime(df["date"]) |
举个例子,清洗价格字段:
import pandas as pd
df = pd.read_csv("lots.csv")
# 清洗价格:去掉货币符号和逗号
def clean_price(price_str):
if pd.isna(price_str):
return None
# 去掉美元符号、欧元符号、逗号
price_str = price_str.replace("$", "").replace("€", "").replace(",", "")
try:
return float(price_str)
except:
return None
df["hammer_price_clean"] = df["hammer_price"].apply(clean_price)
print(df[["title", "hammer_price", "hammer_price_clean"]].head())
注意:清洗时一定要保留原始字段。我习惯在清洗后的新列名上加个_clean后缀,这样万一清洗逻辑有bug,还能回退到原始数据。别问我怎么知道的……
4.4 数据存储:CSV vs 数据库
清洗完的数据,得找个地方存起来。两种主流方案:
4.4.1 CSV文件
适合小规模数据(几千条以内),简单直接。Excel就能打开,方便人工检查。
df.to_csv("auction_data_clean.csv", index=False, encoding="utf-8-sig")
注意编码用utf-8-sig,不然Excel打开中文会乱码。这个坑我踩过两次。
4.4.2 数据库
数据量大了(几万条以上),或者需要频繁查询,就得用数据库。我推荐SQLite,轻量、无需安装、Python自带支持。
import sqlite3
conn = sqlite3.connect("auction.db")
df.to_sql("lots", conn, if_exists="replace", index=False)
# 查询示例
query = "SELECT artist, AVG(hammer_price_clean) as avg_price FROM lots GROUP BY artist ORDER BY avg_price DESC"
result = pd.read_sql(query, conn)
print(result.head(10))
如果你团队协作,或者数据量上百万,可以考虑MySQL或PostgreSQL。但说实话,个人项目用SQLite完全够了。
我的习惯:原始数据存CSV(方便回溯),清洗后的数据存数据库(方便查询)。两个版本都保留,互不干扰。
好了,数据获取这块就聊到这儿。从API到爬虫,从清洗到存储,每一步都有讲究。记住一个原则:数据质量决定分析质量。花80%的时间在数据准备上,一点都不夸张。