从0到1修复Library Checker测试用例解析:Competitive Companion核心问题修复指南

从0到1修复Library Checker测试用例解析:Competitive Companion核心问题修复指南

【免费下载链接】competitive-companion Browser extension which parses competitive programming problems 【免费下载链接】competitive-companion 项目地址: https://gitcode.com/gh_mirrors/co/competitive-companion

问题背景:你是否也遇到过这些解析失败?

在算法竞赛中,手动复制粘贴测试用例不仅浪费时间,还容易出错。Competitive Companion作为一款强大的浏览器扩展(Browser Extension),能够自动解析编程题目(Competitive Programming Problems)的测试用例,极大提升刷题效率。然而,许多用户反馈在解析Library Checker平台题目时遇到各种问题:测试用例缺失、格式错误、时间限制读取失败等。本文将深入剖析这些问题的根源,并提供完整的解决方案。

读完本文,你将获得:

  • 理解测试用例解析的工作原理
  • 掌握调试解析器的实用技巧
  • 学会修复常见的解析器问题
  • 了解如何贡献自己的修复代码

问题分析:为什么Library Checker解析会失败?

Library Checker平台特点

Library Checker(https://yosupo.jp/problem/*)是一个专注于算法库正确性验证的在线评测平台,其页面结构具有以下特点:

  1. 使用Material-UI框架构建,DOM结构复杂
  2. 动态加载内容,传统静态解析容易失效
  3. 测试用例以连续的<pre>标签形式呈现
  4. 时间限制和内存限制信息位置固定但格式特殊

原解析器实现

我们先来看原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();
  }
}

主要问题点

通过代码分析和实际测试,我们发现原解析器存在以下问题:

  1. 时间限制解析脆弱:使用/(\d+) sec/正则表达式,但未处理匹配失败情况
  2. 测试用例选择不当:使用pre + pre选择器可能匹配到非测试用例的<pre>标签
  3. 内存限制写死:硬编码为1024MB,未从页面读取实际值
  4. 错误处理缺失:没有任何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从页面动态读取
测试用例解析可能匹配错误精准匹配,过滤空用例
异常处理崩溃返回默认任务对象

边缘情况测试

  1. 页面结构变化:模拟DOM结构变化,验证错误处理机制
  2. 网络异常:模拟HTML不完整情况,验证解析器稳定性
  3. 特殊题目:测试包含多个测试用例和特殊字符的题目

扩展知识:解析器开发最佳实践

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;

贡献指南:如何提交你的修复

  1. Fork仓库:访问https://gitcode.com/gh_mirrors/co/competitive-companion,点击Fork按钮
  2. 创建分支git checkout -b fix/library-checker-parser
  3. 提交修改git commit -m "Fix LibraryCheckerProblemParser issues"
  4. 推送到远程git push origin fix/library-checker-parser
  5. 创建Pull Request:在GitCode界面创建新的Pull Request

总结与展望

本文详细介绍了如何诊断和修复Competitive Companion中Library Checker测试用例解析问题。通过改进选择器、添加错误处理、动态读取配置等措施,显著提高了解析器的健壮性和准确性。

未来可以进一步优化的方向:

  • 添加对动态加载内容的支持
  • 实现更智能的测试用例识别算法
  • 增加用户自定义解析规则的功能

Competitive Companion作为开源项目,欢迎各位开发者贡献自己的力量,一起打造更完善的算法竞赛辅助工具!

【免费下载链接】competitive-companion Browser extension which parses competitive programming problems 【免费下载链接】competitive-companion 项目地址: https://gitcode.com/gh_mirrors/co/competitive-companion

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值