4. 拍卖数据获取:API接口、爬虫与数据清洗

数据获取,是整个量化分析的起点。没有数据,再牛的模型也是空中楼阁。我刚开始做艺术品量化时,最头疼的就是数据源。拍卖行官网、艺术数据库、画廊记录……信息散落在各个角落。今天我们就来聊聊,怎么把这些数据「抓」到手,并且整理成能用的样子。

核心逻辑:API接口(官方通道)→ Scrapy爬虫(补充采集)→ 数据清洗(去杂质)→ 存储(CSV/数据库)。

API接口 官方数据通道 Scrapy爬虫 网页数据采集 数据清洗 去重/补缺/格式化 数据存储 CSV / 数据库 RESTful / JSON XPath / CSS选择器 Pandas处理 SQLite / MySQL 数据流方向:从采集到存储 每个环节都可能遇到坑,后面会细说

4.1 API接口:官方数据通道

API,说白了就是官方给你开的后门。你不用去网页上复制粘贴,直接发个请求,数据就整整齐齐地回来了。我个人习惯优先用API,因为稳定、规范、不容易被封。

艺术品拍卖领域,常见的API有这些:

  • Artnet API:覆盖全球主要拍卖行的成交记录,数据质量高。不过需要申请密钥,而且有调用次数限制。
  • Artsy API:偏向当代艺术,数据更新快。我曾在项目里用它抓取过近5年的当代艺术拍卖数据,效果不错。
  • Christie's / Sotheby's 官方API:这两家顶级拍卖行有自己的数据接口,但通常只对合作机构开放。
  • MutualArt API:聚合了多家拍卖行的数据,适合做横向对比。

小技巧:很多API有沙箱环境(sandbox),可以先在那里测试,别一上来就怼生产环境。我曾经在Artsy的沙箱里调试了三天,省了不少调用配额。

API调用的基本流程,其实就三步:

  1. 获取密钥:去官网注册应用,拿到API Key或Token。
  2. 构造请求:用Python的requests库,带上参数发GET或POST请求。
  3. 解析响应:返回的数据通常是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%的时间在数据准备上,一点都不夸张。


专注资料整理