GBGB(Greyhound Board of Great Britain)是英国赛狗运动的官方管理机构,其官网提供了完整的比赛结果查询功能。相比传统网页爬虫,直接调用其背后的API接口可以更稳定、更高效地获取结构化数据。本文将以Python为例,带你完整实现一套按日期和赛道抓取GBGB赛狗结果数据的采集程序。

一、分析GBGB的结果查询接口
打开GBGB官网的结果查询页面,通过浏览器的开发者工具(F12)切换到Network标签页,选择XHR过滤,就能观察到页面在查询时实际请求的接口地址。结果查询的核心接口大致形式如下:
https://api.gbgb.org.uk/api/results?date=2024-05-01&track=Newcastle
这个接口返回标准的JSON数据,包含当天该赛道的所有比赛场次(Race)以及每场比赛的详细结果,包括犬只名称、名次、起跑箱号、SP赔率、最终时间、训犬师等字段。需要注意的是,接口的域名和参数格式可能随官方调整而变化,动手前务必先自己抓包确认一遍。
接口通常支持的参数包括:
date:比赛日期,格式为YYYY-MM-DDtrack:赛道名称,如Newcastle、Romford等page:分页参数,部分查询结果较多时需要翻页
返回的JSON结构一般是嵌套的字典和列表,先手动请求一次,把返回内容格式化打印出来,弄清字段层级后再编写解析代码,可以少走很多弯路。
二、用requests封装请求并解析数据
确认接口后,我们用requests库编写基础的请求函数。为了降低被拦截的概率,建议带上浏览器标准的请求头,尤其是User-Agent字段。下面是完整的请求与解析示例:
import requests
def fetch_results(date: str, track: str) -> dict:
"""按日期和赛道抓取GBGB赛狗结果"""
url = "https://api.gbgb.org.uk/api/results"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept": "application/json",
"Referer": "https://www.gbgb.org.uk/"
}
params = {"date": date, "track": track}
resp = requests.get(url, headers=headers, params=params, timeout=15)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
data = fetch_results("2024-05-01", "Newcastle")
print(data)拿到JSON后,解析的关键是找准字段路径。假设返回数据中results字段是比赛列表,每场比赛又有greyhounds字段保存参赛犬只明细,可以这样提取核心信息:
def parse_races(data: dict) -> list:
"""提取每场比赛的关键信息"""
rows = []
for race in data.get("results", []):
race_time = race.get("raceTime", "")
race_name = race.get("raceName", "")
for dog in race.get("greyhounds", []):
rows.append({
"date": race.get("date", ""),
"track": race.get("track", ""),
"race_time": race_time,
"race_name": race_name,
"position": dog.get("position"),
"trap": dog.get("trap"),
"dog_name": dog.get("name", ""),
"sp": dog.get("sp", ""),
"run_time": dog.get("runTime", ""),
"trainer": dog.get("trainer", "")
})
return rows解析时建议统一使用.get()方法并提供默认值,避免个别场次缺少某个字段时抛出KeyError导致整个采集任务中断。犬只名次字段在不同接口版本中可能是position或finishingPosition,实际使用时根据抓包结果调整即可。
三、批量采集与稳定性优化
如果要抓取一段日期范围内的多个赛道,就需要循环拼接参数并控制请求节奏。高频请求不仅容易触发对方风控,也会给服务器造成压力,务必在每次请求之间加入延迟。同时加上异常重试机制,保证长时间任务稳定运行:
import time
import random
from datetime import date, timedelta
def daterange(start: date, end: date):
d = start
while d <= end:
yield d
d += timedelta(days=1)
TRACKS = ["Newcastle", "Romford", "Sunderland", "Yarmouth"]
def crawl(start: date, end: date):
all_rows = []
for d in daterange(start, end):
for track in TRACKS:
for attempt in range(3): # 最多重试3次
try:
data = fetch_results(d.isoformat(), track)
all_rows.extend(parse_races(data))
break
except requests.RequestException as e:
print(f"请求失败: {d} {track}, 第{attempt+1}次: {e}")
time.sleep(5)
time.sleep(random.uniform(1.5, 3.5)) # 随机延迟,模拟人工访问
return all_rows采集到的数据建议直接保存为CSV文件或写入SQLite数据库,方便后续用pandas做统计分析。CSV保存非常简单,一行代码即可:
import csv
def save_csv(rows: list, filename: str):
if not rows:
return
keys = rows[0].keys()
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
writer.writerows(rows)使用utf-8-sig编码可以避免Excel打开中文时出现乱码。如果数据量大、需要增量更新,推荐改用SQLite,按比赛日期和场次ID建立唯一索引,每次采集时用INSERT OR IGNORE跳过已存在的记录,就能实现幂等的增量采集。
四、常见问题与注意事项
第一,接口返回为空。有些赛道当天没有比赛,接口可能返回空列表或错误信息,代码中要对此做容错判断,不要当成异常处理。第二,日期边界问题。英国时间和本地时间存在时差,抓取当天数据时建议以英国当地日期为准,避免漏抓或重复。第三,遵守爬虫礼仪。控制请求频率、设置合理的超时时间、不要在高峰期大规模并发请求,这既是技术要求,也是对数据提供方的尊重。
另外,如果接口启用了更严格的反爬机制(比如动态Token校验),可以考虑使用cloudscraper库或者结合Selenium先获取有效会话再回退到requests。不过就目前的实际情况来看,GBGB的接口对正常频率的请求是比较宽容的,规范使用requests就足够了。
总结一下,整套流程的核心就是:抓包分析接口、封装请求函数、容错解析JSON、循环批量采集、控制节奏并持久化存储。掌握这套思路后,你可以把它迁移到几乎所有提供JSON接口的数据网站上,快速搭建属于自己的数据采集管道。