一、确定目标网站
二、目标数据分析
2.1 查看目标数据
点击教材后,发现需要登录,如下图。
注册登录后查看,同时打开DevTools记录数据包,发现教材PDF下载链接,但无法直接下载,如下图。
网上搜索相关话题后发现可通过更改URL绕过该限制,经测试可行,如下图。
2.2 爬取思路
既然找到了实际下载链接,那么只需要再找到所有教材PDF的ID即可,有两种思路。
- 找到相关数据集请求包,直接拿到所有ID或者下载链接;
- 使用selenium逐个点击教材获取地址栏的文件ID,之后使用requests下载。
三、实际爬取
3.1 使用selenium爬取文件ID
由于未登录看不到教材内容,所以我测试时均登录了账号,但是经过反复抓包均未看到相关数据集请求(后证实和缓存有关,尽管我在DevTools中设置了禁用缓存)。所以先采用selenium爬取文件ID的方法。
由于科目众多,所以我们需要使用循环嵌套的形式遍历所有教材,结构如下图所示。
经过上面4层嵌套后,再获取当前页面下教材列表,逐个点击获取教材ID即可。
因为每次点击都会打开新标签页,我们可以使用selenium.webdriver.switch_to.window(window_handle)的方法切换到新标签页,然后调用close()方法后再切换回去。这样就避免了大量标签页带来的内存消耗。
由于后面有更好的方法,这里就不放代码了。
3.2 使用requests获取所有教材PDF
在经历了DevTools源代码搜索,JS断点调试,Burpsuite代理逐项通过请求等方法尝试找出数据集请求均未果后,我在无痕模式中重新打开教材链接,意味发现了数据接口相关请求,真可谓踏破铁鞋无觅处,得来全不费功夫。如下图所示。
xhr请求有三个,链接不完全相同,那么肯定有请求可以获取到这三个文件的的地址,经搜索可以定位到。如下图所示。
至此,我们便得到了所有需要的信息,按照以下步骤编写代码即可。
- 访问入口文件获取数据请求API;
- 访问数据请求API获取所有文件信息;
- 构造教材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
亦可。