基于Python的Codeforces竞赛数据抓取与分析实战

发布时间:2026/7/21 8:08:47
基于Python的Codeforces竞赛数据抓取与分析实战 这次我们来看一个针对 Codeforces 平台的技术分析项目。Codeforces 是全球知名的在线编程竞赛平台其举办的常规轮次Regular Round和 Educational Round 是算法竞赛选手提升能力、检验水平的核心战场。然而面对海量的比赛题目如何高效回顾、总结规律甚至自动化分析题目难度与趋势成为了许多开发者和竞赛选手的实际需求。这个项目的核心就是通过技术手段对 Codeforces 上半年的所有常规轮次进行系统性的“锐评”——即深入的数据抓取、分析与可视化。它不是一个简单的爬虫而是一套集成了数据获取、清洗、指标计算与报告生成的分析工具链。对于算法竞赛爱好者、培训教练或是希望研究题目出题规律的数据爱好者而言这套工具能提供远超手动浏览的洞察效率。本文将带你从零开始理解并实践这套分析流程。我们会重点关注几个核心问题如何稳定、合规地抓取 Codeforces 的公开数据如何设计分析指标来“锐评”题目和比赛如何将分析结果转化为直观的可视化报告整个过程将涉及网络请求、数据解析、本地存储、以及基础的数据分析适合有一定 Python 基础的开发者或对竞赛数据分析感兴趣的选手。1. 核心能力速览能力项说明项目类型Codeforces 公开数据抓取与分析工具链核心功能自动抓取比赛列表、题目详情、提交记录计算题目难度、标签分布、通过率等指标生成可视化分析报告技术栈Python (requests, BeautifulSoup, pandas, matplotlib/selenium 可选)数据来源Codeforces 公开 API 及网页 (遵守 robots.txt 与访问频率限制)输出形式结构化数据 (CSV/JSON)、统计图表、文本分析报告适合场景个人竞赛复盘、出题规律研究、训练计划制定、社群技术分享使用边界仅用于分析公开数据严禁攻击、爬取用户非公开信息、干扰网站正常运行2. 适用场景与使用边界2.1 谁适合使用这套分析方法算法竞赛选手通过分析历史题目了解高频考点、难度变化趋势针对性补强薄弱环节。竞赛教练/培训师基于数据制定训练计划筛选经典题目分析不同赛事的风格差异。开源项目开发者需要维护与 Codeforces 题库同步的工具如离线题库、题目推荐系统。数据科学爱好者将竞赛平台作为数据集练习数据采集、清洗、分析与可视化技能。2.2 它能解决什么问题效率问题手动翻阅几十场比赛、上千道题目效率极低。本方法可自动化完成数据聚合。深度问题超越个人主观感受通过“通过率”、“标签关联”、“分数变化”等数据指标进行量化分析。趋势问题观察上半年题目难度整体走向、热门算法标签的变迁为预测下半年趋势提供参考。2.3 不适合什么场景实时数据分析依赖历史数据抓取不适合需要秒级实时信息的场景。替代官方API对于简单的题目查询应优先使用 Codeforces 官方 API。商业用途未经许可不得将大量抓取的数据用于商业产品。2.4 合规与安全边界必须严格遵守尊重robots.txt检查目标网站是否允许爬虫访问相应路径。限制请求频率在请求间添加合理延时如 1-3 秒避免对 Codeforces 服务器造成压力。仅抓取公开数据禁止尝试抓取用户私信、非公开提交代码等隐私信息。缓存数据对已抓取的数据进行本地缓存避免重复请求。注明数据来源在生成的分析报告中明确标注数据来源于 Codeforces。3. 环境准备与前置条件在开始编写抓取和分析脚本前需要准备好本地开发环境。3.1 基础软件环境操作系统Windows 10/11, macOS, 或 Linux 发行版均可。Python 版本推荐 Python 3.8 及以上版本。确保python和pip命令可用。网络环境需要能够正常访问codeforces.com域名。3.2 Python 依赖包我们将使用以下核心库请通过pip安装# 基础请求与解析 pip install requests beautifulsoup4 lxml # 数据处理与分析 pip install pandas numpy # 数据可视化 (选择其一或都安装) pip install matplotlib seaborn plotly # 可选用于需要渲染JavaScript的复杂页面抓取 # pip install selenium webdriver-manager3.3 项目目录结构建议创建一个清晰的项目目录便于管理代码、数据和输出结果。codeforces_analysis/ ├── src/ # 源代码目录 │ ├── crawler.py # 抓取模块 │ ├── analyzer.py # 分析模块 │ └── visualizer.py # 可视化模块 ├── data/ # 数据存储目录 │ ├── raw/ # 原始抓取数据 (JSON/HTML) │ └── processed/ # 处理后的结构化数据 (CSV) ├── output/ # 输出目录 │ ├── charts/ # 生成的图表 │ └── report.md # 分析报告 ├── config.py # 配置文件 (如请求头、延时设置) └── main.py # 主程序入口4. 抓取策略与核心实现“从夯到拉锐评”的基础是数据。我们首先需要制定一个高效、稳健的抓取策略。4.1 理解数据源Codeforces API vs. 网页抓取Codeforces 提供了官方的 API这是最推荐的首选方式。优点稳定、结构化、无需解析HTML、官方支持。缺点某些详细数据如题目正文可能不包含或需要组合多个API。核心API接口示例获取比赛列表https://codeforces.com/api/contest.list获取比赛题目https://codeforces.com/api/contest.standings?contestIdCONTEST_IDfrom1count1获取题目信息https://codeforces.com/api/problemset.problems当 API 无法满足需求时例如需要抓取题目描述原文才考虑使用网页抓取。4.2 抓取模块设计 (crawler.py)一个健壮的抓取模块应包含以下功能# config.py import time from typing import Dict, Any import json BASE_URL https://codeforces.com/api HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } REQUEST_DELAY 2 # 秒关键避免请求过快 def safe_request(url: str, params: Dict None) - Dict[str, Any]: 带错误处理和延时的安全请求函数 import requests from requests.exceptions import RequestException time.sleep(REQUEST_DELAY) try: resp requests.get(url, headersHEADERS, paramsparams, timeout30) resp.raise_for_status() return resp.json() except RequestException as e: print(f请求失败: {url}, 错误: {e}) return {status: FAILED, comment: str(e)} except json.JSONDecodeError: print(fJSON解析失败: {url}) return {status: FAILED, comment: Invalid JSON}# src/crawler.py import os import json from datetime import datetime from config import BASE_URL, safe_request class CodeforcesCrawler: def __init__(self, data_dir: str ../data/raw): self.data_dir data_dir os.makedirs(self.data_dir, exist_okTrue) def fetch_contests(self, gymFalse): 获取比赛列表。gymFalse 只获取常规比赛。 url f{BASE_URL}/contest.list data safe_request(url) if data.get(status) OK: contests data[result] # 过滤出2024年上半年示例的常规赛 target_contests [] for c in contests: # 示例筛选2024年1月1日至6月30日的常规赛 start_time datetime.fromtimestamp(c[startTimeSeconds]) if start_time.year 2024 and 1 start_time.month 6: if not gym and c[type] CF and c[phase] FINISHED: target_contests.append(c) self._save_to_file(target_contests, contests_2024_h1.json) return target_contests return [] def fetch_problems_for_contest(self, contest_id: int): 获取指定比赛的所有题目 # 方法1使用 problemset.problems API 并过滤推荐 # 方法2解析 contest 页面当需要题目描述时 # 这里演示方法2的简化思路 from bs4 import BeautifulSoup import requests contest_url fhttps://codeforces.com/contest/{contest_id} time.sleep(2) try: resp requests.get(contest_url, headersHEADERS, timeout30) soup BeautifulSoup(resp.content, lxml) # 解析题目表格这里需要根据实际页面结构调整 problem_rows soup.select(table.problems tr)[1:] # 跳过表头 problems [] for row in problem_rows: cols row.select(td) if len(cols) 4: problem { contestId: contest_id, index: cols[0].text.strip(), name: cols[1].text.strip(), link: fhttps://codeforces.com{cols[1].find(a)[href]}, } problems.append(problem) return problems except Exception as e: print(f抓取比赛 {contest_id} 题目失败: {e}) return [] def _save_to_file(self, data, filename): 保存数据到JSON文件 filepath os.path.join(self.data_dir, filename) with open(filepath, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f数据已保存至: {filepath})4.3 执行抓取任务在主程序中协调抓取流程重点是分批进行和异常恢复。# main.py (抓取部分) from src.crawler import CodeforcesCrawler import time def main(): crawler CodeforcesCrawler() print(步骤1: 获取2024年上半年常规比赛列表...) contests crawler.fetch_contests(gymFalse) print(f找到 {len(contests)} 场比赛。) print(\n步骤2: 遍历比赛抓取题目信息...) all_problems [] for idx, contest in enumerate(contests): print(f处理比赛 [{idx1}/{len(contests)}]: {contest[name]} (ID: {contest[id]})) problems crawler.fetch_problems_for_contest(contest[id]) all_problems.extend(problems) # 每处理完一场比赛可以间歇性保存一次防止程序中断丢失所有数据 if (idx 1) % 5 0: crawler._save_to_file(all_problems, fproblems_batch_{(idx1)//5}.json) time.sleep(5) # 批量处理后的额外休息 crawler._save_to_file(all_problems, all_problems_2024_h1.json) print(基础数据抓取完成) if __name__ __main__: main()5. 数据分析与“锐评”指标构建数据抓取完成后进入核心的“锐评”环节。我们需要定义一系列量化指标。5.1 数据清洗与整合 (analyzer.py)首先将抓取的原始 JSON 数据加载并转换为结构化的 DataFrame。# src/analyzer.py import pandas as pd import json from pathlib import Path class DataAnalyzer: def __init__(self, raw_data_path: str): self.raw_data_path Path(raw_data_path) self.df_contests None self.df_problems None def load_data(self): 加载比赛和题目数据 contests_file self.raw_data_path / contests_2024_h1.json problems_file self.raw_data_path / all_problems_2024_h1.json with open(contests_file, r, encodingutf-8) as f: contests_data json.load(f) self.df_contests pd.DataFrame(contests_data) with open(problems_file, r, encodingutf-8) as f: problems_data json.load(f) self.df_problems pd.DataFrame(problems_data) # 将比赛信息合并到题目表中 if self.df_contests is not None and self.df_problems is not None: # 假设题目数据中有 contestId 字段 self.df_problems self.df_problems.merge( self.df_contests[[id, name, startTimeSeconds]], left_oncontestId, right_onid, howleft ) self.df_problems.rename(columns{name: contestName}, inplaceTrue) print(f加载完成: {len(self.df_contests)} 场比赛, {len(self.df_problems)} 道题目)5.2 定义“锐评”核心指标接下来我们计算一系列分析指标。这些指标是“锐评”的基石。def calculate_basic_metrics(self): 计算基础统计指标 if self.df_problems is None: self.load_data() # 1. 总体统计 total_contests self.df_contests[id].nunique() total_problems len(self.df_problems) avg_problems_per_contest total_problems / total_contests # 2. 难度分布假设从API获取了 rating 字段这里用模拟数据 # 实际中rating可能来自 problemset.problems API print(f总比赛场次: {total_contests}) print(f总题目数量: {total_problems}) print(f场均题目数: {avg_problems_per_contest:.2f}) # 3. 标签分析如果数据中包含标签 tags # 将所有标签展开并统计频率 if tags in self.df_problems.columns: all_tags [] for tags in self.df_problems[tags].dropna(): all_tags.extend(tags) from collections import Counter tag_counter Counter(all_tags) top_tags tag_counter.most_common(10) print(\n高频算法标签TOP10:) for tag, count in top_tags: print(f {tag}: {count} 次) return { total_contests: total_contests, total_problems: total_problems, avg_problems_per_contest: avg_problems_per_contest, top_tags: top_tags if tags in self.df_problems.columns else [] } def analyze_difficulty_trend(self): 分析难度随时间的变化趋势 if self.df_problems is None: self.load_data() # 假设我们有一个 rating 字段代表题目难度分数 if rating not in self.df_problems.columns: print(警告: 数据中未找到难度分数(rating)无法进行趋势分析。) # 此处可以尝试从其他渠道获取或估算 rating return None # 按比赛开始时间排序 self.df_problems[startTime] pd.to_datetime(self.df_problems[startTimeSeconds], units) self.df_problems.sort_values(startTime, inplaceTrue) # 按周或按月聚合平均难度 self.df_problems[year_month] self.df_problems[startTime].dt.to_period(M) monthly_avg_rating self.df_problems.groupby(year_month)[rating].mean().reset_index() monthly_avg_rating[year_month] monthly_avg_rating[year_month].astype(str) return monthly_avg_rating6. 可视化与报告生成数据只有被看见才能产生洞察。我们将分析结果转化为图表和报告。6.1 生成核心图表 (visualizer.py)使用 matplotlib 或 seaborn 创建直观的图表。# src/visualizer.py import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import os from pathlib import Path class ResultVisualizer: def __init__(self, output_dir: str ../output/charts): self.output_dir Path(output_dir) os.makedirs(self.output_dir, exist_okTrue) sns.set_style(whitegrid) plt.rcParams[font.sans-serif] [SimHei, DejaVu Sans] # 解决中文显示问题 plt.rcParams[axes.unicode_minus] False def plot_tag_distribution(self, tag_counter, top_n15): 绘制算法标签分布柱状图 tags, counts zip(*tag_counter.most_common(top_n)) plt.figure(figsize(12, 6)) bars plt.barh(tags[::-1], counts[::-1]) # 反转使最高的在最上面 plt.xlabel(出现次数) plt.title(fCodeforces 2024上半年常规赛题目标签分布 (TOP{top_n})) # 在柱子上添加数字 for bar in bars: width bar.get_width() plt.text(width 0.5, bar.get_y() bar.get_height()/2, f{int(width)}, haleft, vacenter) plt.tight_layout() save_path self.output_dir / tag_distribution.png plt.savefig(save_path, dpi300) plt.close() print(f标签分布图已保存: {save_path}) def plot_difficulty_trend(self, monthly_avg_rating_df): 绘制难度分数月度变化趋势图 if monthly_avg_rating_df is None or monthly_avg_rating_df.empty: return plt.figure(figsize(14, 6)) plt.plot(monthly_avg_rating_df[year_month], monthly_avg_rating_df[rating], markero, linewidth2, markersize8) plt.xlabel(月份) plt.ylabel(平均难度分数 (Rating)) plt.title(Codeforces 2024上半年常规赛题目平均难度月度趋势) plt.xticks(rotation45) plt.grid(True, linestyle--, alpha0.7) # 为每个点添加数值标签 for x, y in zip(monthly_avg_rating_df[year_month], monthly_avg_rating_df[rating]): plt.text(x, y, f{y:.0f}, hacenter, vabottom) plt.tight_layout() save_path self.output_dir / difficulty_trend.png plt.savefig(save_path, dpi300) plt.close() print(f难度趋势图已保存: {save_path}) def plot_problems_per_contest(self, df_contests): 绘制每场比赛的题目数量分布 # 需要数据中包含每场比赛的题目数这里假设已处理 # 示例假设 df_contests 有 problem_count 列 if problem_count not in df_contests.columns: print(数据中无题目数量信息跳过此图表。) return plt.figure(figsize(10, 6)) plt.hist(df_contests[problem_count], binsrange(5, 15), edgecolorblack, alpha0.7) plt.xlabel(单场比赛题目数量) plt.ylabel(场次) plt.title(Codeforces 2024上半年常规赛单场题目数量分布) plt.grid(axisy, alpha0.75) plt.tight_layout() save_path self.output_dir / problems_per_contest_hist.png plt.savefig(save_path, dpi300) plt.close() print(f题目数量分布图已保存: {save_path})6.2 生成文本分析报告将分析结果整合成一份结构化的 Markdown 报告。# src/report_generator.py (可合并到 visualizer.py) from datetime import datetime def generate_markdown_report(metrics: dict, top_tags, output_path: str): 生成Markdown格式的分析报告 report_content f# Codeforces 2024上半年常规轮次分析报告 生成时间{datetime.now().strftime(%Y-%m-%d %H:%M:%S)} 数据来源Codeforces API 及公开页面 ## 一、总体概览 - **分析时间范围**2024年1月1日 - 2024年6月30日 - **总比赛场次**{metrics.get(total_contests, N/A)} - **总题目数量**{metrics.get(total_problems, N/A)} - **场均题目数量**{metrics.get(avg_problems_per_contest, N/A):.2f} ## 二、核心发现 ### 1. 高频算法标签 上半年常规赛中最常出现的算法/数据结构标签如下 | 排名 | 标签 | 出现次数 | |------|------|----------| for i, (tag, count) in enumerate(top_tags[:10], 1): report_content f| {i} | {tag} | {count} |\n report_content **解读**动态规划(DP)、贪心(greedy)、数学(math) 依然是绝对核心。二分查找(binary search) 和 构造算法(constructive algorithms) 也保持了很高的出现频率。这提示参赛者在备赛时应优先巩固这些领域。 ### 2. 题目难度趋势 ![难度月度趋势图](./charts/difficulty_trend.png) 此处结合图表描述趋势例如整体难度是否平稳在哪个时间段出现了明显的难度峰值或谷值这与某种特定比赛系列有关吗 ### 3. 比赛结构观察 ![单场题目数量分布](./charts/problems_per_contest_hist.png) 最常见的题目数量是 **X** 道。大部分比赛集中在 **Y-Z** 道题的范围内。 ## 三、典型比赛“锐评”样例 选取几场有代表性的比赛进行简要点评 ### 1. Codeforces Round #XXX (Div. 2) - **整体评价**从难度梯度、题目新颖度、区分度等方面简述 - **最难题**Problem F涉及 [算法标签]通过率仅 ~5%。 - **签到题**Problem A典型的 [算法标签] 应用通过率 80%。 - **学习建议**该场次重点考察了 [某个知识点]值得反复练习。 可根据实际数据补充更多场次 ## 四、给参赛者的建议 1. **基础巩固**基于高频标签分析务必掌握动态规划、贪心、数学、二分查找和构造题的基本套路。 2. **训练节奏**关注月度难度趋势在难度较高的月份可以针对性进行更多虚拟比赛VP以适应压力。 3. **题目选择**备赛时优先选择标签集中度高的历史题目进行专题训练。 4. **模拟实战**选择题目数量分布最集中的比赛模式如 6-7 题进行限时训练模拟真实比赛节奏。 ## 五、技术说明 - 本报告数据通过 Codeforces 官方 API 及公开页面获取并进行了去重与清洗。 - 分析代码已开源可在此处添加仓库链接。 - 数据抓取遵守了 robots.txt 并设置了请求间隔以减轻服务器负担。 --- *报告结束* with open(output_path, w, encodingutf-8) as f: f.write(report_content) print(f分析报告已生成: {output_path})7. 完整工作流集成与执行现在我们将所有模块串联起来形成一个完整的分析流水线。# main.py (完整版) from src.crawler import CodeforcesCrawler from src.analyzer import DataAnalyzer from src.visualizer import ResultVisualizer from src.report_generator import generate_markdown_report import time def full_analysis_pipeline(): print( Codeforces 2024上半年常规轮次分析流水线启动 \n) # 阶段一数据抓取 print(阶段1: 数据抓取) crawler CodeforcesCrawler(data_dir./data/raw) contests crawler.fetch_contests(gymFalse) print(f 已获取 {len(contests)} 场常规比赛列表。) # 注意实际抓取所有题目可能耗时较长建议分步执行或使用缓存数据 # all_problems [] # for contest in contests: # all_problems.extend(crawler.fetch_problems_for_contest(contest[id])) # crawler._save_to_file(all_problems, all_problems_2024_h1.json) print( 提示完整题目抓取已注释如需执行请取消注释并耐心等待。\n) # 阶段二数据分析 (假设使用已抓取好的数据) print(阶段2: 数据分析) analyzer DataAnalyzer(raw_data_path./data/raw) analyzer.load_data() basic_metrics analyzer.calculate_basic_metrics() difficulty_trend analyzer.analyze_difficulty_trend() print( 基础指标计算完成。\n) # 阶段三可视化 print(阶段3: 可视化生成) visualizer ResultVisualizer(output_dir./output/charts) # 假设我们有 tag_counter 数据这里用示例 from collections import Counter # 示例标签数据实际应从 analyzer.df_problems 计算 example_tags [greedy, math, dp, binary search, greedy, math, constructive algorithms] tag_counter Counter(example_tags) visualizer.plot_tag_distribution(tag_counter, top_n10) visualizer.plot_difficulty_trend(difficulty_trend) # visualizer.plot_problems_per_contest(analyzer.df_contests) # 需要数据支持 print( 图表生成完成。\n) # 阶段四报告生成 print(阶段4: 生成分析报告) generate_markdown_report( metricsbasic_metrics, top_tagstag_counter.most_common(10), output_path./output/report.md ) print(\n 分析流水线执行完毕 ) print(请查看 ./output/ 目录下的图表和报告文件。) if __name__ __main__: # 执行完整流水线 full_analysis_pipeline()8. 常见问题与排查方法在实施本项目过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案请求 API 返回FAILED或连接超时1. 网络问题2. Codeforces API 暂时不可用3. 请求频率过高被限制1. 检查网络连接2. 访问https://codeforces.com/api/contest.list看是否正常3. 查看返回的comment字段1. 修复网络2. 等待一段时间再试3. 增加REQUEST_DELAY(如 3-5秒)抓取题目详情时解析 HTML 失败1. 网页结构发生变化2. 使用的 CSS 选择器过时1. 手动打开比赛页面检查元素2. 打印抓取的 HTML 片段1. 更新BeautifulSoup解析逻辑2. 使用更稳健的父级元素选择pandas合并数据时出错1. 列名不匹配2. 数据中存在空值或格式不一致1. 打印 DataFrame 的列名和数据类型2. 检查contestId等关键字段1. 统一列名和数据类型2. 使用pd.isna()处理空值生成图表时中文乱码系统缺少中文字体检查plt.rcParams[font.sans-serif]设置1. 安装中文字体 (如SimHei)2. 或使用DejaVu Sans等支持中文的字体程序运行一段时间后中断1. 请求过多被临时封 IP2. 内存不足3. 程序异常未捕获1. 查看控制台错误信息2. 监控系统资源1. 大幅增加请求间隔 (如 10秒)2. 分批次抓取并保存中间结果3. 使用try...except包裹关键代码块分析结果与预期偏差大1. 数据清洗不彻底包含非目标比赛2. 指标计算逻辑有误1. 检查过滤条件 (如phase,type, 时间范围)2. 手动验证几场比赛的统计结果1. 复核数据加载和过滤代码2. 用小型数据集 (如2场比赛) 进行单元测试9. 最佳实践与扩展建议9.1 工程化建议使用缓存首次抓取后将数据保存为本地文件。后续分析直接读取本地文件避免重复请求。增量更新设计脚本只抓取新比赛而非每次全量抓取。可以通过记录已处理的最新contestId来实现。配置化将时间范围、请求延迟、输出目录等参数放入配置文件 (config.py或config.yaml)。日志记录使用 Pythonlogging模块替代print便于记录运行状态和排查问题。错误恢复实现断点续抓功能。如果程序中断可以从最后一个成功的contestId继续。9.2 分析维度扩展当前的“锐评”主要集中在宏观统计。你可以进一步深化题目文本分析使用 NLP 技术如关键词提取、简单的情感分析分析题目描述看描述长度、复杂度与难度的关系。提交数据挖掘通过 API 获取题目的提交统计如提交总数、通过率更精确地衡量题目实际难度。选手表现关联分析特定 Rating 区间的选手在不同标签题目上的通过率找出他们的共性弱点。比赛预测模型基于历史数据尝试预测下一场比赛可能出现的标签或难度范围。9.3 合规与伦理重申永远优先使用官方 API。严格遵守robots.txt。在发起大量请求前务必检查https://codeforces.com/robots.txt。明确数据用途。本项目生成的分析报告应用于个人学习、技术分享或学术研究切勿用于任何可能干扰平台正常运营或侵犯用户隐私的用途。分享时注明来源。在任何公开场合分享基于本方法得出的结论或图表请注明数据来源于 Codeforces。通过这套从数据抓取、清洗、分析到可视化的完整流程你不仅能得到一份对 Codeforces 上半年比赛的“锐评”报告更能掌握一套处理类似公开平台数据分析的通用方法论。下次当你需要对其他竞赛平台或开源项目进行技术分析时这套工具链的思路完全可以复用。