mirror of
https://github.com/happycola233/tchMaterial-parser.git
synced 2026-10-27 13:21:16 +08:00
@@ -93,4 +93,4 @@ body:
|
||||
id: extra
|
||||
attributes:
|
||||
label: 补充说明
|
||||
description: 其他您认为有帮助的信息,例如出问题的教材链接(不含 Token)。
|
||||
description: 其他您认为有帮助的信息,例如出问题的资源链接(不含 Token)。
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
- 📂 **自动命名文件**:工具会自动使用电子课本的名称作为默认文件名,方便管理下载的课本文件。
|
||||
- 🔖 **自动添加书签**:若开启 “添加 PDF 书签”,则会在下载完成后为电子课本添加书签,在查看 PDF 时可更方便地跳转到指定位置。
|
||||
- 🔑 **支持 Access Token**:支持用户[手动输入 Access Token](#2--设置-access-token可选) 并自动保存,下次启动可自动加载。
|
||||
- 🔎 **教材快速搜索**:可按教材名称或 “学段、学科、年级” 等分类组合搜索,结果会自动展开;长名称支持横向滚动,悬停时可查看完整信息和大尺寸封面。
|
||||
- 🔎 **资源快速搜索**:可按资源名称或 “学段、学科、年级” 等分类组合搜索,结果会自动展开;长名称支持横向滚动,悬停时可查看完整信息和大尺寸封面。
|
||||
- 🖥️ **高 DPI 适配**:优化 UI 以适配高分辨率屏幕,避免界面模糊问题。
|
||||
- 🌗 **深色模式**:启动时自动跟随系统的浅色/深色模式,也可点击右上角的按钮手动切换,切换结果会被记住。
|
||||
- 💻 **跨平台支持**:支持 Windows、Linux、macOS 等操作系统(需要图形界面)。
|
||||
@@ -250,7 +250,7 @@ https://basic.smartedu.cn/tchMaterial/detail?contentType=assets_document&content
|
||||
|
||||
## ⚖️ 免责声明
|
||||
|
||||
- 本工具**仅提供下载上的便利**,不存储、不托管、不分发任何教材内容,所有资源均直接来自[国家中小学智慧教育平台](https://basic.smartedu.cn/)。
|
||||
- 本工具**仅提供下载上的便利**,不存储、不托管、不分发任何资源内容,所有资源均直接来自[国家中小学智慧教育平台](https://basic.smartedu.cn/)。
|
||||
- 所下载资源的**版权归原平台及相关权利人所有**,请仅用于个人学习与教学参考,**请勿用于商业用途或二次分发**。
|
||||
- 使用本工具时请遵守该平台的服务条款及您所在地区的法律法规。因使用本工具产生的任何后果由使用者自行承担。
|
||||
- 本项目与国家中小学智慧教育平台**没有任何隶属或合作关系**。
|
||||
|
||||
@@ -19,13 +19,26 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
|
||||
|
||||
if "contentId" in params:
|
||||
content_id = params["contentId"][0]
|
||||
elif re.search(r"^https?://([^/]+)/syncClassroom/classActivity", url): # 课程资源
|
||||
content_type = "national_lesson"
|
||||
if "activityId" in params:
|
||||
content_id = params["activityId"][0]
|
||||
else:
|
||||
return None
|
||||
elif re.search(r"^https?://([^/]+)/qualityCourse", url): # 精品课
|
||||
content_type = "quality_course"
|
||||
if "courseId" in params:
|
||||
content_id = params["courseId"][0]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
if "contentType" in params:
|
||||
content_type = params["contentType"][0]
|
||||
else:
|
||||
content_type = "assets_document"
|
||||
if not content_type:
|
||||
if "contentType" in params:
|
||||
content_type = params["contentType"][0]
|
||||
else:
|
||||
content_type = "assets_document"
|
||||
|
||||
# 2. 获取资源的信息
|
||||
# 返回数据示例:
|
||||
@@ -60,19 +73,20 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
|
||||
"""
|
||||
# 其中 $.ti_items 的每一项对应一个资源
|
||||
|
||||
if re.search(r"^https?://([^/]+)/syncClassroom/basicWork/detail", url): # 对基础性作业的解析
|
||||
if re.search(r"^https?://([^/]+)/tchMaterial/detail", url) and content_type == "assets_document": # 对普通电子课本的解析
|
||||
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/resources/tch_material/details/{content_id}.json")
|
||||
elif content_type == "national_lesson": # 对课程资源的解析
|
||||
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/national_lesson/resources/details/{content_id}.json")
|
||||
elif content_type == "quality_course": # 对精品课的解析
|
||||
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/resources/{content_id}.json")
|
||||
else: # 对专题课程(含电子课本、视频等)、其他类型资源的解析
|
||||
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/special_edu/resources/details/{content_id}.json")
|
||||
else: # 对课本的解析
|
||||
if content_type == "thematic_course": # 对专题课程(含电子课本、视频等)的解析
|
||||
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/special_edu/resources/details/{content_id}.json")
|
||||
else: # 对普通电子课本的解析
|
||||
response = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrv2/resources/tch_material/details/{content_id}.json")
|
||||
|
||||
data: dict = response.json()
|
||||
|
||||
# 3. 获取资源标题、下载链接及章节目录
|
||||
def get_resource_info(resource_data: dict) -> tuple[str, str, str, list[dict]] | None:
|
||||
title: str = resource_data.get("title")
|
||||
def get_resource_info(resource_data: dict, root_title: str | None = None) -> tuple[str, str, str, list[dict]] | None:
|
||||
title: str = f"{root_title} - {resource_data.get('title') or resource_data.get('id')}" if root_title else resource_data.get("title") or resource_data.get("id")
|
||||
resource_url: str | None = None
|
||||
resource_format = "pdf"
|
||||
|
||||
@@ -93,6 +107,24 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
|
||||
continue
|
||||
break
|
||||
|
||||
if not resource_url: # 使用不同的判断条件寻找源文件
|
||||
for item in resource_data["ti_items"]:
|
||||
if not item["ti_file_flag"] in ("source", "pdf"):
|
||||
continue
|
||||
|
||||
resource_format = item.get("ti_format") or "pdf"
|
||||
if resource_format == "folder":
|
||||
continue
|
||||
|
||||
resource_url = item.get("ti_storage")
|
||||
if resource_url:
|
||||
resource_url = resource_url.replace("cs_path:${ref-path}", "https://r1-ndr-private.ykt.cbern.com.cn")
|
||||
else:
|
||||
resource_url = next((url for url in item["ti_storages"] if url), None)
|
||||
if not resource_url:
|
||||
continue
|
||||
break
|
||||
|
||||
if not resource_url:
|
||||
return None
|
||||
|
||||
@@ -167,17 +199,27 @@ def parse(url: str, bookmarks: bool) -> list[tuple[str, str, str, list[dict]]] |
|
||||
|
||||
return title, resource_url, resource_format, chapters
|
||||
|
||||
resource_info = get_resource_info(data)
|
||||
if resource_info:
|
||||
resources_info.append(resource_info)
|
||||
|
||||
if content_type == "thematic_course": # 专题课程
|
||||
resources_resp = session.get(f"https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/special_edu/thematic_course/{content_id}/resources/list.json")
|
||||
resources_data: list[dict] = resources_resp.json()
|
||||
for resource in resources_data:
|
||||
resource_info = get_resource_info(resource)
|
||||
resource_info = get_resource_info(resource, data["title"])
|
||||
if resource_info:
|
||||
resources_info.append(resource_info)
|
||||
elif content_type == "national_lesson": # 课程资源
|
||||
for resource in data["relations"]["national_course_resource"]:
|
||||
resource_info = get_resource_info(resource, data["title"])
|
||||
if resource_info:
|
||||
resources_info.append(resource_info)
|
||||
elif content_type == "quality_course": # 精品课
|
||||
for resource in data["relations"]["course_resource"]:
|
||||
resource_info = get_resource_info(resource, data["title"])
|
||||
if resource_info:
|
||||
resources_info.append(resource_info)
|
||||
else: # 其他类型资源
|
||||
resource_info = get_resource_info(data)
|
||||
if resource_info:
|
||||
resources_info.append(resource_info)
|
||||
|
||||
return resources_info
|
||||
|
||||
|
||||
@@ -31,18 +31,15 @@ class ResourceHelper: # 获取网站上资源的数据
|
||||
for book in book_data:
|
||||
if book.get("tag_paths"): # 某些非课本资料的 tag_paths 属性为空数组
|
||||
# 解析课本层级数据
|
||||
tag_paths: list[str] = book["tag_paths"][0].split("/")[2:] # 电子课本 tag_paths 的前两项为“教材”、“电子教材”
|
||||
|
||||
# 如果课本层级数据不在层级数据中,跳过
|
||||
temp_hier = parsed_hier[book["tag_paths"][0].split("/")[1]]
|
||||
if not tag_paths[0] in temp_hier["children"]:
|
||||
continue
|
||||
tag_paths: list[str] = book["tag_paths"][0].split("/")
|
||||
|
||||
# 分别解析课本层级
|
||||
for p in tag_paths:
|
||||
if temp_hier["children"] and temp_hier["children"].get(p):
|
||||
temp_hier = temp_hier["children"].get(p)
|
||||
if not temp_hier["children"]:
|
||||
temp_hier = parsed_hier[tag_paths[1]]
|
||||
|
||||
for p in tag_paths[2:]: # 电子课本 tag_paths 的前两项为 “教材”、“电子教材”
|
||||
if temp_hier.get("children") and temp_hier["children"].get(p):
|
||||
temp_hier = temp_hier["children"][p]
|
||||
if not temp_hier.get("children"):
|
||||
temp_hier["children"] = {}
|
||||
|
||||
book["display_name"] = book["title"] if "title" in book else book["name"] if "name" in book else f"(未知电子课本 {book['id']})"
|
||||
@@ -51,11 +48,11 @@ class ResourceHelper: # 获取网站上资源的数据
|
||||
|
||||
return parsed_hier
|
||||
|
||||
def fetch_lesson_list(self) -> dict: # 获取课件列表
|
||||
def fetch_national_lesson_list(self) -> dict: # 获取自学课件列表
|
||||
# 获取课件层级数据
|
||||
tags_resp = session.get("https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/tags/national_lesson_tag.json")
|
||||
tags_data: dict = tags_resp.json()
|
||||
parsed_hier = self.parse_hierarchy([{ "children": [{ "tag_id": "__internal_national_lesson", "hierarchies": tags_data["hierarchies"], "tag_name": "课件资源" }] }])
|
||||
parsed_hier = self.parse_hierarchy([{ "children": [{ "tag_id": "__internal_national_lesson", "hierarchies": tags_data["hierarchies"], "tag_name": "学生自主学习课件" }] }])
|
||||
|
||||
# 获取课件 URL 列表
|
||||
list_resp = session.get("https://s-file-1.ykt.cbern.com.cn/zxx/ndrs/national_lesson/teachingmaterials/version/data_version.json")
|
||||
@@ -73,12 +70,48 @@ class ResourceHelper: # 获取网站上资源的数据
|
||||
# 分别解析课件层级(tag_paths 为乱序)
|
||||
def parse_tag_path(hier: dict) -> dict:
|
||||
for p in tag_paths:
|
||||
if hier["children"] and hier["children"].get(p):
|
||||
return parse_tag_path(hier["children"].get(p))
|
||||
if hier.get("children") and hier["children"].get(p):
|
||||
return parse_tag_path(hier["children"][p])
|
||||
return hier
|
||||
|
||||
hier = parse_tag_path(parsed_hier["__internal_national_lesson"])
|
||||
if not hier["children"]:
|
||||
if not hier.get("children"):
|
||||
hier["children"] = {}
|
||||
|
||||
lesson["display_name"] = lesson["title"] if "title" in lesson else lesson["name"] if "name" in lesson else f"(未知课件 {lesson['id']})"
|
||||
|
||||
hier["children"][lesson["id"]] = lesson
|
||||
|
||||
return parsed_hier
|
||||
|
||||
def fetch_prepare_lesson_list(self) -> dict: # 获取备课课件列表
|
||||
# 获取课件层级数据
|
||||
tags_resp = session.get("https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/tags/k12.json")
|
||||
tags_data: dict = tags_resp.json()
|
||||
parsed_hier = self.parse_hierarchy([{ "children": [{ "tag_id": "__internal_prepare_lesson", "hierarchies": tags_data["hierarchies"], "tag_name": "教师备课授课课件" }] }])
|
||||
|
||||
# 获取课件 URL 列表
|
||||
list_resp = session.get("https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/prepare_lesson/teachingmaterials/parts.json")
|
||||
list_data: list[str] = list_resp.json()
|
||||
|
||||
# 获取课件列表
|
||||
for url in list_data:
|
||||
lesson_resp = session.get(url)
|
||||
lesson_data: list[dict] = lesson_resp.json()
|
||||
for lesson in lesson_data:
|
||||
if lesson.get("tag_list"):
|
||||
# 解析课件层级数据
|
||||
tag_paths: list[str] = [tag["tag_id"] for tag in sorted(lesson["tag_list"], key=lambda tag: tag["order_num"])]
|
||||
|
||||
# 分别解析课件层级(tag_paths 为乱序)
|
||||
def parse_tag_path(hier: dict) -> dict:
|
||||
for p in tag_paths:
|
||||
if hier.get("children") and hier["children"].get(p):
|
||||
return parse_tag_path(hier["children"][p])
|
||||
return hier
|
||||
|
||||
hier = parse_tag_path(parsed_hier["__internal_prepare_lesson"])
|
||||
if not hier.get("children"):
|
||||
hier["children"] = {}
|
||||
|
||||
lesson["display_name"] = lesson["title"] if "title" in lesson else lesson["name"] if "name" in lesson else f"(未知课件 {lesson['id']})"
|
||||
@@ -89,7 +122,8 @@ class ResourceHelper: # 获取网站上资源的数据
|
||||
|
||||
def fetch_resource_list(self) -> dict: # 获取资源列表
|
||||
book_hier = self.fetch_book_list()
|
||||
# lesson_hier = self.fetch_lesson_list()
|
||||
# national_lesson_hier = self.fetch_national_lesson_list()
|
||||
# prepare_lesson_hier = self.fetch_prepare_lesson_list()
|
||||
return { **book_hier }
|
||||
|
||||
def filter_resource_items(items: dict[str, dict], query: str) -> dict[str, dict]: # 按完整分类路径筛选资源树
|
||||
|
||||
@@ -62,7 +62,7 @@ def download() -> None: # 下载资源文件
|
||||
resource_urls.add(resource_url)
|
||||
|
||||
if len(resources_info_list) > 1:
|
||||
messagebox.showinfo("提示", "您将下载多个文件,请选择要下载文件的位置,本程序将在选定的文件夹中使用教材名称作为文件名进行下载。")
|
||||
messagebox.showinfo("提示", "您将下载多个文件,请选择要下载文件的位置,本程序将在选定的文件夹中使用资源名称作为文件名进行下载。")
|
||||
dir_path = filedialog.askdirectory() # 选择文件夹
|
||||
if not dir_path: # 用户取消或关闭对话框
|
||||
download_btn.config(state="normal") # 恢复下载按钮为启用状态
|
||||
|
||||
@@ -166,8 +166,9 @@ def build_resource_tree(pane: ttk.Frame, resource_list: dict[str, dict], url_tex
|
||||
|
||||
resource_type = resource_data.get("resource_type_code") or "assets_document"
|
||||
content_id = resource_data.get("content_id") or item.split(":")[-1]
|
||||
root_id = item.split(":")[0]
|
||||
if resource_type == "teachingmaterials":
|
||||
url = f"https://basic.smartedu.cn/syncClassroom?defaultTag={'%2F'.join(item.split(':')[1:])}"
|
||||
url = f"https://basic.smartedu.cn/syncClassroom{'/prepare' if root_id == '__internal_prepare_lesson' else ''}?defaultTag={'%2F'.join(item.split(':')[1:])}"
|
||||
else:
|
||||
url = f"https://basic.smartedu.cn/tchMaterial/detail?contentType={resource_type}&contentId={content_id}&catalogType=tchMaterial&subCatalog=tchMaterial"
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ def show_access_token_window() -> None: # 打开输入 Access Token 的窗口
|
||||
help_frame.pack(fill="both", expand=True)
|
||||
|
||||
help_text = """\
|
||||
国家中小学智慧教育平台需要登录后才可获取教材,因此要使用本程序下载教材,您需要在平台内登录账号(如没有需注册),然后获得登录凭据(Access Token)。本程序仅保存该凭据至本地。
|
||||
国家中小学智慧教育平台需要登录后才可获取资源,因此要使用本程序下载资源,您需要在平台内登录账号(如没有需注册),然后获得登录凭据(Access Token)。本程序仅保存该凭据至本地。
|
||||
|
||||
获取方法如下:
|
||||
1. 打开浏览器,访问国家中小学智慧教育平台(https://auth.smartedu.cn/uias/login)并登录账号。
|
||||
|
||||
Reference in New Issue
Block a user