从0到1修复Library Checker测试用例解析:Competitive Companion核心问题修复指南
问题背景:你是否也遇到过这些解析失败?
在算法竞赛中,手动复制粘贴测试用例不仅浪费时间,还容易出错。Competitive Companion作为一款强大的浏览器扩展(Browser Extension),能够自动解析编程题目(Competitive Programming Problems)的测试用例,极大提升刷题效率。然而,许多用户反馈在解析Library Checker平台题目时遇到各种问题:测试用例缺失、格式错误、时间限制读取失败等。本文将深入剖析这些问题的根源,并提供完整的解决方案。
读完本文,你将获得:
- 理解测试用例解析的工作原理
- 掌握调试解析器的实用技巧
- 学会修复常见的解析器问题
- 了解如何贡献自己的修复代码
问题分析:为什么Library Checker解析会失败?
Library Checker平台特点
Library Checker(https://yosupo.jp/problem/*)是一个专注于算法库正确性验证的在线评测平台,其页面结构具有以下特点:
- 使用Material-UI框架构建,DOM结构复杂
- 动态加载内容,传统静态解析容易失效
- 测试用例以连续的
<pre>标签形式呈现 - 时间限制和内存限制信息位置固定但格式特殊
原解析器实现
我们先来看原LibraryCheckerProblemParser.ts的核心代码:
export class LibraryCheckerProblemParser extends Parser {
public getMatchPatterns(): string[] {
return ['https://yosupo.jp/problem/*'];
}
public async parse(url: string, html: string): Promise<Sendable> {
const elem = htmlToElement(html);
const task = new TaskBuilder('Library Checker').setUrl(url);
const container = elem.querySelector('.MuiContainer-root.MuiContainer-maxWidthLg');
task.setName(container.querySelector('.MuiTypography-h2').textContent);
const timeLimitStr = container.querySelector('.MuiTypography-body1').textContent.trim();
task.setTimeLimit(parseInt(/(\d+) sec/.exec(timeLimitStr)[1], 10) * 1000);
task.setMemoryLimit(1024);
container.querySelectorAll('pre + pre').forEach(outputBlock => {
const inputBlock = outputBlock.previousElementSibling;
task.addTest(inputBlock.textContent, outputBlock.textContent);
});
return task.build();
}
}
主要问题点
通过代码分析和实际测试,我们发现原解析器存在以下问题:
- 时间限制解析脆弱:使用
/(\d+) sec/正则表达式,但未处理匹配失败情况 - 测试用例选择不当:使用
pre + pre选择器可能匹配到非测试用例的<pre>标签 - 内存限制写死:硬编码为1024MB,未从页面读取实际值
- 错误处理缺失:没有任何try-catch块,一处出错导致整个解析失败
解决方案:分步骤修复解析器
1. 改进时间限制解析
原代码中时间限制解析存在严重缺陷,当正则表达式匹配失败时会抛出TypeError: Cannot read property '1' of null。我们需要添加错误处理:
// 原代码
const timeLimitStr = container.querySelector('.MuiTypography-body1').textContent.trim();
task.setTimeLimit(parseInt(/(\d+) sec/.exec(timeLimitStr)[1], 10) * 1000);
// 修复后
const timeLimitElem = container.querySelector('.MuiTypography-body1');
if (timeLimitElem) {
const timeLimitStr = timeLimitElem.textContent.trim();
const timeLimitMatch = /(\d+) sec/.exec(timeLimitStr);
if (timeLimitMatch && timeLimitMatch[1]) {
task.setTimeLimit(parseInt(timeLimitMatch[1], 10) * 1000);
} else {
task.setTimeLimit(2000); // 默认值
}
} else {
task.setTimeLimit(2000); // 默认值
}
2. 精准选择测试用例
原代码使用pre + pre选择器可能匹配到非测试用例的<pre>标签,我们需要更精确的选择:
// 原代码
container.querySelectorAll('pre + pre').forEach(outputBlock => {
const inputBlock = outputBlock.previousElementSibling;
task.addTest(inputBlock.textContent, outputBlock.textContent);
});
// 修复后
const preBlocks = container.querySelectorAll('pre');
// 确保有偶数个pre标签,且至少有2个(1组测试用例)
if (preBlocks.length >= 2 && preBlocks.length % 2 === 0) {
for (let i = 0; i < preBlocks.length; i += 2) {
const inputBlock = preBlocks[i];
const outputBlock = preBlocks[i + 1];
// 过滤掉可能为空的测试用例
if (inputBlock.textContent.trim() && outputBlock.textContent.trim()) {
task.addTest(inputBlock.textContent, outputBlock.textContent);
}
}
}
3. 动态读取内存限制
原代码将内存限制硬编码为1024MB,而实际上Library Checker页面显示了内存限制信息:
// 添加内存限制解析
const memoryLimitElem = container.querySelectorAll('.MuiTypography-body1')[1];
if (memoryLimitElem) {
const memoryLimitStr = memoryLimitElem.textContent.trim();
const memoryLimitMatch = /(\d+) MB/.exec(memoryLimitStr);
if (memoryLimitMatch && memoryLimitMatch[1]) {
task.setMemoryLimit(parseInt(memoryLimitMatch[1], 10));
} else {
task.setMemoryLimit(1024); // 默认值
}
} else {
task.setMemoryLimit(1024); // 默认值
}
4. 添加完整错误处理
为提高解析器健壮性,添加try-catch块和错误处理:
public async parse(url: string, html: string): Promise<Sendable> {
try {
const elem = htmlToElement(html);
const task = new TaskBuilder('Library Checker').setUrl(url);
const container = elem.querySelector('.MuiContainer-root.MuiContainer-maxWidthLg');
if (!container) {
throw new Error('Main container not found');
}
// ... 其他解析逻辑 ...
return task.build();
} catch (e) {
console.error('LibraryCheckerProblemParser error:', e);
// 返回一个包含错误信息的基本任务对象
return new TaskBuilder('Library Checker')
.setUrl(url)
.setName('解析失败')
.setTimeLimit(2000)
.setMemoryLimit(1024)
.build();
}
}
完整修复代码
整合以上修复,完整的LibraryCheckerProblemParser.ts代码如下:
import { Sendable } from '../../models/Sendable';
import { TaskBuilder } from '../../models/TaskBuilder';
import { htmlToElement } from '../../utils/dom';
import { Parser } from '../Parser';
export class LibraryCheckerProblemParser extends Parser {
public getMatchPatterns(): string[] {
return ['https://yosupo.jp/problem/*'];
}
public async parse(url: string, html: string): Promise<Sendable> {
try {
const elem = htmlToElement(html);
const task = new TaskBuilder('Library Checker').setUrl(url);
const container = elem.querySelector('.MuiContainer-root.MuiContainer-maxWidthLg');
if (!container) {
throw new Error('Main container not found');
}
// 解析题目名称
const titleElem = container.querySelector('.MuiTypography-h2');
if (titleElem) {
task.setName(titleElem.textContent);
} else {
task.setName('未知题目');
}
// 解析时间限制
const timeLimitElem = container.querySelector('.MuiTypography-body1');
if (timeLimitElem) {
const timeLimitStr = timeLimitElem.textContent.trim();
const timeLimitMatch = /(\d+) sec/.exec(timeLimitStr);
if (timeLimitMatch && timeLimitMatch[1]) {
task.setTimeLimit(parseInt(timeLimitMatch[1], 10) * 1000);
} else {
task.setTimeLimit(2000); // 默认值
}
} else {
task.setTimeLimit(2000); // 默认值
}
// 解析内存限制
const infoElements = container.querySelectorAll('.MuiTypography-body1');
if (infoElements.length >= 2) {
const memoryLimitStr = infoElements[1].textContent.trim();
const memoryLimitMatch = /(\d+) MB/.exec(memoryLimitStr);
if (memoryLimitMatch && memoryLimitMatch[1]) {
task.setMemoryLimit(parseInt(memoryLimitMatch[1], 10));
} else {
task.setMemoryLimit(1024); // 默认值
}
} else {
task.setMemoryLimit(1024); // 默认值
}
// 解析测试用例
const preBlocks = container.querySelectorAll('pre');
// 确保有偶数个pre标签,且至少有2个(1组测试用例)
if (preBlocks.length >= 2 && preBlocks.length % 2 === 0) {
for (let i = 0; i < preBlocks.length; i += 2) {
const inputBlock = preBlocks[i];
const outputBlock = preBlocks[i + 1];
// 过滤掉可能为空的测试用例
if (inputBlock.textContent.trim() && outputBlock.textContent.trim()) {
task.addTest(inputBlock.textContent, outputBlock.textContent);
}
}
}
return task.build();
} catch (e) {
console.error('LibraryCheckerProblemParser error:', e);
// 返回一个包含错误信息的基本任务对象
return new TaskBuilder('Library Checker')
.setUrl(url)
.setName('解析失败')
.setTimeLimit(2000)
.setMemoryLimit(1024)
.build();
}
}
}
测试验证:确保修复有效
测试用例设计
为验证修复效果,我们使用以下测试用例(tests/data/library-checker/problem/normal.json):
{
"url": "https://yosupo.jp/problem/aplusb",
"parser": "LibraryCheckerProblemParser",
"before": "beforeLibraryChecker",
"result": {
"name": "A + B",
"group": "Library Checker",
"url": "https://yosupo.jp/problem/aplusb",
"interactive": false,
"memoryLimit": 1024,
"timeLimit": 2000,
"tests": [
{
"input": "1234 5678\n",
"output": "6912\n"
},
{
"input": "1000000000 1000000000\n",
"output": "2000000000\n"
}
],
"testType": "single",
"input": {
"type": "stdin"
},
"output": {
"type": "stdout"
},
"languages": {
"java": {
"mainClass": "Main",
"taskClass": "AB"
}
}
}
}
测试结果对比
| 测试项 | 修复前 | 修复后 |
|---|---|---|
| 题目名称解析 | 正常 | 正常,添加错误处理 |
| 时间限制解析 | 正常,无错误处理 | 正常,添加默认值和错误处理 |
| 内存限制解析 | 固定1024MB | 从页面动态读取 |
| 测试用例解析 | 可能匹配错误 | 精准匹配,过滤空用例 |
| 异常处理 | 崩溃 | 返回默认任务对象 |
边缘情况测试
- 页面结构变化:模拟DOM结构变化,验证错误处理机制
- 网络异常:模拟HTML不完整情况,验证解析器稳定性
- 特殊题目:测试包含多个测试用例和特殊字符的题目
扩展知识:解析器开发最佳实践
1. DOM解析技巧
// 推荐的选择器使用方式
const container = elem.querySelector('.problem-container');
if (!container) return null;
// 使用data属性定位元素(如果可用)
const timeLimit = container.dataset.timeLimit;
// 避免过深的选择器嵌套
const title = container.querySelector('h1.title'); // 好
// const title = elem.querySelector('body > div > div > div > h1'); // 差
2. 错误处理策略
// 防御性编程
const element = container.querySelector('.some-element');
if (!element) {
console.warn('Element not found, using default value');
// 使用默认值或继续处理
}
// 类型检查
if (typeof someValue !== 'string') {
throw new Error(`Expected string, got ${typeof someValue}`);
}
3. 性能优化
对于大型页面或大量测试用例,考虑以下优化:
// 1. 使用DocumentFragment处理大量DOM操作
const fragment = document.createDocumentFragment();
// 2. 避免频繁的DOM查询,缓存结果
const allPre = container.querySelectorAll('pre');
// 3. 使用textContent而非innerHTML,避免XSS风险
const text = element.textContent;
贡献指南:如何提交你的修复
- Fork仓库:访问https://gitcode.com/gh_mirrors/co/competitive-companion,点击Fork按钮
- 创建分支:
git checkout -b fix/library-checker-parser - 提交修改:
git commit -m "Fix LibraryCheckerProblemParser issues" - 推送到远程:
git push origin fix/library-checker-parser - 创建Pull Request:在GitCode界面创建新的Pull Request
总结与展望
本文详细介绍了如何诊断和修复Competitive Companion中Library Checker测试用例解析问题。通过改进选择器、添加错误处理、动态读取配置等措施,显著提高了解析器的健壮性和准确性。
未来可以进一步优化的方向:
- 添加对动态加载内容的支持
- 实现更智能的测试用例识别算法
- 增加用户自定义解析规则的功能
Competitive Companion作为开源项目,欢迎各位开发者贡献自己的力量,一起打造更完善的算法竞赛辅助工具!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



