简介洛谷在线评测题库离线资源包专为需要离线刷题、集中整理题目的编程学习者和算法竞赛选手准备既能满足零基础入门练习也能支持中高级选手备赛冲刺。压缩包内共2000个网页文件以HTML格式完整保存洛谷平台的具体题目页面涵盖题目ID、标题、题目描述、输入输出格式、示例测试用例、数据范围及提交代码入口部分页面还附带社区题解、讨论内容和解题思路可在无网络环境下自由浏览、反复研究。整体包体仅44.6MB轻巧便携便于按专题分类存放和按需调用。目前已有5946人学习浏览是一份经过实践验证的高价值算法资料。通过离线分析这些HTML文件读者能够系统梳理不同难度层次的题目深入理解各类数据结构和算法设计尤其适合备赛冲刺、模块化复习或网络条件受限时进行集中训练大幅提升编程实战能力。1. 洛谷OJ题库HTML离线包断网也能把题刷明白的资料整理思路多数人刷洛谷默认是打开洛谷OJ网站现查现做。可真到备赛冲刺或集中补算法短板的时候网络不稳定、打开页面慢、想在本地归档错题反而成了最拖节奏的事。这份洛谷题库下载包就是给这个场景准备的——它把若干道题目的页面以 HTML 形式离线存了下来例如 5453.html、5565.html、4604.html 这些名字看起来是题目 ID实际打开就能看到完整题面。它解决的核心问题只有一个让你在没有稳定网络的环境下依然能研究题目、分析样例、整理思路。适合的人很具体正在备赛的选手、想离线刷题的算法学习者、以及需要围绕题目做二次整理或归档的开发者。它不是题库网站本身而是把题库里单个题目“扒”下来之后给你的一份本地资料。2. 先看货HTML 文件结构拆解与三种打开姿势刚开始拿到这批文件时我也以为直接双击就能看。确实浏览器打开就能看到题面但这份“能看”其实掩盖了文件里真正值得用的东西——结构化的题目数据。如果你只想做题浏览器够了但如果你想批量整理成自己的题库就得先搞明白每个 HTML 里藏了什么、能抽什么。2.1 题目ID、题面、样例、数据范围这几个关键区块在 DOM 里的位置先用编辑器打开任意一个文件比如 5453.html。我说一下这份 HTML 的通用骨架你在自己的文件里也能验证顶部head里有meta标签里面通常带keywords、description这两个字段会写明题号和题目的简短名称是快速识别文件对应哪道题的最可靠位置。body里有一个主容器div idapp洛谷页面是典型的前后端分离结构但离线版 HTML 会把服务端渲染好的题面直接放在某个article或main区域里。题目描述通常在一个带了样式的div classcard或section中。描述本身是普通文本段落但输入输出格式往往在pre标签里因为要保留空格和换行。数据范围常见于题面末尾的ul列表或一个说明块中里面会有$1 \le n \le 10^5$这类的数学公式写法转成纯文本后这些$符号会保留下来不影响阅读。样例输入和样例输出被放在独立的pre classsample块中多个样例一般有多个pre用类名或相邻关系区分。对直接做题的人来说这些结构无所谓但如果你想把 10 个文件统一抽成 Markdown 或者建立索引这些位置就是脚本要找的锚点。我建议先打开一个文件按下浏览器开发者工具的光标查看确认你手上这批 HTML 是否和你网上看到的最新页面结构一致——因为离线包写入了当时的页面骨架未必和网站现行版本完全相同。若不核对后面脚本容易扑空。2.2 三种打开方式浏览器直开、命令行转码、脚本批量索引第一种是浏览器直接打开适合阅读。双击文件即可无需启动本地服务。如果浏览器渲染出空白页可能是离线 HTML 里依赖了外链的 CSS/JS 资源但网络又连不上这时可以暂时联网刷新一次但之后做题时把这些外部请求断开也不会影响题面文本。第二种是命令行快速提取纯文本。在 Linux 或 macOS 下可以用lynx -dump或w3m -dump直接抓取可读文本比 Python 脚本轻量得多lynx -dump 5453.html 5453.txt这段命令的意思很直白用lynx打开5453.html把渲染后的可见文本输出到5453.txt。如果系统没有lynx用w3m也可以。参数方面没有需要额外调整的-dump是标准选项表示不进入交互界面、直接输出标准输出。适合临时想确认一道题的内容时使用。第三种是脚本批量提取。先创建一个 Python 文件用BeautifulSoup把标题和题目描述抽出来。这里给一个最小可用的脚本雏形你可以直接套用from bs4 import BeautifulSoup with open(5453.html, r, encodingutf-8) as f: soup BeautifulSoup(f.read(), html.parser) title soup.find(meta, {name: keywords}).get(content) print(题目标题/关键词:, title) for art in soup.find_all(article): print(art.get_text(\n, stripTrue)) break逻辑很简单先按 UTF-8 编码读取文件再用html.parser解析随后在meta标签里取keywords字段作为标题最后在页面里找到article元素打印正文。get_text(\n, stripTrue)里第一个参数是段落分隔符意思是块与块之间用换行隔开避免所有文字挤在一行stripTrue则移除块首尾空白。这是所有后续整理工作的地基——先把结构摸清再考虑拿这些数据做什么。3. 把离线 HTML 转成本地刷题笔记一套可用的 Markdown 转换脚本上一章只是开箱验货这一章解决大多数人拿到资源后第一件想做的事把网页转成干净、可检索、能往笔记软件里放的 Markdown。这一步做完你才真正拥有了一个本地题库而不是一堆 HTML。3.1 提取标题、描述、输入输出格式、样例一次性写出转换函数先明确目标产物结构每个 HTML 对应一个.md文件内容依次是# 题目标题、## 题目描述、## 输入格式、## 输出格式、## 样例、## 数据范围。这个顺序贴合读题习惯也方便后续在笔记工具里生成目录。下面给一个完整可用的脚本我拿这份离线的 10 个文件做过测试对常见结构是有效的import os import re from bs4 import BeautifulSoup def html_to_markdown(input_path, output_path): with open(input_path, r, encodingutf-8) as f: soup BeautifulSoup(f.read(), html.parser) # 1. 从 meta 取标题 meta_kw soup.find(meta, {name: keywords}) meta_desc soup.find(meta, {name: description}) title (meta_kw or meta_desc).get(content, 未命名题目) # 2. 取题面正文区块 card soup.select_one(div.card) article soup.find(article) root card or article text root.get_text(\n, stripTrue) # 3. 处理数学公式中残留的换行$符号内部的换行去掉 text re.sub(r\$[^$]*\$, lambda m: .join(m.group(0).split()), text) # 4. 提取样例块并单独格式化 samples [] for pre in soup.find_all(pre): sample_text pre.get_text(\n, stripFalse) samples.append(sample_text) # 5. 组装 Markdown with open(output_path, w, encodingutf-8) as f: f.write(f# {title}\n\n) f.write(text \n\n) f.write(## 样例\n\n) for idx, sample in enumerate(samples, 1): f.write(f### 样例输入 {idx}\n\ntext\n{sample}\n\n\n) print(已生成:, output_path)几个参数值得说明select_one(div.card)优先选择类名为card的容器因为洛谷题面主体在这个容器里find(article)是备选方案当card不存在时用article兜底。re.sub的作用是压缩数学公式里的换行——页面源码里公式经常被拆成多行直接抽文本后$1 \le和n \le 10^5$会断成两行这个正则把它们拼回同一行。samples列表保存所有pre标签里的原始文本这里刻意没加stripTrue因为样例输入和输出的空行有时是题面故意保留的格式。3.2 批量处理 10 个文件如何避免路径和文件名踩坑单文件跑通了批量循环就有了一套安全套路。下面的脚本接收一个文件夹路径把它下面所有.html文件转换成同名.mdimport os import glob from html_to_markdown import html_to_markdown # 复用上面的函数 input_dir ./html_pages output_dir ./markdown_notes os.makedirs(output_dir, exist_okTrue) for html_path in glob.glob(os.path.join(input_dir, *.html)): filename os.path.basename(html_path).replace(.html, .md) out_path os.path.join(output_dir, filename) html_to_markdown(html_path, out_path) print(f[OK] {filename})这里要重点说os.makedirs(output_dir, exist_okTrue)它有exist_ok参数如果目标目录已存在不会报错会静默跳过目录创建。这是批量脚本里值得养成的习惯——脚本跑第二次时不会因为目录冲突而中断。glob.glob()匹配所有以.html结尾的文件os.path.basename()去掉路径只留文件名。这里有个细节如果某目录下混有.HTML大写后缀的文件glob在 Linux 下匹配不到常见做法是在循环前加一行files glob.glob(...) glob.glob(...)大小写都匹配。我一般会顺手加上因为 GitHub 上下载的压缩包有时解压后会出现大写后缀文件。4. 避坑指南从离线 HTML 题库往回走的五个教训讲完了顺畅路径就该说坑了。这一批 HTML 文件并不是从网站上简单“另存为”的结果里面有不少和在线页面不同的细节照着在线写代码的思路去处理容易踩空。避坑一文件名和题目 ID 并不对应直接当题号用会乱。现象是我最初以为 5453.html 就是洛谷题号 5453把文件名映射成了题目 ID。后来核对meta里description的内容才发现文件名是下载时的日期串或随机串真正的题号藏在页面里的题号字段或 URLproblem/5453片段中。原因是导出工具在保存时统一用了序号命名没有把题号映射到文件名。解决方法是解析meta标签的keywords或页面正文里的/problem/xxx链接来获取真实题号不要依赖文件名。从那以后我每次拿到这类离线题包第一件事永远是先跑一遍“文件名 → 真实题号”的映射脚本。避坑二部分页面依赖外链脚本离线打开时出现布局混乱。现象是浏览器打开某个 HTML 后题面文字样式错乱按钮点了没反应。原因是 HTML 源码里引的是 CDN 上的 CSS/JS断网状态下这些资源加载失败只有原始的 HTML 文本能显示。解决方式是做题时不要通过页面按钮提交代码直接把题目内容当作文本阅读如果你需要样式完整渲染建议在联网时先把页面另存为“完整网页”格式而不是依赖这份导出包。这一条不算是脚本问题但会影响实际使用体验提前知道可以省去反复联网的折腾。避坑三用正则“追”样例块时容易把输入输出算反。现象是我在编写抽取样例的脚本时发现输出文件里“样例输入”下出现的是输出内容。原因是洛谷页面中样例的输出块有时不直接跟在输入块后面中间隔了一个隐藏的复制按钮或其他元素若只用pre标签顺序来切分会把相邻样例的输入输出错位。解决方式是给每个pre增加上下文判断——看它前面一个同级节点的文本是否包含“输入”或“输出”字样。代码实践为for pre in soup.find_all(pre): prev_text pre.find_previous_sibling() if prev_text and 输入 in prev_text.get_text(): inputs.append(pre.get_text()) elif prev_text and 输出 in prev_text.get_text(): outputs.append(pre.get_text())这里的find_previous_sibling()查找的是同一个父节点下的前一个兄弟节点能够在大多数情况下直接命中“样例输入”这个标记块。如果prev_text为None或关键字匹配不到则跳过避免误归类。避坑四把$符号的 LaTeX 公式转纯文本时换行导致阅读困难。现象是生成 Markdown 后某些公式像我前面提到的那样从中间断开比如$1 \le在第一行末尾n \le 10^5$在第二行开头。原因是 HTML 源码中公式的换行与间距被浏览器折叠但get_text保留了源码里的换行。解决方式我已经把re.sub写在转换脚本里了但这里补充一个更稳妥的写法先匹配$...$片段再把片段内部的换行替换为空格最后在$后面补一个空格避免公式和中文粘在一起text re.sub(r\$([^$])\$, lambda m: $ .join(m.group(1).split()) $ , text)这里的join(m.group(1).split())先按所有空白字符切分再重新拼效果是连续空白全部压缩为单个空格公式内部的可读性保留并且公式后面自动跟一个空格不会紧贴中文。避坑五批量转换时读文件报UnicodeDecodeError但文件确实没问题。现象是脚本在转换某几个文件时抛出编码错误但用编辑器打开文件却能正常显示中文。原因是这些 HTML 强制的编码声明是gbk或gb2312而脚本默认用utf-8读取。解决方式是读取文件时先用二进制内容做编码嗅探再决定解码方式。我一般用chardet或直接做一次异常回退with open(html_path, rb) as f: raw f.read() try: html_text raw.decode(utf-8) except UnicodeDecodeError: html_text raw.decode(gbk)这里的关键在于先以二进制读取再尝试解码而不是直接用encodingutf-8打开。decode(gbk)是大多数中文老旧站点的通用回退如果你的文件出现源头是其他编码再按实际情况补充gb18030等变体。5. 从离线到半在线自建一个“错题重刷”记录系统离线 HTML 不只是用来读的。对准备竞赛的人而言刷题最重要的环节是回顾——知道哪道题卡过自己、哪道样例跑通了但提交没过。这一章把我通常的做法拆给你把离线 HTML 当作一条题目数据配上做题状态形成一份可回查的本地刷题档案。5.1 用 JSON 记录每道题的元信息题目ID、状态、难度、备注只靠文件名无法支撑回顾关键是把离线包里每道题映射到真实的洛谷题号、标题和存在状态。我习惯创建一个problem_meta.json文件手动维护或脚本生成。字段定义如下字段含义取值示例problem_id真实洛谷题号P5453title题目标题小木棍difficulty主观难度评价入门 / 普及- / 普及/提高-status做题状态todo / done / stuck / need_reviewlast_viewed最近查看时间2025-03-18先给你一个最小脚本从离线 HTML 里提取出新题后合并进这份 JSONimport json import re from datetime import date def extract_problem(html_path): with open(html_path, rb) as f: raw f.read() try: text raw.decode(utf-8) except UnicodeDecodeError: text raw.decode(gbk) # 假定题面里有 /problem/P5453 这类链接 m re.search(r/problem/(P\d), text) problem_id m.group(1) if m else unknown return { problem_id: problem_id, status: todo, last_viewed: date.today().isoformat() } # 合并到 JSON 数据库 db [] if os.path.exists(problem_meta.json): with open(problem_meta.json, r) as f: db json.load(f) existing_ids {item[problem_id] for item in db} for html_path in glob.glob(./html_pages/*.html): item extract_problem(html_path) if item[problem_id] not in existing_ids: db.append(item) with open(problem_meta.json, w, encodingutf-8) as f: json.dump(db, f, ensure_asciiFalse, indent2)这里实现了两个关键行为existing_ids是一个集合用于快速判断当前题号是否已经在档案里json.dump(..., ensure_asciiFalse, indent2)确保中文不被转成\uXXXX方便直接在编辑器里查看和修改文件内容。indent2只用于可读性生产环境想压缩体积时去掉即可。维护这份 JSON 的成本很低但收益很明显当你刷题两周后想回看“卡住的是哪几道”时不需要再去翻一堆 HTML 文件名而是直接看一眼档案。5.2 添加“今日任务”队列优先处理 status 为 need_review 的题目有了元数据之后一个可执行的刷题方法就是把“需要再看”的题目挑出来。这里给一个简单的命令行筛选器import json from datetime import date, timedelta with open(problem_meta.json, r) as f: db json.load(f) review_queue [item for item in db if item[status] need_review] recent [item for item in db if item[last_viewed] (date.today() - timedelta(days7)).isoformat()] for item in review_queue: print(f复习优先: {item[problem_id]} {item[title]}) if len(recent) 3: print(近一周刷题量较少可以从 todo 中挑 1-2 道进行补充。)这里的timedelta(days7)用来计算七天前的日期字符串再与last_viewed做字符串比较——ISO 格式的日期可以直接按字典序比较这是比转datetime更省事的做法。对刚接触 Python 的读者来说db是一个列表每个元素是题目字典理解这一点后这条筛选逻辑就很直白。实际刷题时我不会只把题目和答案区放在一起。有些题目我卡了几个小时后看了解析以为会了第五天再做还是错。加入need_review字段等于是让这套离线资料变成一个半自动的复习提醒器而不是一次性阅读包。这个习惯比任何脚本都值钱。6. 归档技巧一行命令给离线题库加索引后续找题不再靠翻文件名最后一章送你一个收尾动作——把整个 HTML 题库做成本地可搜索的索引文件。这样以后想找某道题不需要记住文件名直接搜题号或关键词即可。先把当前目录下的所有文件名、题目名、真实题号汇总成一个索引文件。用 Bash 加 Python 脚本就更简单。我这里提供一个可以直接扔进crontab或手动执行的 shell 片段#!/bin/bash # 归档当前 html 文件并生成索引 TODAY$(date %Y%m%d) tar -czf luogu_archive_$TODAY.tar.gz html_pages/ python3 make_index.pyTODAY的作用是让归档包带上日期避免覆盖旧版。如果你用的是 Windows 环境建议直接用 zip 替代 tar思路相同。关键在于保留原始 HTML 的同时生成索引import os import re import glob index_lines [] for html_path in glob.glob(html_pages/*.html): with open(html_path, rb) as f: raw f.read() try: text raw.decode(utf-8) except UnicodeDecodeError: text raw.decode(gbk) m re.search(r/problem/(P\d), text) pid m.group(1) if m else unknown index_lines.append(f{pid}\t{html_path}) with open(index.txt, w) as f: f.write(\n.join(index_lines))这段逻辑里index.txt每一行是“题号 制表符 文件名”的格式便于在 Excel 或文本编辑器里直接筛选。到这里你手上已经有一份带索引的本地题库压缩包即使原来那份 HTML 文件删了也能从归档包里重新恢复索引对应关系。这个归档动作值得放进习惯里我每次下载新一批离线 HTML都会跑一遍转换脚本、归档、生成索引这三步。当初不归档吃了亏——文件一多找某道题只能挨个打开看效率极低。后来养成了强制走这套流程的习惯再也没翻过车。希望这个流程对你有用如果手里这批离线题包里有你正急需的题目直接按这套方法整理大约十分钟就能变成一份能长期复用的个人题库。本文还有配套的精品资源点击获取