国家中小学智慧教育平台教材PDF下载爬虫

发布时间 2023-12-13 10:19:13作者: 那个白熊

一、确定目标网站

image

二、目标数据分析

2.1 查看目标数据

点击教材后,发现需要登录,如下图。
image
注册登录后查看,同时打开DevTools记录数据包,发现教材PDF下载链接,但无法直接下载,如下图。
image
网上搜索相关话题后发现可通过更改URL绕过该限制,经测试可行,如下图。
image

2.2 爬取思路

既然找到了实际下载链接,那么只需要再找到所有教材PDF的ID即可,有两种思路。

  1. 找到相关数据集请求包,直接拿到所有ID或者下载链接;
  2. 使用selenium逐个点击教材获取地址栏的文件ID,之后使用requests下载。

三、实际爬取

3.1 使用selenium爬取文件ID

由于未登录看不到教材内容,所以我测试时均登录了账号,但是经过反复抓包均未看到相关数据集请求(后证实和缓存有关,尽管我在DevTools中设置了禁用缓存)。所以先采用selenium爬取文件ID的方法。
image
由于科目众多,所以我们需要使用循环嵌套的形式遍历所有教材,结构如下图所示。
image
经过上面4层嵌套后,再获取当前页面下教材列表,逐个点击获取教材ID即可。
因为每次点击都会打开新标签页,我们可以使用selenium.webdriver.switch_to.window(window_handle)的方法切换到新标签页,然后调用close()方法后再切换回去。这样就避免了大量标签页带来的内存消耗。
由于后面有更好的方法,这里就不放代码了。

3.2 使用requests获取所有教材PDF

在经历了DevTools源代码搜索,JS断点调试,Burpsuite代理逐项通过请求等方法尝试找出数据集请求均未果后,我在无痕模式中重新打开教材链接,意味发现了数据接口相关请求,真可谓踏破铁鞋无觅处,得来全不费功夫。如下图所示。
image
xhr请求有三个,链接不完全相同,那么肯定有请求可以获取到这三个文件的的地址,经搜索可以定位到。如下图所示。
image
至此,我们便得到了所有需要的信息,按照以下步骤编写代码即可。

  1. 访问入口文件获取数据请求API;
  2. 访问数据请求API获取所有文件信息;
  3. 构造教材PDF链接进行下载。
"""
@filename=main.py
@version=0.1.0.20231212
@author=amnotgcs
@createTime=20231212 19:01
@lastModifiedTime=20231212 20:43
@description=爬取国家中小学智慧教育平台所有教材PDF
@target.url=https://basic.smartedu.cn/tchMaterial
"""


import json

import requests
import pandas as pd


# 数据文件获取链接
ENTRY_URL = 'https://s-file-2.ykt.cbern.com.cn/zxx/ndrs/resources/tch_material/version/data_version.json'
HEADER = {
    'Accept': 'application/json, text/plain, */*',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
    'Referer': 'https://basic.smartedu.cn/',
}
TIMEOUT = 3
JSON_FILENAME = 'books_info.json'  # JSON结果文件名
XLSX_FILENAME = 'books_info.xlsx'  # XLSX结果文件名


def get_data_json_url() -> list[str]:
    """访问数据文件获取链接,获取数据URL"""
    data_json_url = requests.get(ENTRY_URL, headers=HEADER, timeout=TIMEOUT).json()
    if result := data_json_url.get('urls'):
        return result.split(',')
    return []


def get_books_info(url: str) -> list[dict]:
    "访问数据URL,获取所有图书的信息"
    response = requests.get(url, headers=HEADER, timeout=TIMEOUT)
    if response.status_code == 200:
        return response.json()
    return []
    

def retrieve_all_books() -> list[dict]:
    """获取平台所有教材PDF"""
    books = []
    for data_json_url in get_data_json_url():
        books.extend(get_books_info(data_json_url))
    for book in books:
        book_id = book.get('id')
        book_url = f'https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets/{book_id}.pkg/pdf.pdf'
        book['pdf_url'] = book_url
        # TODO(amnotgcs): 此处未实际下载pdf,根据个人需要进行下载即可。
        # 样例代码:
        # pdf = requests.get(book_url, headers=HEADER)
        # with open(f'{book_id}.pdf', 'wb') as file:
        #     file.write(pdf.content)
        print('正在下载:', book_url)
    return books


def save_books_info(books: list[dict]) -> None:
    """将PDF信息保存到文件"""
    # 提取需要的信息
    book_info_for_save = []
    for book in books:
        book_info = {}
        for item in ['id', 'title', 'language', 'resource_type_code', 'create_time', 'update_time', 'pdf_url']:
            book_info[item] = book.get(item)
        for item in ['resolution', 'size', 'width']:
            book_info[item] = book.get('custom_properties').get(item)
        book_info['tag'] = '/'.join([tag.get('tag_name') for tag in book.get('tag_list')])
        book_info_for_save.append(book_info)
    # 保存为JSON格式
    with open(JSON_FILENAME, 'wt', encoding='UTF-8') as file:
        json.dump(book_info_for_save, file, ensure_ascii=False, indent=4)
    # 保存为xlsx格式
    book_info_df = pd.read_json(JSON_FILENAME)
    # 去除时区信息以便存为xlsx
    book_info_df['create_time'] = book_info_df['create_time'].dt.tz_localize(None)
    book_info_df['update_time'] = book_info_df['update_time'].dt.tz_localize(None)
    book_info_df.to_excel(XLSX_FILENAME)


if __name__ == '__main__':
    books = retrieve_all_books()
    save_books_info(books)

【说明】因为headers中'Accept-Encoding': 'gzip, deflate, br'指明了可以使用br压缩算法,所以需要pip安装brotli库,否则可能会有乱码问题。如果不想安装,去除headers中的br亦可。