From 0d91ec7c2a0b4166a61107f998196433bde148a4 Mon Sep 17 00:00:00 2001 From: siknet <32444042+siknet@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:10:41 +0800 Subject: [PATCH] =?UTF-8?q?1.4=20=E5=8A=A0=E5=85=A5=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=BB=93=E6=9E=9C=E5=92=8C=E9=AB=98=E6=B8=85?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 32 +++ .gitignore | 2 + FreePEP 📚 人教社中小学电子教材批量下载器.md | 209 ---------------- FreePEP.spec | 51 ++++ README.md | 35 ++- __pycache__/cli.cpython-311.pyc | Bin 0 -> 18583 bytes __pycache__/download_all.cpython-311.pyc | Bin 0 -> 15318 bytes __pycache__/pep_core.cpython-311.pyc | Bin 0 -> 36211 bytes __pycache__/webui.cpython-311.pyc | Bin 0 -> 38679 bytes cli.py | 18 +- download_all.py | 49 ++-- pep_core.py | 38 ++- requirements-dev.txt | 2 + .../test_all.cpython-311-pytest-9.1.1.pyc | Bin 0 -> 32552 bytes .../test_all.cpython-39-pytest-8.4.2.pyc | Bin 0 -> 12780 bytes tests/test_all.py | 234 ++++++++++++++++++ webui.py | 43 ++-- 17 files changed, 457 insertions(+), 256 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore delete mode 100644 FreePEP 📚 人教社中小学电子教材批量下载器.md create mode 100644 FreePEP.spec create mode 100644 __pycache__/cli.cpython-311.pyc create mode 100644 __pycache__/download_all.cpython-311.pyc create mode 100644 __pycache__/pep_core.cpython-311.pyc create mode 100644 __pycache__/webui.cpython-311.pyc create mode 100644 requirements-dev.txt create mode 100644 tests/__pycache__/test_all.cpython-311-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_all.cpython-39-pytest-8.4.2.pyc create mode 100644 tests/test_all.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..acecae0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run pytest + run: | + pytest -v tests/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2247d5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build +/dist diff --git a/FreePEP 📚 人教社中小学电子教材批量下载器.md b/FreePEP 📚 人教社中小学电子教材批量下载器.md deleted file mode 100644 index 909486b..0000000 --- a/FreePEP 📚 人教社中小学电子教材批量下载器.md +++ /dev/null @@ -1,209 +0,0 @@ -# FreePEP 📚 人教社中小学电子教材批量下载器 - -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -**FreePEP** 是一款专为[人民教育出版社中小学电子教材平台](https://jc.pep.com.cn/)开发的自动化教材解析、批量抓取与高清 PDF 合成工具。 - -提供**WebUI 界面**与**交互式命令行**,内置全量 780+ 本教材目录(数据截止到2026年08月31日)自动解密引擎与阿里云 WAF 滑块验证码自动破解机制,支持一键下载指定学段、学科、年级的全套教材并自动生成高清 PDF 文件。 - -**更新内容:** - -2026.09.04 FreePEP v1.1 命令行加入了批量下载功能,详情见**启动办法三** - - ---- - -## ✨ 核心特性 - -- 🎯 **双操作模式**: - - **现代化 WebUI 界面**:全响应式布局,还原官网层级筛选体验,支持复选框一键批量下载、实时进度条展示与本地保存目录快捷打开。 - - **交互式 CLI 终端**:支持数字菜单导航选择与命令行参数直达(脚本集成与服务器环境友好)。 - - 📦 **高清单页下载与 PDF 合成**:自动探测各教材实际总页数,批量下载高分辨率原始 JPG,并通过 Pillow 库自动合成为标准 PDF 文件。 - - ---- - -## 📸 界面预览 - -### WebUI 网页端 - -![](https://img.wemd.app/1788139668274_4n5d28.png) - ---- - -### CLI 网页端 - -![](https://img.wemd.app/1788139734008_2tmuak.png) - - -# 🚀 使用方式 - -## 🖥️ 方式一:(推荐,适合小白) - -直接下载发行包,解压缩后运行FreePEP.exe,在弹出的Web页面里面自行操作下载。 - -## 方式二:从源码启动 - -### 克隆仓库与安装依赖 - -```bash -# 克隆仓库 -git clone https://github.com/siknet/FreePEP.git -cd FreePEP - -# 安装 Python 依赖 -pip install -r requirements.txt - -# 安装 Playwright 所需的 Chromium 浏览器内核 -playwright install chromium -``` - -### 启动办法一:Webui - -在终端中执行以下命令,系统将自动启动本地服务并在默认浏览器中打开管理页面: - -```bash -python webui.py -``` - -- **访问地址**:`http://127.0.0.1:8000` -- **默认下载目录**:项目根目录下的 `./downloads` 文件夹(可在 Web 界面点击「📁 打开保存目录」直达)。 - ---- - -### 启动办法二:使用 CLI 命令行 - -#### 1. 交互式菜单模式 - -直接运行 `cli.py`,根据控制台提示逐步选择学段、学科与年级: - -```bash -python cli.py -``` - -#### 2. 参数直达模式(适合自动化与脚本调用) - -通过命令行参数直接指定条件过滤并自动开始下载: - -```bash -# 1. 下载小学一年级的所有学科教材 -python cli.py --xd "小学" --nj "一年级" -y - -# 2. 下载高中数学的所有必修/选修教材 -python cli.py --xd "高中" --xk "数学" -y - -# 3. 下载初中全部教材 -python cli.py --xd "初中" -y - -# 4. 全局关键词搜索并下载(如包含"物理"的所有教材) -python cli.py --search "物理" - -# 5. 清理本地临时图片缓存 -python cli.py --clear-cache - -# 6. 自定义 PDF 输出路径 -python cli.py --xd "小学(六三学制)" --xk "语文" --nj "一年级" -o "D:/Textbooks" -y -``` - -**参数说明**: - -| 参数 | 说明 | 示例 | -| :--- | :--- | :--- | -| `--xd` | 指定学段 | `小学(六三学制)`、`初中(六三学制)`、`小学(五四学制)`、`初中(五·四学制)`、`高中`、`培智学校`、`聋校`、`盲校(盲文版)`、`盲校(低视力版)` | -| `--xk` | 指定学科 | `语文`、`数学`、`英语`、`物理`、`化学`、`历史`、`道德与法治` 等 | -| `--nj` | 指定年级/册次 | `一年级`、`二年级` ... `九年级`、`专项`、`必修` 等 | -| `--search`, `-s` | 关键词全局搜索 | `必修`、`高一`、`地理` | -| `--clear-cache`, `-c` | 清理本地下载临时图片缓存 (`temp_pages`) | 无需参数 | -| `--refresh`, `-r` | 强制重新从官方服务器拉取解密最新目录 | 无需参数 | -| `--output`, `-o` | 指定 PDF 保存目录 | 默认: `./downloads` | -| `--yes`, `-y` | 跳过确认提示直接开始下载 | 开启免交互 | - -### 启动办法三:按「学段 ➔ 年级」层级批量多线程下载 (download_all.py) - -如果您希望将教材按照 **`学段/年级/`** 两层规范文件夹分类归档下载,直接运行专属的高速批量多线程下载脚本: - -```bash -# 1. 一键下载全网全量教材(默认 3 线程并发,按「学段/年级」两层子目录自动归档) -python download_all.py - -# 2. 启用多线程并发加速(例如 10 线程并行高速下载) -python download_all.py -w 10 - -# 3. 下载多个指定学段(支持逗号分隔,如:义务教育六三学制、五四学制与高中)并自定义保存路径 -python download_all.py --xd "义务教育(六三学制),义务教育(五四学制),高中" -w 10 -o "D:/人教社教材" - -# 4. 仅下载单个学段(如高中全部教材) -python download_all.py --xd "高中" - -# 5. 仅下载某个指定年级教材 -python download_all.py --nj "一年级" -``` - -**功能特性**: - -- 🚀 **多线程并发提速**:默认 **3 线程**并发同时下载,支持通过 `-w / --workers` 自定义并发线程数(如 5~10 线程),下载效率大幅提升。 -- 🎯 **支持多学段组合**:`--xd` 支持传入多个学段(逗号分隔),精准下载普通义务教育或高中学段,自动跳过特教教材。 -- 🌲 **两层层级目录**:自动分类保存为 `downloads/<学段>/<年级>/<教材名>.pdf`,告别成百上千文件堆在单目录。 -- ⚡ **智能断点续传**:已完整下载的教材**自动秒跳过**,中途随时中断无缝继续,绝不重复下载。 -- 🧹 **切片自动清理**:每本教材合成 PDF 后自动清理临时图片分片,极大保护硬盘空间。 -- 💬 **清爽紧凑输出**:不打印繁琐单页过程,仅在教材下载合并完成或跳过时输出进度与提示。 - ---- - -## 📦 一键打包为 Windows 独立 EXE 发行版 - -如果您想将本项目打包成 **脱离 Python 环境** 的独立绿色软件包发布给普通用户,只需执行工作区自带的一键打包脚本: - -```bash -python build_exe.py -``` - -### 打包脚本会自动完成以下操作: - -1. 自动检查并安装 `PyInstaller` 编译工具。 -2. 将 WebUI 及所有 Python 依赖打包为独立可执行文件 `FreePEP.exe`。 -3. **自动提取并内嵌绿色便携版 Chromium 浏览器内核**至 `browsers/` 目录。 -4. 自动在 `dist/` 目录下生成 `FreePEP-Windows-x64.zip` 发行压缩包。 - -> **分发给用户使用**:用户下载压缩包后解压,**双击 `FreePEP.exe` 即可直接使用**(会自动弹出系统默认浏览器打开 WebUI,无需安装 Python、无需配置环境变量、无需额外下载浏览器内核)。 - ---- - -## 📁 项目目录结构 - -```text -FreePEP/ -├── pep_core.py # 核心底层库(AES 解密、Playwright 爬取、PDF 合成) -├── webui.py # FastAPI WebUI 服务器与一体化前端界面 -├── cli.py # 交互式与参数化 CLI 终端下载器 -├── download_all.py # 按「学段/年级」层级全量下载脚本(支持断点续传与缓存清理) -├── build_exe.py # 一键打包发布 Windows EXE 独立便携包脚本 -├── pep_crawler.py # 命令行测试与示例下载脚本 -├── pep_catalog.json # 全量教材元数据本地缓存(自动生成) -├── requirements.txt # 项目 Python 依赖清单 -├── README.md # 项目使用说明文档 -├── temp_pages/ # 图片下载临时缓存目录(下载后自动清理/保留) -└── downloads/ # 生成的高清 PDF 默认存放目录 -``` - ---- - -## 🔍 技术原理解析 - -1. 略。我是不明白为什么免费教材免费提供阅读不提供免费下载。 - ---- - -## ⚠️ 免责声明 (Disclaimer) - -1. 本项目仅供 Python 爬虫技术交流、逆向工程学习与个人学习研究使用,严禁用于任何商业用途或盈利活动。 -2. 本项目下载的所有教材版权均归**人民教育出版社(PEP)**及相关版权所有方所有。 -3. 使用本项目时请控制请求频率,严禁进行任何可能对官方服务器造成过大负载的行为。请于下载后 24 小时内自行删除,如需长期使用请购买或支持官方正版出版物。 -4. 使用者因违反版权或不当使用造成的一切法律纠纷与责任,均由使用者个人自行承担,与本项目作者无关。 - ---- - -## 📄 开源许可 - -本项目基于 [MIT License](LICENSE) 协议开源。欢迎提交 Issue 与 Pull Request! - diff --git a/FreePEP.spec b/FreePEP.spec new file mode 100644 index 0000000..68819dc --- /dev/null +++ b/FreePEP.spec @@ -0,0 +1,51 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_all + +datas = [] +binaries = [] +hiddenimports = ['uvicorn.logging', 'uvicorn.loops', 'uvicorn.loops.auto', 'uvicorn.protocols', 'uvicorn.protocols.http', 'uvicorn.protocols.http.auto', 'uvicorn.protocols.websockets', 'uvicorn.protocols.websockets.auto', 'uvicorn.lifespans', 'uvicorn.lifespans.on'] +tmp_ret = collect_all('playwright') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] + + +a = Analysis( + ['webui.py'], + pathex=[], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='FreePEP', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='FreePEP', +) diff --git a/README.md b/README.md index 428e31a..2959c72 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,27 @@ --- +# 2026/09/19 FreePEP 1.4 +### 🚀 功能新增与改进 (Features & Improvements) + 1. **支持下载原图高清版本 (Large Resolution)**: + - 核心下载器支持抓取 PEP 官方 `large` 目录高清原图(分辨率提升至 2174×3071),并自带 404 自动回退机制; + - WebUI 新增“下载高清原图版本”快捷勾选项,CLI 新增 `--high-res / --hd` 命令行参数与交互选择。 + 2. **文档补充**: + - README 新增常见问题排查(FAQ),针对 macOS 环境提示补装 `playwright install chromium`。 + ### 🐛 Bug 修复与代码重构 (Bug Fixes & Refactoring) + 1. **空值异常修复**:修复 `cli.py` 与 `webui.py` 中因元数据字段 `nj: null` 导致的 `AttributeError: 'NoneType' + object has no attribute 'strip'` 隐蔽崩溃。 + 2. **架构重构**:将 `download_all.py` 内部嵌套的 `is_match_xd` 函数提取为模块顶层函数,提升可复用性与可测试性。 + + ### 🧪 自动化测试与 CI 护栏 (Testing & CI) + 1. **分层单元测试**:新建 `tests/test_all.py`(32 个测试用例全部通过),涵盖纯函数映射、排序权重、学段过滤、AES- + 128-CBC 加解密 Round-trip、下载器离线快路径与 FastAPI WebUI 接口测试。 + 2. **开发依赖分离**:新增 `requirements-dev.txt`(`pytest`、`httpx`),避免污染生产/打包依赖。 + 3. **持续集成配置**:新增 GitHub Actions CI 工作流 (`.github/workflows/ci.yml`),在 Python 3.9~3.12 + 矩阵下自动运行自动化测试。 + + ## ✨ 核心特性 - 🎯 **双操作模式**: @@ -104,6 +124,7 @@ python cli.py --xd "小学(六三学制)" --xk "语文" --nj "一年级" -o | `--xk` | 指定学科 | `语文`、`数学`、`英语`、`物理`、`化学`、`历史`、`道德与法治` 等 | | `--nj` | 指定年级/册次 | `一年级`、`二年级` ... `九年级`、`专项`、`必修` 等 | | `--search`, `-s` | 关键词全局搜索 | `必修`、`高一`、`地理` | +| `--high-res`, `--hd` | 下载高清原图版本 (`large`),默认普通版本 (`mobile`) | 无需参数 | | `--clear-cache`, `-c` | 清理本地下载临时图片缓存 (`temp_pages`) | 无需参数 | | `--refresh`, `-r` | 强制重新从官方服务器拉取解密最新目录 | 无需参数 | | `--output`, `-o` | 指定 PDF 保存目录 | 默认: `./downloads` | @@ -177,9 +198,19 @@ FreePEP/ --- -## 🔍 技术原理解析 +## ❓ 疑难解答 (FAQ) -1. 略。我是不明白为什么免费教材免费提供阅读不提供免费下载。 +### Q: macOS 用户使用 WebUI 可以在线阅读,但点击下载后显示“任务完成”,而 `downloads` 目录没有任何 PDF 文件? +**原因分析**: +* **“在线看”正常**:点击“在线阅读”是由您的本机浏览器直接打开人教社公开阅读页面,不依赖后台 Python 浏览器驱动。 +* **下载没有文件**:后台批量下载切片和过盾需要通过 Playwright 驱动无头 Chromium 浏览器。在 macOS 系统源码运行环境下,如果仅执行了 `pip install -r requirements.txt`,而**漏装了 Playwright 浏览器内核**,后台就会抛出 `Executable doesn't exist` 错误导致下载中断;而任务退出后界面可能误提示完成。 + +**解决方案**: +在 macOS 终端中激活当前 Python 环境,执行以下命令手动补全安装 Playwright 的 Chromium 内核: +```bash +playwright install chromium +``` +> **排查提示**:如仍有问题,请查看运行 `python webui.py` 的终端控制台窗口,观察是否有详细的异常报错输出(如网络超时或环境异常)。 --- diff --git a/__pycache__/cli.cpython-311.pyc b/__pycache__/cli.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b51e1520a7aa6ae6d5b4311809bf5174d6900a68 GIT binary patch literal 18583 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 literal 0 HcmV?d00001 diff --git a/__pycache__/download_all.cpython-311.pyc b/__pycache__/download_all.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a23988bd78042d2b0a0129b9a5cc9ce926195b98 GIT binary patch literal 15318 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 literal 0 HcmV?d00001 diff --git a/__pycache__/webui.cpython-311.pyc b/__pycache__/webui.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eef00989ecae2e3da39127ef21246518bb98b492 GIT binary patch literal 38679 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 IW;Edc0c>?^egFUf literal 0 HcmV?d00001 diff --git a/cli.py b/cli.py index 0a981ed..dbb4074 100644 --- a/cli.py +++ b/cli.py @@ -144,14 +144,17 @@ def interactive_mode(): print("[-] 未选择有效教材,退出。") return + res_choice = input("\n请选择画质 [1] 普通清晰度 (默认) [2] 高清大图 (large): ").strip() + is_high_res = (res_choice == "2") + out_dir = os.path.abspath("./downloads") - print(f"\n🚀 即将开始下载 {len(to_download)} 本教材,基础保存目录: {out_dir}") + print(f"\n🚀 即将开始下载 {len(to_download)} 本教材 ({'高清版本' if is_high_res else '普通版本'}),基础保存目录: {out_dir}") print(f"🌲 采用「学段 ➔ 年级」两层子目录分类保存 (已存在 PDF 自动跳过)") downloader = PepDownloader(headless=True, output_dir=out_dir) for idx, b in enumerate(to_download, 1): xd = b.get("xd", "其他学段") - nj = b.get("nj", "通用").strip() or "通用" + nj = (b.get("nj") or "通用").strip() or "通用" import re safe_xd = re.sub(r'[\/:*?"<>|]', '_', xd).strip() safe_nj = re.sub(r'[\/:*?"<>|]', '_', nj).strip() @@ -165,7 +168,8 @@ def interactive_mode(): custom_title=b.get("title"), sub_dir=sub_dir, skip_if_exists=True, - clean_temp=True + clean_temp=True, + high_res=is_high_res ) print("\n🎉 全部选定任务执行完毕!") @@ -198,16 +202,18 @@ def cli_args_mode(args): out_dir = os.path.abspath(args.output) downloader = PepDownloader(headless=True, output_dir=out_dir) use_tree = not args.flat + is_high_res = args.high_res print(f"\n📂 保存根目录: {out_dir}") print(f"🌲 目录结构: {'按「学段/年级」两层子目录' if use_tree else '全部平铺在根目录'}") + print(f"🖼️ 图像规格: {'高清大图模式 (large)' if is_high_res else '普通模式 (mobile)'}") for idx, b in enumerate(matched, 1): sub_dir = None if use_tree: import re safe_xd = re.sub(r'[\/:*?"<>|]', '_', b.get("xd", "其他学段")).strip() - safe_nj = re.sub(r'[\/:*?"<>|]', '_', b.get("nj", "通用") or "通用").strip() + safe_nj = re.sub(r'[\/:*?"<>|]', '_', (b.get("nj") or "通用").strip() or "通用").strip() sub_dir = os.path.join(safe_xd, safe_nj) print(f"\n[{idx}/{len(matched)}] 正在下载: 《{b.get('title')}》...") @@ -216,7 +222,8 @@ def cli_args_mode(args): custom_title=b.get("title"), sub_dir=sub_dir, skip_if_exists=True, - clean_temp=True + clean_temp=True, + high_res=is_high_res ) print(f"\n🎉 下载完成!文件已保存至: {out_dir}") @@ -229,6 +236,7 @@ def main(): parser.add_argument("--xk", help="指定学科(如:语文、数学、英语、物理等)") parser.add_argument("--nj", help="指定年级(如:一年级、七年级、必修等)") parser.add_argument("--search", "-s", help="全局搜索关键词") + parser.add_argument("--high-res", "--hd", action="store_true", help="下载高清大图版本 (large),默认普通版本 (mobile)") parser.add_argument("--output", "-o", default="./downloads", help="PDF 文件保存目录 (默认: ./downloads)") parser.add_argument("--flat", action="store_true", help="平铺存放在根目录下(默认自动按「学段/年级」两层子目录分类)") parser.add_argument("--yes", "-y", action="store_true", help="免确认直接开始下载") diff --git a/download_all.py b/download_all.py index 02eab9e..1c8bdae 100644 --- a/download_all.py +++ b/download_all.py @@ -29,6 +29,7 @@ import time import random import argparse import threading +from typing import List from concurrent.futures import ThreadPoolExecutor, as_completed from pep_core import PepCatalog, PepDownloader, normalize_xd, get_base_dir @@ -41,12 +42,30 @@ def sanitize_filename(name: str) -> str: return re.sub(r'[\/:*?"<>|]', '_', name).strip() +def is_match_xd(b_xd: str, b_xdtype: str, target_xds: List[str]) -> bool: + """判断教材学段是否与目标学段过滤条件匹配""" + if not target_xds: + return True + for t in target_xds: + # 精确匹配或关键词包含匹配(如匹配 六三、五四、高中) + if t == b_xd or t == b_xdtype: + return True + if ("六三" in t and "六三" in b_xdtype) or ("六三" in t and "六三" in b_xd): + return True + if ("五四" in t or "五·四" in t) and ("五四" in b_xdtype or "五·四" in b_xdtype or "五四" in b_xd or "五·四" in b_xd): + return True + if ("高中" in t) and ("高中" in b_xd or "高中" in b_xdtype): + return True + return False + + def run_download_all(output_dir: str = "./downloads", target_xd: str = None, target_nj: str = None, max_workers: int = 3, delay: float = 1.0, - clean_temp: bool = True): + clean_temp: bool = True, + high_res: bool = False): print("=" * 70) print(" 📚 人民教育出版社 (PEP) 全量教材多线程层级下载器") print("=" * 70) @@ -61,28 +80,13 @@ def run_download_all(output_dir: str = "./downloads", if target_xd and target_xd != "全部": target_xds = [x.strip() for x in re.split(r'[,,|/]', target_xd) if x.strip()] - def is_match_xd(b_xd, b_xdtype): - if not target_xds: - return True - for t in target_xds: - # 精确匹配或关键词包含匹配(如匹配 六三、五四、高中) - if t == b_xd or t == b_xdtype: - return True - if ("六三" in t and "六三" in b_xdtype) or ("六三" in t and "六三" in b_xd): - return True - if ("五四" in t or "五·四" in t) and ("五四" in b_xdtype or "五·四" in b_xdtype or "五四" in b_xd or "五·四" in b_xd): - return True - if ("高中" in t) and ("高中" in b_xd or "高中" in b_xdtype): - return True - return False - books_to_download = [] for b in all_books: xd = normalize_xd(b.get("xd", "其他学段")) xdtype = b.get("xdtype", "").strip() - nj = b.get("nj", "通用").strip() or "通用" + nj = (b.get("nj") or "通用").strip() or "通用" - if not is_match_xd(xd, xdtype): + if not is_match_xd(xd, xdtype, target_xds): continue if target_nj and target_nj != "全部" and nj != target_nj: continue @@ -130,7 +134,7 @@ def run_download_all(output_dir: str = "./downloads", else: stage_dir_name = xd - nj = b.get("nj", "通用").strip() or "通用" + nj = (b.get("nj") or "通用").strip() or "通用" safe_xd = sanitize_filename(stage_dir_name) safe_nj = sanitize_filename(nj) @@ -161,7 +165,8 @@ def run_download_all(output_dir: str = "./downloads", sub_dir=sub_dir, skip_if_exists=True, clean_temp=clean_temp, - quiet=True + quiet=True, + high_res=high_res ) with lock: @@ -215,6 +220,7 @@ def main(): parser.add_argument("--xd", help="只下载指定学段(如:小学(六三学制)、初中(六三学制)、高中 等)") parser.add_argument("--nj", help="只下载指定年级(如:一年级、七年级 等)") parser.add_argument("--delay", "-d", type=float, default=0.5, help="单线程任务间休眠秒数 (默认: 0.5 秒)") + parser.add_argument("--high-res", "--hd", action="store_true", help="下载高清大图版本 (large),默认普通版本 (mobile)") parser.add_argument("--keep-temp", action="store_true", help="保留单页切片图片缓存(默认会自动删除以节约空间)") args = parser.parse_args() @@ -225,7 +231,8 @@ def main(): target_nj=args.nj, max_workers=args.workers, delay=args.delay, - clean_temp=not args.keep_temp + clean_temp=not args.keep_temp, + high_res=args.high_res ) diff --git a/pep_core.py b/pep_core.py index 050e9d4..ea5caa5 100644 --- a/pep_core.py +++ b/pep_core.py @@ -391,7 +391,8 @@ class PepDownloader: log_cb: Optional[Callable[[str], None]] = None, skip_if_exists: bool = True, clean_temp: bool = True, - quiet: bool = False) -> Optional[str]: + quiet: bool = False, + high_res: bool = False) -> Optional[str]: """ 下载单本教材 :param book_id: 教材 ID(如 1284001101241) @@ -402,6 +403,7 @@ class PepDownloader: :param skip_if_exists: 若本地已存在完整 PDF 则自动跳过 :param clean_temp: 合成 PDF 后自动删除该书的切片图片以节约磁盘空间 :param quiet: 静默模式,不打印各页下载过程,仅在关键节点或报错时提示 + :param high_res: 是否下载高清版本图片 (large),默认普通版本 (mobile) :return: 生成的 PDF 绝对路径 """ target_dir = os.path.join(self.output_dir, sub_dir) if sub_dir else self.output_dir @@ -498,17 +500,20 @@ class PepDownloader: browser.close() return None + res_type = "large" if high_res else "mobile" if not quiet: - info_msg = f"[+] 教材: 《{safe_title}》 | 总页数: {total_pages} 页" + mode_str = "高清模式 (large)" if high_res else "普通模式 (mobile)" + info_msg = f"[+] 教材: 《{safe_title}》 | 规格: {mode_str} | 总页数: {total_pages} 页" if log_cb: log_cb(info_msg) else: print(info_msg) - temp_dir = os.path.join(get_base_dir(), "temp_pages", book_id) + temp_dir = os.path.join(get_base_dir(), "temp_pages", f"{book_id}_{res_type}") os.makedirs(temp_dir, exist_ok=True) image_files = [] for page_num in range(1, total_pages + 1): - img_url = f"https://book.pep.com.cn/{book_id}/files/mobile/{page_num}.jpg" + img_url = f"https://book.pep.com.cn/{book_id}/files/{res_type}/{page_num}.jpg" + fallback_url = f"https://book.pep.com.cn/{book_id}/files/mobile/{page_num}.jpg" if high_res else None img_path = os.path.join(temp_dir, f"{page_num}.jpg") # 本地已有合法 JPEG 则跳过 @@ -521,6 +526,8 @@ class PepDownloader: download_success = False for retry in range(4): + # 尝试下载图片(如果为高清模式且返回404等错误,自动回退到普通 mobile 版本) + current_fetch_url = img_url res = page.evaluate("""async (url) => { try { const resp = await fetch(url); @@ -538,7 +545,28 @@ class PepDownloader: } catch (e) { return { status: 500, error: e.toString() }; } - }""", img_url) + }""", current_fetch_url) + + # 如果高清版资源不存在(404),尝试回退到普通版 + if fallback_url and res.get("status") == 404: + res = page.evaluate("""async (url) => { + try { + const resp = await fetch(url); + const ctype = resp.headers.get('content-type') || ''; + const blob = await resp.blob(); + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve({ + status: resp.status, + ctype: ctype, + data: reader.result + }); + reader.readAsDataURL(blob); + }); + } catch (e) { + return { status: 500, error: e.toString() }; + } + }""", fallback_url) data_uri = res.get("data", "") if res.get("status") == 200 and data_uri and ("image" in res.get("ctype", "") or data_uri.startswith("data:image")): diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..5c79490 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest>=7.0.0 +httpx>=0.23.0 diff --git a/tests/__pycache__/test_all.cpython-311-pytest-9.1.1.pyc b/tests/__pycache__/test_all.cpython-311-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52f87c33b68207f0fff5866328cd2928a6ec7b61 GIT binary patch literal 32552 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 literal 0 HcmV?d00001 diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..8164b35 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,234 @@ +""" +单元测试集合 (tests/test_all.py) +涵盖: +1. 纯函数与排序、映射规则测试 (表驱动) +2. 学段匹配逻辑与空值安全性测试 (is_match_xd, nj None-safety) +3. 本地教材目录检索与结构化分类测试 (PepCatalog filter & structure) +4. AES-128-CBC 加解密链路 round-trip 与正则匹配测试 +5. PepDownloader skip_if_exists 离线快路径测试 +6. WebUI FastAPI API 接口测试 +""" + +import os +import sys +import json +import base64 +import binascii +import tempfile +import pytest +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from fastapi.testclient import TestClient + +# 将项目根目录加入 sys.path +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if BASE_DIR not in sys.path: + sys.path.insert(0, BASE_DIR) + +from pep_core import ( + normalize_xd, + sort_xd_key, + sort_xk_key, + sort_nj_key, + map_book_xd, + PepCatalog, + PepDownloader, + XD_ORDER, + XK_ORDER_PREFIX, + NJ_ORDER, +) +from download_all import sanitize_filename, is_match_xd +from webui import app + + +# ========================================== +# 1. 纯函数测试 +# ========================================== + +@pytest.mark.parametrize("raw, expected", [ + ("小学", "小学(六三学制)"), + ("初中", "初中(六三学制)"), + ("小学(五·四学制)", "小学(五四学制)"), + ("小学(五四学制)", "小学(五四学制)"), + ("初中(五·四学制)", "初中(五·四学制)"), + ("初中(五四学制)", "初中(五·四学制)"), + ("高中", "高中"), + ("", "其他"), + (None, "其他"), +]) +def test_normalize_xd(raw, expected): + assert normalize_xd(raw) == expected + + +def test_sort_keys(): + # 学段排序测试 + sorted_xds = sorted(XD_ORDER, key=sort_xd_key) + assert sorted_xds == XD_ORDER + # 未知学段排在后面 + assert sort_xd_key("未知学段")[0] == 1 + + # 学科前缀优先排序 + sorted_xks = sorted(XK_ORDER_PREFIX, key=sort_xk_key) + assert sorted_xks == XK_ORDER_PREFIX + assert sort_xk_key("未知学科")[0] == 1 + + # 年级排序 + sorted_njs = sorted(NJ_ORDER, key=sort_nj_key) + assert sorted_njs == NJ_ORDER + + +@pytest.mark.parametrize("meta, expected", [ + ({"xd": "小学", "xdtype": "盲文"}, "盲校(盲文版)"), + ({"xd": "初中", "xdtype": "低视力"}, "盲校(低视力版)"), + ({"xd": "小学", "xdtype": "聋校"}, "聋校"), + ({"xd": "小学", "xdtype": "培智"}, "培智学校"), + ({"xd": "初中", "xdtype": "六三"}, "初中(六三学制)"), + ({"xd": "小学", "xdtype": "六三"}, "小学(六三学制)"), + ({"xd": "初中", "xdtype": "五四"}, "初中(五·四学制)"), + ({"xd": "小学", "xdtype": "五四"}, "小学(五四学制)"), + ({"xd": "高中", "xdtype": ""}, "高中"), +]) +def test_map_book_xd(meta, expected): + assert map_book_xd(meta) == expected + + +def test_sanitize_filename(): + assert sanitize_filename('test/book:name*1') == "test_book_name_1" + assert sanitize_filename('test/book:name*1?"<>|') == "test_book_name_1_____" + + +# ========================================== +# 2. 学段匹配与空值安全 +# ========================================== + +@pytest.mark.parametrize("b_xd, b_xdtype, targets, expected", [ + ("小学(六三学制)", "六三学制", ["义务教育(六三学制)"], True), + ("初中(五·四学制)", "五四学制", ["义务教育(五四学制)"], True), + ("高中", "普通高中", ["高中"], True), + ("小学(六三学制)", "六三学制", ["高中"], False), + ("小学", "六三", [], True), # 无过滤条件全部匹配 +]) +def test_is_match_xd(b_xd, b_xdtype, targets, expected): + assert is_match_xd(b_xd, b_xdtype, targets) == expected + + +def test_nj_null_safety(): + """测试 nj 为 None 时的安全性""" + b = {"id": "123", "title": "测试教材", "xd": "小学", "nj": None} + nj = (b.get("nj") or "通用").strip() or "通用" + assert nj == "通用" + + +# ========================================== +# 3. 目录检索与结构化分类 (读取本地 pep_catalog.json) +# ========================================== + +def test_pep_catalog_filter_and_structure(): + books = PepCatalog.fetch_and_decrypt_all() + assert len(books) > 0, "应成功从本地缓存读取教材数据" + + # 测试条件过滤 + filtered = PepCatalog.filter_books(xd="高中", xk="数学") + assert len(filtered) > 0 + for b in filtered: + assert b["xd"] == "高中" + assert b["xk"] == "数学" + + # 测试关键词搜索 + kw_filtered = PepCatalog.filter_books(keyword="语文") + assert len(kw_filtered) > 0 + + # 测试结构体解析 + structure = PepCatalog.get_structure() + assert "高中" in structure + assert "数学" in structure["高中"]["subjects"] + + +# ========================================== +# 4. AES-128-CBC 加解密链路 round-trip 测试 +# ========================================== + +def test_aes_round_trip(): + key = PepCatalog.KEY + iv = PepCatalog.IV + + sample_data = {"data": [{"id": "999999", "title": "单元测试教材"}]} + plain_bytes = json.dumps(sample_data).encode("utf-8") + + # 模拟加密与 PKCS7 padding + padder = padding.PKCS7(128).padder() + padded_data = padder.update(plain_bytes) + padder.finalize() + + cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) + encryptor = cipher.encryptor() + cipher_bytes = encryptor.update(padded_data) + encryptor.finalize() + hex_str = binascii.hexlify(cipher_bytes).decode("ascii").upper() + + # 模拟从前端 JS 中匹配 hex_str + mock_js = f'var o, c = "{hex_str}";'.encode("ascii") + import re + m = re.search(rb'c\s*=\s*"([A-F0-9]+)"', mock_js) + assert m is not None + + extracted_hex = m.group(1).decode("ascii") + extracted_cipher = binascii.unhexlify(extracted_hex) + + # 执行解密 + decryptor = cipher.decryptor() + decrypted_padded = decryptor.update(extracted_cipher) + decryptor.finalize() + unpadder = padding.PKCS7(128).unpadder() + decrypted_plain = unpadder.update(decrypted_padded) + unpadder.finalize() + + res_json = json.loads(decrypted_plain.decode("utf-8")) + assert res_json == sample_data + + +# ========================================== +# 5. 下载器 skip_if_exists 离线快路径测试 +# ========================================== + +def test_downloader_skip_if_exists(): + with tempfile.TemporaryDirectory() as tmp_dir: + downloader = PepDownloader(headless=True, output_dir=tmp_dir) + fake_pdf = os.path.join(tmp_dir, "测试教材.pdf") + # 写入大于 50KB 的假文件模拟已存在 PDF + with open(fake_pdf, "wb") as f: + f.write(b"%PDF-1.4 " + b"0" * 60000) + + # 调用 download_book,由于文件已存在且 > 50KB,应直接秒退并返回路径,无需启动 Playwright 浏览器 + result_path = downloader.download_book( + book_id="1384001301261", + custom_title="测试教材", + skip_if_exists=True + ) + assert result_path == fake_pdf + + +# ========================================== +# 6. WebUI API 接口测试 +# ========================================== + +client = TestClient(app) + +def test_webui_api_structure(): + response = client.get("/api/structure") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, dict) + assert len(data) > 0 + + +def test_webui_api_books(): + response = client.post("/api/books", json={"xd": "高中", "xk": "数学", "nj": "全部", "keyword": ""}) + assert response.status_code == 200 + data = response.json() + assert "books" in data + assert data["total"] > 0 + + +def test_webui_api_status(): + response = client.get("/api/status") + assert response.status_code == 200 + data = response.json() + assert "is_running" in data + assert "queue_len" in data diff --git a/webui.py b/webui.py index 9b278de..49941fb 100644 --- a/webui.py +++ b/webui.py @@ -58,16 +58,19 @@ class TaskManager: "recent_logs": self.logs[-25:] } - def add_tasks(self, books: List[Dict]): + def add_tasks(self, books: List[Dict], high_res: bool = False): with self._lock: existing_ids = {b["id"] for b in self.queue} if self.current_book: existing_ids.add(self.current_book["id"]) for b in books: if b["id"] not in existing_ids: - self.queue.append(b) + item = dict(b) + item["_high_res"] = high_res + self.queue.append(item) existing_ids.add(b["id"]) - self.log(f"[*] 已添加 {len(books)} 本教材至下载队列。") + quality_str = "高清模式" if high_res else "普通模式" + self.log(f"[*] 已添加 {len(books)} 本教材至下载队列 ({quality_str})。") def run_worker(self): """后台单线程顺序执行下载队列中的教材""" @@ -104,14 +107,16 @@ class TaskManager: self.log(txt) try: + import re xd = normalize_xd(book_to_download.get("xd", "其他学段")) - nj = book_to_download.get("nj", "通用").strip() or "通用" + nj = (book_to_download.get("nj") or "通用").strip() or "通用" safe_xd = re.sub(r'[\/:*?"<>|]', '_', xd).strip() safe_nj = re.sub(r'[\/:*?"<>|]', '_', nj).strip() sub_dir = os.path.join(safe_xd, safe_nj) + is_high_res = book_to_download.get("_high_res", False) self.log(f"==================================================") - self.log(f"[*] 开始下载教材: [{safe_xd}/{safe_nj}] 《{book_to_download.get('title')}》") + self.log(f"[*] 开始下载教材: [{safe_xd}/{safe_nj}] 《{book_to_download.get('title')}》 ({'高清版' if is_high_res else '普通版'})") downloader.download_book( book_id=book_to_download["id"], custom_title=book_to_download.get("title"), @@ -119,7 +124,8 @@ class TaskManager: progress_cb=progress_callback, log_cb=log_callback, skip_if_exists=True, - clean_temp=True + clean_temp=True, + high_res=is_high_res ) except Exception as e: self.log(f"[-] 下载异常: {e}") @@ -143,6 +149,7 @@ class FilterQuery(BaseModel): class BatchDownloadRequest(BaseModel): book_ids: List[str] + high_res: Optional[bool] = False @app.get("/api/structure") @@ -175,7 +182,7 @@ def add_download(req: BatchDownloadRequest): all_books = {b["id"]: b for b in PepCatalog.fetch_and_decrypt_all()} selected = [all_books[bid] for bid in req.book_ids if bid in all_books] if selected: - task_manager.add_tasks(selected) + task_manager.add_tasks(selected, high_res=bool(req.high_res)) return {"status": "ok", "added_count": len(selected)} @@ -324,7 +331,7 @@ def index_page():

-
+
- +
+ + +
@@ -560,10 +573,11 @@ def index_page(): } async function downloadSingle(id) { + const highRes = document.getElementById('highResToggle')?.checked || false; await fetch('/api/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ book_ids: [id] }) + body: JSON.stringify({ book_ids: [id], high_res: highRes }) }); pollStatus(); } @@ -571,10 +585,11 @@ def index_page(): async function downloadSelected() { const ids = Array.from(selectedBookIds); if (ids.length === 0) return; + const highRes = document.getElementById('highResToggle')?.checked || false; await fetch('/api/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ book_ids: ids }) + body: JSON.stringify({ book_ids: ids, high_res: highRes }) }); selectedBookIds.clear(); updateSelectionUI();