ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Scrapy足球数据抓取工程骨架:从反爬到结构化存储

2026/9/12 22:47:42 拓冰建站 浏览量
Scrapy足球数据抓取工程骨架:从反爬到结构化存储 简介这是一套面向Python开发者与体育数据分析初学者的足球比赛数据抓取工具源码聚焦解决赛事数据自动化采集难题适用于球队表现分析、赔率研究、训练效果评估等实际场景。资源共31个文件含15个Python源码如spiders/下的matchGet.py、bifenGet.py等爬虫核心模块、11个pyc编译文件用于快速部署运行、3个txt文本含urlList.txt等目标站点配置、1个scrapy.cfg框架配置文件及1个teacher.json示例数据文件整体压缩包仅83KB轻量易上手。已有377人学习下载体现其在小规模实战项目中的实用价值。读者可直接复用Scrapy框架结构掌握动态URL调度、多页面解析、结构化数据存储soccerResult.py等关键能力并通过readme.txt快速理解项目逻辑与运行流程是学习网络爬虫工程化实践的典型范例。1. 这不是“爬个网页就完事”的玩具项目一个能跑通完整足球数据链路的 Scrapy 工程骨架你可能试过用requests BeautifulSoup抓几场英超比分但很快会卡在反爬验证、动态加载、多页面跳转、数据结构不一致、IP 被封、结果无法对齐等环节——而这个名为soccerResult的项目恰恰绕开了“单点突破”的陷阱直接交付了一套可落地、可调试、可扩展的足球比赛数据抓取工程骨架。它不是教你怎么写soup.find()而是把从 URL 列表管理urlList.txt、IP 代理轮换GetIp.py、多 Spider 协同matchGet.py/bifenGet.py/oddGet.py、字段清洗pipelines.py、结构化存储soccerResult.py到命令行调度cmdline_match.py等全部串成闭环。适合两类人一是刚学完 Scrapy 基础、正卡在“写完第一个 spider 就不知道下一步干啥”的中级学习者二是需要快速搭建赛事数据采集通道、但不想从零造轮子的数据分析师或体育科技团队工程师。它不承诺“一键抓全全球联赛”但提供了所有关键模块的命名规范、调用契约和错误埋点位置——这才是真实生产环境中最稀缺的“可维护性”。2. 为什么选 Scrapy 而非 requests asyncio从scrapy.cfg到spiders/目录的工程逻辑拆解Scrapy 不是“更高级的 requests”它的价值在于强制你把数据采集过程拆解为可复用、可监控、可插拔的标准化阶段。这个项目正是以scrapy.cfg为起点构建出符合框架范式的分层结构。我们先看配置锚点再深入spiders/目录的设计意图。2.1scrapy.cfg不只是路径声明而是部署契约的起点该文件内容虽短但定义了整个项目的运行上下文[settings] default soccerResult.settings [deploy] project soccerResult提示default soccerResult.settings表明项目根目录下必须存在soccerResult/包且其内含settings.py。这不是约定俗成而是 Scrapy 启动时解析scrapy crawl命令的硬性依赖。若你将项目重命名为football_crawler却未同步修改此处scrapy crawl match会直接报错ModuleNotFoundError: No module named football_crawler.settings。settings.py中的关键配置项决定了抓取行为的底层逻辑配置项典型值作用说明BOT_NAMEsoccerResult爬虫标识名影响 User-Agent 和日志前缀SPIDER_MODULES[soccerResult.spiders]框架自动扫描 spider 类的位置必须与实际目录结构严格一致DOWNLOAD_DELAY1.5请求间隔秒规避服务器限速数值需结合目标站点 robots.txt 调整COOKIES_ENABLEDFalse关闭 Cookie 持久化适用于无登录态的静态比分页若需抓取需登录的赔率详情则需设为True并配合LoginSpiderITEM_PIPELINES{soccerResult.pipelines.SoccerResultPipeline: 300}定义数据处理流水线数字越小优先级越高此处仅启用一个清洗管道2.2spiders/目录按业务维度切分而非技术维度matchGet.py与bifenGet.py的协同设计项目未采用“一个 spider 抓所有”的粗放模式而是将足球数据流拆解为三个核心业务单元matchGet.py负责抓取赛程主列表页如“2023-24 英超赛程”提取每场比赛的唯一 ID 和基础链接bifenGet.py基于matchGet输出的 ID抓取实时比分与事件流角球、黄牌、进球时间oddGet.py同样基于 ID抓取赔率变化历史亚盘、欧赔、大小球。这种拆分不是为了炫技而是解决三个现实问题①页面加载策略不同赛程页多为静态 HTML比分页常含 AJAX 动态刷新赔率页则可能嵌套 iframe 或 WebSocket②更新频率差异大赛程页每月更新一次比分页需分钟级轮询赔率页甚至需秒级监听③失败隔离需求若赔率接口临时不可用不应阻塞比分数据入库。查看matchGet.py的核心片段# soccerResult/spiders/matchGet.py import scrapy from soccerResult.items import MatchItem class MatchSpider(scrapy.Spider): name match allowed_domains [score.bet365.com] # 示例域名实际需替换 start_urls [https://score.bet365.com/soccer/england/premier-league/] def parse(self, response): # 提取比赛链接假设结构为 a href/match/123456 for href in response.css(div.match-item a::attr(href)).getall(): if href and /match/ in href: yield scrapy.Request( urlresponse.urljoin(href), callbackself.parse_match_detail, meta{match_id: href.split(/)[-1]} # 提取ID传入下一级 ) def parse_match_detail(self, response): item MatchItem() item[match_id] response.meta[match_id] item[home_team] response.css(span.home-team::text).get() item[away_team] response.css(span.away-team::text).get() item[start_time] response.css(time.start-time::attr(datetime)).get() # 注意此处未抓取比分因比分由 bifenGet.py 专项处理 yield item注意meta{match_id: ...}是 Scrapy 在请求间传递上下文的标准方式。bifenGet.py的start_requests方法会读取urlList.txt中的match_id列表构造独立请求避免与matchGet的 DOM 解析强耦合。这种解耦让bifenGet可单独运行如只更新昨日比赛比分无需重跑整个赛程抓取。2.3items.py定义数据契约而非随意字典items.py中的MatchItem类不是装饰性代码而是数据流的类型契约# soccerResult/items.py import scrapy class MatchItem(scrapy.Item): match_id scrapy.Field() # 字符串ID全局唯一 home_team scrapy.Field() # 主队名称清洗后如Manchester City away_team scrapy.Field() # 客队名称 start_time scrapy.Field() # ISO8601 时间字符串如 2024-05-19T15:00:0000:00 status scrapy.Field() # 比赛状态not_started, in_play, finished # 注意比分字段未在此定义因属于 bifenGet.py 的职责范围提示scrapy.Field()本身不校验类型但它是 Pipeline 中做类型转换的标记点。例如pipelines.py的SoccerResultPipeline.process_item()会检查item.get(start_time)是否为有效 datetime 字符串若为空则抛出DropItem(Missing start_time)该 item 将被丢弃且不进入后续 pipeline 或存储。3. 数据清洗与持久化从pipelines.py到soccerResult.py的字段归一化实践抓取来的原始 HTML 文本充满噪声球队名缩写不统一Man City vs Manchester City、时间格式混杂May 19, 2024 / 19/05/2024 / 2024-05-19、比分字段含空格或特殊符号2 : 1。pipelines.py承担着将脏数据转化为分析友好格式的核心任务而soccerResult.py则定义了最终落库的载体。3.1pipelines.py三阶段清洗链——标准化 → 校验 → 归一化该文件实现了SoccerResultPipeline类其process_item方法构成清洗流水线# soccerResult/pipelines.py import re from datetime import datetime from scrapy.exceptions import DropItem class SoccerResultPipeline: def process_item(self, item, spider): # 阶段1标准化去除首尾空格、统一编码 for field in [home_team, away_team]: if item.get(field): item[field] item[field].strip().replace(\u3000, ) # 全角空格转半角 # 阶段2时间字段校验与转换 if item.get(start_time): try: # 尝试多种常见格式 dt datetime.fromisoformat(item[start_time].replace(Z, 00:00)) except ValueError: # fallback尝试 YYYY-MM-DD HH:MM 格式 try: dt datetime.strptime(item[start_time], %Y-%m-%d %H:%M) except ValueError: raise DropItem(fInvalid start_time format: {item[start_time]}) item[start_time] dt.isoformat() # 统一输出 ISO8601 # 阶段3球队名归一化映射到标准名称 team_mapping { Man City: Manchester City, Man Utd: Manchester United, Spurs: Tottenham Hotspur, Leeds: Leeds United } for field in [home_team, away_team]: if item.get(field) in team_mapping: item[field] team_mapping[item[field]] return item注意DropItem异常是 Scrapy 的标准中断机制。当start_time无法解析时该 item 被丢弃不会进入数据库但会在日志中记录Dropped: Invalid start_time format...。这比让脏数据入库后再清洗更高效也避免了下游分析时的NULL处理开销。3.2soccerResult.py不只是文件名而是数据落地的物理载体项目中的soccerResult.py并非 spider 或 pipeline而是一个数据模型定义文件其核心是SoccerResultStorage类# soccerResult/soccerResult.py import json import os from pathlib import Path class SoccerResultStorage: def __init__(self, output_diroutput): self.output_dir Path(output_dir) self.output_dir.mkdir(exist_okTrue) def save_match(self, item): 保存单场比赛基础信息为 JSON 文件 filename fmatch_{item[match_id]}.json filepath self.output_dir / filename # 仅保存 items.py 中定义的字段忽略 pipeline 添加的临时字段 data {k: v for k, v in item.items() if k in [match_id, home_team, away_team, start_time, status]} with open(filepath, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) def save_bifen(self, match_id, bifen_data): 保存比分数据按 match_id 分目录存储 match_dir self.output_dir / bifen / match_id match_dir.mkdir(parentsTrue, exist_okTrue) # bifen_data 是 list of dict如 [{minute: 23, event: goal, team: home}, ...] with open(match_dir / events.json, w, encodingutf-8) as f: json.dump(bifen_data, f, ensure_asciiFalse, indent2)提示save_bifen方法将比分事件按match_id创建子目录而非扁平化存为单一文件。这解决了两个痛点① 避免单目录下数万文件导致的ls命令卡顿② 支持按比赛 ID 快速定位全部相关数据基础信息 比分事件 赔率变化。这种目录结构设计是面向后续数据分析如 Pandaspd.read_json(output/bifen/123456/events.json)的友好实践。3.3urlList.txt与urlList222.txt手动维护的 URL 清单如何参与自动化流程项目包含两个文本文件表面看只是 URL 列表实则是控制抓取粒度的开关urlList.txt存放当前赛季所有比赛的 ID 列表每行一个 ID如123456供bifenGet.py读取并构造请求urlList222.txt可能是上一赛季或测试用 ID 列表用于灰度验证新 pipeline 逻辑。bifenGet.py的start_requests方法会主动读取这些文件# soccerResult/spiders/bifenGet.py def start_requests(self): # 读取 urlList.txt 获取待抓取 match_id with open(urlList.txt, r, encodingutf-8) as f: ids [line.strip() for line in f if line.strip()] for match_id in ids: url fhttps://api.example.com/match/{match_id}/live yield scrapy.Request(urlurl, callbackself.parse_bifen, meta{match_id: match_id})注意此写法将文件路径硬编码为urlList.txt意味着该文件必须位于 Scrapy 项目根目录与scrapy.cfg同级。若需切换数据源只需替换urlList.txt内容无需修改代码——这是运维友好的设计。4. 命令行调度与 IP 轮换cmdline_match.py与GetIp.py的实战调用链Scrapy 自带scrapy crawl命令但本项目额外提供cmdline_*.py文件目的是封装环境准备 → 参数注入 → 结果验证的完整工作流。GetIp.py则解决动态 IP 的刚需二者共同构成生产级抓取的基础设施。4.1cmdline_match.py不只是启动脚本而是参数化执行入口该文件本质是一个独立 Python 脚本用于替代scrapy crawl match的裸命令# cmdline_match.py from scrapy.crawler import CrawlerProcess from scrapy.utils.project import get_project_settings from soccerResult.spiders.matchGet import MatchSpider if __name__ __main__: # 加载项目 settings process CrawlerProcess(get_project_settings()) # 注入运行时参数指定起始 URL 和下载延迟 custom_settings { DOWNLOAD_DELAY: 2.0, # 比 settings.py 中的默认值更保守 USER_AGENT: soccerResult-cmdline/1.0 (dataanalyst.org) # 显式声明 UA } # 启动 spider传入自定义设置 process.crawl(MatchSpider, **custom_settings) process.start()提示CrawlerProcess是 Scrapy 提供的进程级启动器适用于脚本化调用。相比scrapy crawl它允许你在 Python 层面动态修改settings如根据目标网站响应速度调整DOWNLOAD_DELAY并可在process.start()前插入日志初始化、数据库连接等前置操作。4.2GetIp.py轻量级代理池实现不依赖第三方服务该项目未使用付费代理 API而是通过GetIp.py实现本地 IP 池管理# GetIp.py import random import time class IpPool: def __init__(self): # 模拟从公开免费代理站抓取的 IP 列表实际需自行填充 self.ip_list [ 118.190.95.43:9001, 117.156.123.45:8080, 220.181.112.244:8080 ] self.last_used {} def get_ip(self, keydefault): 获取一个 IP支持按 key 隔离使用如 match/bifen/odd 分开 if key not in self.last_used: self.last_used[key] 0 ip self.ip_list[self.last_used[key] % len(self.ip_list)] self.last_used[key] 1 return ip def is_valid(self, ip): 简单验证 IP 可用性实际应加入 requests.head 测试 return : in ip and len(ip.split(:)) 2 # 全局实例 ip_pool IpPool() if __name__ __main__: print(Current IP:, ip_pool.get_ip(match)) # 输出118.190.95.43:9001注意GetIp.py本身不直接被 spider 调用而是通过settings.py中的DOWNLOADER_MIDDLEWARES注入# settings.py DOWNLOADER_MIDDLEWARES { soccerResult.middlewares.RandomUserAgentMiddleware: 400, soccerResult.middlewares.ProxyMiddleware: 543, # 此 middleware 会调用 GetIp.ip_pool.get_ip() }ProxyMiddleware的核心逻辑是每次请求前调用ip_pool.get_ip(spider.name)获取对应 spider 的 IP设置request.meta[proxy]。这种设计让matchGet、bifenGet可使用不同 IP 池降低被封风险。4.3 一次完整的端到端抓取验证从启动到数据落地执行以下命令即可完成最小闭环验证# 1. 确保已安装依赖Scrapy 及其依赖 pip install scrapy # 2. 运行 match 抓取生成 urlList.txt python cmdline_match.py # 3. 检查 urlList.txt 是否生成应有若干 match_id head -n 5 urlList.txt # 4. 运行 bifen 抓取需确保 urlList.txt 非空 python cmdline_bifen.py # 5. 验证数据是否落地 ls -l output/match_*.json | head -n 3 ls -l output/bifen/*/events.json | head -n 3若第 5 步看到类似output/match_123456.json和output/bifen/123456/events.json的文件则证明整个链路URL 生成 → 比分抓取 → 清洗 → 存储已跑通。此时output/目录即为后续数据分析的原始数据源。5. 排查常见故障当scrapy crawl bifen报错KeyError: match_id时如何定位 pipeline 与 spider 的契约断裂点这类错误高频出现在bifenGet.py的parse_bifen方法中表面是字典键缺失根源却是matchGet.py与pipelines.py之间的字段契约未对齐。以下是系统化排查路径5.1 错误现场还原与日志定位假设执行scrapy crawl bifen后报错KeyError: match_id File /path/to/soccerResult/spiders/bifenGet.py, line 45, in parse_bifen item[match_id] response.meta[match_id]首先确认response.meta是否携带match_id# 在 bifenGet.py 的 parse_bifen 开头添加调试日志 def parse_bifen(self, response): self.logger.info(fResponse meta keys: {list(response.meta.keys())}) self.logger.info(fResponse meta content: {response.meta}) # ... rest of code若日志显示Response meta keys: []说明start_requests构造的scrapy.Request未正确传入meta。5.2 检查start_requests的meta注入逻辑bifenGet.py的start_requests应如下def start_requests(self): with open(urlList.txt, r) as f: for line in f: match_id line.strip() if match_id: url fhttps://example.com/match/{match_id}/live yield scrapy.Request( urlurl, callbackself.parse_bifen, meta{match_id: match_id} # ← 关键必须显式传入 )提示若此处遗漏meta{match_id: match_id}response.meta将为空字典response.meta[match_id]必然报错。这是新手最常犯的错误也是cmdline_bifen.py封装的价值——它可内置此类校验。5.3 验证pipelines.py是否意外删除了match_id即使meta传入正确若SoccerResultPipeline.process_item()中有误删逻辑也会导致 item 缺失字段。检查pipelines.py中是否有类似代码# ❌ 危险写法无条件 pop 字段 item.pop(match_id, None) # 若存在则删除导致下游丢失正确做法是仅对需清洗的字段操作match_id作为主键应原样保留# ✅ 安全写法只处理明确字段 for field in [home_team, away_team, start_time]: if field in item: # 执行清洗... pass # match_id 不参与任何清洗直接透传5.4 使用 Scrapy Shell 快速验证 XPath/CSS 选择器当parse_bifen中的response.css(...)返回空时需验证选择器是否匹配目标 HTML# 启动 shell 并加载测试页面 scrapy shell https://example.com/match/123456/live # 在 shell 中执行 CSS 选择器 response.css(div.event-time::text).getall() [] # 返回空说明选择器错误 # 尝试更宽松的选择器 response.xpath(//div[contains(class, event)]/span[classminute]/text()).getall() [23, 452, 78]注意Scrapy Shell 是调试选择器的黄金工具。务必在真实响应 HTML 上测试而非仅凭浏览器开发者工具的渲染结果——后者可能含 JS 动态注入内容而 Scrapy 默认不执行 JS。最终当output/bifen/123456/events.json成功生成且含有效事件数组时KeyError: match_id问题即宣告解决。此时你已穿透了从 URL 输入、请求构造、响应解析到数据落库的全链路具备了独立维护和扩展该足球数据抓取器的能力。本文还有配套的精品资源点击获取