我们教练要我们的提交记录表格,我就调教了会AI弄出来了这个。
支持洛谷、Codeforces、AtCoder 的提交记录爬取并导出为 Excel。
把代码导入篡改猴,点击网页左下角导出即可。
// ==UserScript==
// @name CP 提交记录导出器
// @namespace http://tampermonkey.net/
// @version 2.3
// @description 导出 AC 记录。
// @author Awatesolo
// @match https://www.luogu.com.cn/*
// @match https://codeforces.com/*
// @match https://mirror.codeforces.com/*
// @match https://atcoder.jp/*
// @require https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/xlsx.full.min.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// 1. 公共工具
// ==========================================
function createButton(text, color = '#3498db') {
const btn = document.createElement('button');
btn.innerText = text;
btn.style.position = 'fixed';
btn.style.bottom = '20px';
btn.style.left = '20px';
btn.style.zIndex = '999999';
btn.style.padding = '10px 15px';
btn.style.backgroundColor = color;
btn.style.color = 'white';
btn.style.border = 'none';
btn.style.borderRadius = '5px';
btn.style.cursor = 'pointer';
btn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
btn.style.fontWeight = 'bold';
btn.style.fontSize = '14px';
document.body.appendChild(btn);
return btn;
}
function exportToExcel(data, filename, sheetName = "Accepted Records") {
if (data.length === 0) {
alert('❌ 当前日期范围内没有找到 AC 记录!');
return;
}
const worksheet = XLSX.utils.json_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
XLSX.writeFile(workbook, filename);
alert(`✅ 成功导出 ${data.length} 条记录!\n文件已保存为: ${filename}`);
}
// 获取并解析日期
function getStartDate() {
const input = prompt("请输入最早爬取日期 (格式: YYYY-MM-DD)\n留空则爬取所有历史记录:", "2023-01-01");
if (!input) return 0; // 0 表示 1970年,即爬取所有
const timestamp = new Date(input).getTime() / 1000; // 转换为秒级时间戳
if (isNaN(timestamp)) {
alert("日期格式错误,将爬取所有记录。");
return 0;
}
return timestamp;
}
// ==========================================
// 2. 路由逻辑
// ==========================================
const host = window.location.hostname;
if (host.includes('luogu')) {
runLuogu();
} else if (host.includes('codeforces')) {
runCodeforces();
} else if (host.includes('atcoder')) {
runAtCoder();
}
// ==========================================
// 3. 洛谷 (Luogu) 逻辑
// ==========================================
function runLuogu() {
const btn = createButton('📥 导出洛谷 AC', '#e74c3c');
const difficultyMap = {
0: "暂无评定", 1: "入门", 2: "普及-", 3: "普及/提高-",
4: "普及+/提高", 5: "提高+/省选-", 6: "省选/NOI-", 7: "NOI/NOI+/CTSC"
};
btn.onclick = async () => {
// 1. 获取UID
let uid = null;
if (window._feInjection && window._feInjection.currentUser) {
uid = window._feInjection.currentUser.uid;
} else {
const userLink = document.querySelector('a[href^="/user/"]');
if (userLink) uid = userLink.href.match(/\/user\/(\d+)/)[1];
}
let inputUid = prompt("请输入要爬取的洛谷UID:", uid || "");
if (!inputUid) return;
uid = inputUid.trim();
// 2. 获取日期
const startTime = getStartDate();
let page = 1;
let rawRecords = [];
let keepRunning = true;
btn.innerText = '⏳ 爬取中...';
btn.disabled = true;
try {
// 3. 爬取数据 (带时间截断)
while (keepRunning) {
btn.innerText = `⏳ 第 ${page} 页...`;
const url = `https://www.luogu.com.cn/record/list?user=${uid}&page=${page}&_contentOnly=1`;
const response = await fetch(url);
const json = await response.json();
if (!json.currentData || !json.currentData.records.result.length) {
break;
}
const pageRecords = json.currentData.records.result;
// 洛谷每一页是从新到旧排序的
// 如果这一页最旧的一条记录 比 startTime 还早,说明下一页肯定不需要爬了
const lastRecordTime = pageRecords[pageRecords.length - 1].submitTime;
// 筛选符合日期的记录加入数组
const validRecords = pageRecords.filter(r => r.submitTime >= startTime);
rawRecords.push(...validRecords);
// 如果本页最后一条记录已经小于起始时间,停止翻页
if (lastRecordTime < startTime) {
console.log("已到达指定日期,停止爬取旧记录。");
keepRunning = false;
}
page++;
await new Promise(r => setTimeout(r, 800));
}
// 4. 处理数据
btn.innerText = '⏳ 处理数据...';
rawRecords.sort((a, b) => a.submitTime - b.submitTime);
const processedData = [];
const problemMap = {};
for (const rec of rawRecords) {
const pid = rec.problem.pid;
if (!problemMap[pid]) problemMap[pid] = { solved: false, attempts: 0 };
if (problemMap[pid].solved) continue;
problemMap[pid].attempts++;
if (rec.status === 12) {
problemMap[pid].solved = true;
const diffText = difficultyMap[rec.problem.difficulty] || "未知";
processedData.push({
'ID': rec.id,
'题目': `${rec.problem.pid} ${rec.problem.title}`,
'难度': diffText,
'尝试次数': problemMap[pid].attempts,
'分数': rec.score,
'语言': rec.language,
'时间': new Date(rec.submitTime * 1000).toLocaleString(),
'链接': `https://www.luogu.com.cn/record/${rec.id}`
});
}
}
exportToExcel(processedData, `Luogu_AC_${uid}.xlsx`);
} catch (err) {
console.error(err);
alert('❌ 发生错误');
} finally {
btn.innerText = '📥 导出洛谷 AC';
btn.disabled = false;
}
};
}
// ==========================================
// 4. Codeforces 逻辑
// ==========================================
function runCodeforces() {
const btn = createButton('📥 导出 CF AC', '#f39c12');
btn.onclick = async () => {
let handle = null;
const profileLink = document.querySelector('a[href^="/profile/"]');
if (profileLink) handle = profileLink.href.split('/').pop();
let inputHandle = prompt("请输入 Codeforces Handle:", handle || "");
if (!inputHandle) return;
handle = inputHandle.trim();
const startTime = getStartDate();
btn.innerText = '⏳ 获取中...';
btn.disabled = true;
try {
const url = `https://codeforces.com/api/user.status?handle=${handle}&from=1&count=100000`;
const response = await fetch(url);
const json = await response.json();
if (json.status !== 'OK') {
alert('API Error: ' + json.comment);
return;
}
// 过滤日期并排序 (旧 -> 新)
const rawRecords = json.result
.filter(r => r.creationTimeSeconds >= startTime)
.reverse();
const processedData = [];
const problemMap = {};
for (const rec of rawRecords) {
const pid = `${rec.problem.contestId}${rec.problem.index}`;
if (!problemMap[pid]) problemMap[pid] = { solved: false, attempts: 0 };
if (problemMap[pid].solved) continue;
problemMap[pid].attempts++;
if (rec.verdict === 'OK') {
problemMap[pid].solved = true;
const tags = rec.problem.tags ? rec.problem.tags.join(', ') : '';
processedData.push({
'ID': rec.id,
'题目': `${pid} - ${rec.problem.name}`,
'算法标签': tags,
'难度评分': rec.problem.rating || '',
'尝试次数': problemMap[pid].attempts,
'语言': rec.programmingLanguage,
'时间': new Date(rec.creationTimeSeconds * 1000).toLocaleString(),
'链接': `https://codeforces.com/contest/${rec.contestId}/submission/${rec.id}`
});
}
}
exportToExcel(processedData, `Codeforces_AC_${handle}.xlsx`);
} catch (e) {
console.error(e);
alert('❌ 网络错误');
} finally {
btn.innerText = '📥 导出 CF AC';
btn.disabled = false;
}
};
}
// ==========================================
// 5. AtCoder 逻辑
// ==========================================
function runAtCoder() {
const btn = createButton('📥 导出 AC AC', '#2c3e50');
btn.onclick = async () => {
let user = typeof userScreenName !== 'undefined' ? userScreenName : "";
const inputUser = prompt("请输入 AtCoder 用户名:", user);
if (!inputUser) return;
user = inputUser.trim();
const startTime = getStartDate();
btn.innerText = '⏳ 获取题目数据...';
btn.disabled = true;
try {
const problemsUrl = 'https://kenkoooo.com/atcoder/resources/problem-models.json';
const problemsResp = await fetch(problemsUrl);
const problemModels = await problemsResp.json();
btn.innerText = '⏳ 获取提交记录...';
const url = `https://kenkoooo.com/atcoder/atcoder-api/v3/user/submissions?user=${user}&from_second=${startTime}`;
const response = await fetch(url);
if (response.status !== 200) throw new Error("API Failed");
let rawRecords = await response.json();
// Kenkoooo API 可以直接用 from_second 参数过滤,但为了保险,前端再 filter 一次
rawRecords = rawRecords.filter(r => r.epoch_second >= startTime);
rawRecords.sort((a, b) => a.epoch_second - b.epoch_second);
const processedData = [];
const problemMap = {};
for (const rec of rawRecords) {
const pid = rec.problem_id;
if (!problemMap[pid]) problemMap[pid] = { solved: false, attempts: 0 };
if (problemMap[pid].solved) continue;
problemMap[pid].attempts++;
if (rec.result === 'AC') {
problemMap[pid].solved = true;
let difficulty = '-';
if (problemModels[pid] && problemModels[pid].difficulty !== undefined) {
difficulty = problemModels[pid].difficulty;
}
processedData.push({
'ID': rec.id,
'题目ID': rec.problem_id,
'比赛ID': rec.contest_id,
'难度分': difficulty,
'尝试次数': problemMap[pid].attempts,
'语言': rec.language,
'时间': new Date(rec.epoch_second * 1000).toLocaleString(),
'链接': `https://atcoder.jp/contests/${rec.contest_id}/submissions/${rec.id}`
});
}
}
exportToExcel(processedData, `AtCoder_AC_${user}.xlsx`);
} catch (e) {
console.error(e);
alert('❌ 获取失败');
} finally {
btn.innerText = '📥 导出 AC AC';
btn.disabled = false;
}
};
}
})();
严肃批判!
你这难度不对呀!
0: "暂无评定", 1: "入门", 2: "普及-", 3: "普及/提高-",
洛谷香香软软的青题呢?
![[qq:发怒]](https://awate.top/usr/themes/ShuFeiCat/assets/vendor/jquery-emoji/images/emoji/qq/%E5%8F%91%E6%80%92.gif)