Files
FreePEP/cli.py
T
2026-08-31 10:22:43 +08:00

215 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
人教社电子教材交互式终端下载器 (cli.py)
支持:
1. 交互式菜单筛选(学段 -> 学科 -> 年级)
2. 批量全选下载或勾选指定教材下载
3. 命令行参数一键过滤下载 (例如: python cli.py --xd "小学(六三学制)" --xk "语文" --nj "一年级")
4. 关键词全局模糊搜索下载
5. 缓存清理与目录强制刷新
"""
import sys
import os
import argparse
from typing import List, Dict
from pep_core import PepCatalog, PepDownloader, XD_ORDER, XK_ORDER_PREFIX, NJ_ORDER, sort_xk_key, sort_nj_key
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
def print_banner():
print("=" * 65)
print(" 📚 人民教育出版社 (PEP) 电子教材终端下载工具")
print("=" * 65)
def select_from_list(prompt: str, options: List[str], allow_all: bool = True) -> str:
"""辅助函数:提供多选编号菜单"""
items = ["全部"] + options if allow_all and "全部" not in options else options
print(f"\n👉 请选择【{prompt}】:")
for i, item in enumerate(items, 1):
print(f" [{i}] {item}")
while True:
try:
choice = input(f"请输入序号 (1-{len(items)}, 默认 1): ").strip()
if not choice:
return items[0]
idx = int(choice)
if 1 <= idx <= len(items):
return items[idx - 1]
print(f"[-] 输入超出范围,请输入 1 到 {len(items)} 之间的数字。")
except ValueError:
print("[-] 请输入有效的数字序号。")
def interactive_mode():
"""交互式导航筛选模式"""
print_banner()
print("[*] 正在加载教材分类结构...")
structure = PepCatalog.get_structure()
all_xds = list(structure.keys())
print("\n请选择检索模式:")
print(" [1] 分类层级筛选(学段 ➔ 学科 ➔ 年级)")
print(" [2] 关键词全局搜索(如输入:'必修一'、'高一数学'、'生物')")
mode_choice = input("请输入模式编号 (1/2, 默认 1): ").strip()
matched_books = []
if mode_choice == "2":
kw = input("\n🔍 请输入搜索关键词: ").strip()
if not kw:
print("[-] 关键词不能为空!")
return
matched_books = PepCatalog.filter_books(keyword=kw)
else:
# 1. 选择学段(已按规定排序)
selected_xd = select_from_list("学段", all_xds, allow_all=True)
# 获取该学段下的学科和年级
if selected_xd == "全部":
s_set = set(s for x in structure.values() for s in x["subjects"])
all_subjects = sorted(list(s_set), key=sort_xk_key)
g_set = set(g for x in structure.values() for g in x["grades"])
all_grades = sorted(list(g_set), key=sort_nj_key)
else:
all_subjects = structure[selected_xd]["subjects"]
all_grades = structure[selected_xd]["grades"]
# 2. 选择学科(已按规定排序)
selected_xk = select_from_list("学科", all_subjects, allow_all=True)
# 3. 选择年级(已按规定排序)
selected_nj = select_from_list("年级", all_grades, allow_all=True)
matched_books = PepCatalog.filter_books(xd=selected_xd, xk=selected_xk, nj=selected_nj)
if not matched_books:
print("\n[-] 未匹配到任何符合条件的教材!")
return
print(f"\n✅ 共检索到 {len(matched_books)} 本教材:")
print("-" * 65)
for idx, b in enumerate(matched_books, 1):
xd = b.get("xd", "")
xk = b.get("xk", "")
nj = b.get("nj", "")
cc = b.get("cc", "")
title = b.get("title", "")
print(f" [{idx:2d}] [{xd}|{xk}|{nj}{cc}] 《{title}》 (ID: {b['id']})")
print("-" * 65)
print("\n请选择下载范围:")
print(" • 直接按 Enter 或输入 'all': 下载当前列表中的【全部】教材")
print(" • 输入单个序号(如 '3'): 只下载第 3 本")
print(" • 输入多个序号(如 '1,3,5' 或范围 '1-4'): 批量下载指定教材")
print(" • 输入 'q': 退出")
select_str = input("\n请输入下载指令: ").strip().lower()
if select_str == "q":
print("[*] 已取消操作。")
return
to_download = []
if not select_str or select_str == "all":
to_download = matched_books
else:
indices = set()
for part in select_str.split(","):
part = part.strip()
if "-" in part:
try:
s, e = map(int, part.split("-"))
for i in range(s, e + 1):
indices.add(i)
except ValueError:
pass
elif part.isdigit():
indices.add(int(part))
for i in sorted(list(indices)):
if 1 <= i <= len(matched_books):
to_download.append(matched_books[i - 1])
if not to_download:
print("[-] 未选择有效教材,退出。")
return
out_dir = os.path.abspath("./downloads")
print(f"\n🚀 即将开始下载 {len(to_download)} 本教材,保存目录: {out_dir}")
downloader = PepDownloader(headless=True, output_dir=out_dir)
for idx, b in enumerate(to_download, 1):
print(f"\n==================================================")
print(f"[{idx}/{len(to_download)}] 开始下载: 《{b.get('title')}》")
print(f"==================================================")
downloader.download_book(book_id=b["id"], custom_title=b.get("title"))
print("\n🎉 全部选定任务执行完毕!")
def cli_args_mode(args):
"""命令行参数直接执行模式"""
print_banner()
matched = PepCatalog.filter_books(xd=args.xd, xk=args.xk, nj=args.nj, keyword=args.search)
if not matched:
print("[-] 未查找到符合条件的教材!")
return
print(f"[+] 符合条件的教材共 {len(matched)} 本:")
for idx, b in enumerate(matched, 1):
print(f" [{idx}] 《{b.get('title')}》 (ID: {b['id']})")
if not args.yes:
confirm = input(f"\n确认下载以上 {len(matched)} 本教材吗?(y/n, 默认 y): ").strip().lower()
if confirm == "n":
print("[*] 已取消。")
return
out_dir = os.path.abspath(args.output)
downloader = PepDownloader(headless=True, output_dir=out_dir)
for idx, b in enumerate(matched, 1):
print(f"\n[{idx}/{len(matched)}] 正在下载: 《{b.get('title')}》...")
downloader.download_book(book_id=b["id"], custom_title=b.get("title"))
print(f"\n🎉 下载完成!文件已保存至: {out_dir}")
def main():
parser = argparse.ArgumentParser(description="人民教育出版社电子教材 CLI 下载器")
parser.add_argument("--xd", help="指定学段(如:小学(六三学制)、初中(六三学制)、高中等)")
parser.add_argument("--xk", help="指定学科(如:语文、数学、英语、物理等)")
parser.add_argument("--nj", help="指定年级(如:一年级、七年级、必修等)")
parser.add_argument("--search", "-s", help="全局搜索关键词")
parser.add_argument("--output", "-o", default="./downloads", help="PDF 文件保存目录 (默认: ./downloads)")
parser.add_argument("--yes", "-y", action="store_true", help="免确认直接开始下载")
parser.add_argument("--refresh", "-r", action="store_true", help="强制从官方服务器重新拉取并解密最新教材目录")
parser.add_argument("--clear-cache", "-c", action="store_true", help="清理本地临时下载缓存 (temp_pages)")
args = parser.parse_args()
if args.clear_cache:
print_banner()
res = PepCatalog.clear_cache_files()
print(f"[✔] {res['message']}")
return
if args.refresh:
print_banner()
print("[*] 正在从人教社官方前端拉取并解密最新教材目录...")
books = PepCatalog.fetch_and_decrypt_all(force_refresh=True)
print(f"[+] 目录同步成功!共获取到 {len(books)} 本教材。")
return
if args.xd or args.xk or args.nj or args.search:
cli_args_mode(args)
else:
interactive_mode()
if __name__ == "__main__":
main()