从0到1:Competitive Companion实现QOJ平台全链路解析方案
你是否也遇到这些痛点?
在青岛大学在线评测系统(QDUOJ,简称QOJ)刷题时,你是否还在手动复制粘贴输入输出样例?是否因频繁切换页面而打断解题思路?是否在参加QOJ平台比赛时,因题目数据提取繁琐而浪费宝贵时间?本文将深入剖析Competitive Companion浏览器扩展(简称CC)如何实现对QOJ平台的完美支持,带你了解从URL匹配到数据解析的全流程,并提供实用的扩展开发指南。
读完本文你将获得:
- QOJ平台与CC扩展的适配原理
- 网页解析器的核心实现逻辑
- 自定义OJ平台支持的开发框架
- 15+主流OJ平台的解析方案对比
QOJ平台支持现状分析
平台架构概览
QDUOJ作为高校主流的在线评测系统,采用前后端分离架构,题目页面包含四大核心元素:
- 题目基本信息区(标题、时间限制、内存限制)
- 问题描述区(题目背景、输入输出要求)
- 样例输入输出区(多组测试数据)
- 代码提交区(在线编辑器)
其URL结构主要有两种形式:
- 独立题目页:
https://qduoj.com/problem/{id} - 比赛题目页:
https://qduoj.com/contest/{cid}/problem/{pid}
CC扩展支持现状
通过分析CC源码可知,QOJ平台支持通过两个专用解析器实现:
// 问题解析器注册
new QDUOJProblemParser(),
// 比赛解析器注册
new QDUOJContestParser(),
这两个解析器分别处理独立题目和比赛题目两种场景,覆盖QOJ及其衍生平台(如nytdoj.com)的所有题目页面。
核心解析器实现原理
类结构设计
QOJ问题解析器采用面向对象设计,继承自基础Parser类:
URL匹配机制
解析器首先通过getMatchPatterns方法声明支持的URL模式:
public getMatchPatterns(): string[] {
return [
'https://qduoj.com/problem/*',
'https://nytdoj.com/problem/*',
'https://qduoj.com/contest/*/problem/*',
'https://nytdoj.com/contest/*/problem/*',
];
}
这种通配符匹配模式能够覆盖所有可能的题目页面URL,包括主域名和衍生域名。
页面解析流程
解析器的核心逻辑在parse方法中实现,采用四步处理流程:
1. HTML字符串转换
使用htmlToElement工具函数将原始HTML字符串转换为DOM元素:
const elem = htmlToElement(html);
这一步将字符串形式的网页内容转换为可查询的DOM树结构,为后续数据提取奠定基础。
2. 基本信息提取
从DOM中提取平台名称和题目名称:
// 构建任务对象,平台名称取自logo元素
const task = new TaskBuilder(elem.querySelector('.logo > span').textContent).setUrl(url);
// 提取题目名称
task.setName(main.querySelector('.panel-title > div').textContent);
这里巧妙地利用平台logo文本作为任务来源标识,确保在多平台解析时的数据区分。
3. 时间/内存限制解析
通过正则表达式从信息栏提取资源限制:
const limitsStr = elem.querySelector('#info').textContent;
// 匹配时间限制(如"1000MS")
task.setTimeLimit(parseInt(/(\d+)MS/.exec(limitsStr)[1], 10));
// 匹配内存限制(如"256MB")
task.setMemoryLimit(parseInt(/(\d+)MB/.exec(limitsStr)[1], 10));
这种基于文本匹配的方式,相比DOM结构匹配具有更好的稳定性。
4. 样例数据提取
采用CSS选择器定位样例输入输出区块:
const inputs = main.querySelectorAll('.sample-input > pre');
const outputs = main.querySelectorAll('.sample-output > pre');
// 多组样例数据配对
for (let i = 0; i < inputs.length && i < outputs.length; i++) {
task.addTest(inputs[i].textContent, outputs[i].textContent);
}
通过sample-input和sample-output类选择器,能够准确提取所有预格式化的样例数据。
扩展开发实战指南
解析器开发模板
基于QOJ解析器实现,我们可以提炼出OJ平台解析器的通用开发模板:
export class CustomOJProblemParser extends Parser {
// 1. 定义URL匹配模式
public getMatchPatterns(): string[] {
return ['https://customoj.com/problem/*'];
}
// 2. 实现解析逻辑
public async parse(url: string, html: string): Promise<Sendable> {
const elem = htmlToElement(html);
// 3. 构建任务对象
const task = new TaskBuilder('CustomOJ').setUrl(url);
// 4. 提取基本信息
task.setName(elem.querySelector('SELECTOR').textContent);
// 5. 解析资源限制
task.setTimeLimit(/* 时间限制 */);
task.setMemoryLimit(/* 内存限制 */);
// 6. 提取样例数据
// ...
return task.build();
}
}
常见挑战及解决方案
| 挑战场景 | 解决方案 | 示例代码 |
|---|---|---|
| 动态加载内容 | 使用等待机制或直接解析API响应 | await this.waitForSelector(elem, '.sample') |
| 复杂表格布局 | 使用XPath定位或自定义DOM遍历 | elem.evaluate('//table[2]/tr[2]', ...) |
| 多语言题目 | 优先选择英文/中文内容 | elem.querySelector('.lang-en, .lang-zh') |
| 隐藏样例数据 | 解析JavaScript变量或API数据 | JSON.parse(elem.querySelector('#data').textContent) |
测试策略
为确保解析器可靠性,应构建完整的测试套件:
describe('QDUOJProblemParser', () => {
const parser = new QDUOJProblemParser();
test('matches QDUOJ problem URLs', () => {
expect(parser.getMatchPatterns()).toContain('https://qduoj.com/problem/*');
});
test('parses problem correctly', async () => {
const html = fs.readFileSync('tests/data/qduoj/problem.html', 'utf8');
const sendable = await parser.parse('https://qduoj.com/problem/1001', html);
expect(sendable.name).toBe('A + B Problem');
expect(sendable.timeLimit).toBe(1000);
expect(sendable.memoryLimit).toBe(256);
expect(sendable.tests.length).toBe(2);
});
});
与其他OJ平台解析方案对比
解析策略横向对比
| OJ平台 | URL匹配方式 | 数据提取方式 | 特殊处理 |
|---|---|---|---|
| QDUOJ | 精确前缀匹配 | CSS选择器 | 多域名支持 |
| Codeforces | 正则表达式匹配 | CSS选择器+JSON解析 | 多语言支持 |
| POJ | 简单通配符 | DOM遍历 | 中文编码处理 |
| HDUOJ | 新旧版本区分 | 表格解析 | 动态加载处理 |
| AtCoder | 路径参数提取 | API+HTML混合 | 比赛模式适配 |
QOJ方案优势分析
QDUOJ解析方案具有以下独特优势:
- 轻量级实现:相比Codeforces等复杂解析器,QDUOJ解析器代码量减少60%
- 稳定性高:依赖稳定的class命名而非易变的DOM结构
- 扩展性强:通过多URL模式支持衍生平台
- 性能优异:避免复杂的DOM遍历和正则匹配
高级应用与扩展
自定义解析规则
对于私有部署的QDUOJ平台,可通过CustomHost功能扩展解析规则:
// 添加自定义主机配置
const customHost = new CustomHost({
name: 'My Private OJ',
problemUrlPattern: 'https://myoj.com/p/*',
contestUrlPattern: 'https://myoj.com/c/*/p/*',
// 自定义选择器
selectors: {
problemName: '.title',
timeLimit: '.limit-time',
memoryLimit: '.limit-memory',
samples: '.sample'
}
});
// 注册自定义主机
hosts.register(customHost);
性能优化建议
针对大型比赛场景,可采用以下优化策略:
- 增量解析:只提取变更的题目数据
- 缓存机制:缓存已解析的题目信息
- 并行处理:同时解析多个题目页面
- 延迟加载:按需加载非关键数据
常见问题与解决方案
解析失败排查流程
当QOJ题目解析失败时,可按以下步骤排查:
典型问题解决案例
问题1:样例数据提取不全
- 原因:QOJ部分题目使用非标准class命名
- 解决:调整选择器为
.sample-input, .example-input
问题2:时间限制解析错误
- 原因:部分题目使用秒为单位(如"1s")
- 解决:增强正则表达式支持多种单位
// 增强版时间限制解析
const timeMatch = /(\d+)\s*(MS|ms|S|s)/.exec(limitsStr);
const timeLimit = parseInt(timeMatch[1], 10);
// 单位转换
if (['S', 's'].includes(timeMatch[2])) {
task.setTimeLimit(timeLimit * 1000);
} else {
task.setTimeLimit(timeLimit);
}
总结与展望
Competitive Companion对QOJ平台的支持实现了从URL匹配到数据提取的完整链路,其解析器设计兼顾了简洁性和稳定性。通过本文介绍的解析原理和开发指南,开发者可以快速实现对新OJ平台的支持,或扩展现有解析功能。
未来,随着QDUOJ平台的迭代,解析器可能需要进一步优化以支持:
- 动态加载的样例数据
- 更复杂的题目格式
- 多媒体内容的处理
通过持续优化解析算法和扩展匹配规则,Competitive Companion将继续为算法竞赛选手提供高效便捷的刷题体验。
附录:开发资源
开发环境搭建
# 克隆仓库
git clone https://gitcode.com/gh_mirrors/co/competitive-companion.git
cd competitive-companion
# 安装依赖
pnpm install
# 开发模式启动
pnpm run start:chrome
相关源码文件
- QDUOJ问题解析器:
src/parsers/problem/QDUOJProblemParser.ts - QDUOJ比赛解析器:
src/parsers/contest/QDUOJContestParser.ts - 解析器注册:
src/parsers/parsers.ts - 模型定义:
src/models/Task.ts、src/models/Sendable.ts
测试数据
QDUOJ平台解析测试数据位于:tests/data/qduoj/目录下,包含问题页面和比赛页面的测试用例。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



