1. 项目概述
中国大学MOOC作为国内领先的在线教育平台,汇集了众多高校的优质课程资源。对于教育研究者、数据分析师或普通学习者而言,获取这些课程的结构化数据具有重要价值。本项目将使用Python爬虫技术,完整爬取中国大学MOOC的课程目录数据,并将结果以CSV和JSON两种格式导出。
提示:本教程适合具备Python基础语法知识的初学者,无需专业爬虫经验。我们将从最基础的HTTP请求开始,逐步构建完整的爬虫程序。
2. 环境准备与工具选型
2.1 Python环境配置
建议使用Python 3.8及以上版本,这是目前大多数爬虫库稳定支持的版本。可以通过以下命令检查Python版本:
bash复制python --version
如果尚未安装Python,推荐从官网下载安装包,安装时勾选"Add Python to PATH"选项。
2.2 必要库安装
本项目主要依赖以下Python库:
bash复制pip install requests beautifulsoup4 pandas
requests:用于发送HTTP请求获取网页内容beautifulsoup4:HTML解析工具pandas:数据处理和CSV导出
2.3 开发工具推荐
虽然任何文本编辑器都可以编写Python代码,但为了提高效率,推荐使用:
- VS Code:轻量级且功能强大,安装Python插件后支持代码提示和调试
- PyCharm:专业的Python IDE,提供更全面的开发支持
- Jupyter Notebook:适合交互式开发和数据探索
3. 目标网站分析与爬取策略
3.1 网站结构分析
中国大学MOOC的课程页面通常遵循以下URL结构:
code复制https://www.icourse163.org/course/学校简称-课程ID
通过分析页面源代码,我们发现课程目录数据通常以JSON格式嵌入在HTML中,这比直接解析HTML更可靠。
3.2 爬取策略选择
基于对目标网站的分析,我们采用以下策略:
- 静态页面抓取:直接获取HTML内容
- JSON数据提取:从HTML中提取嵌入的JSON数据
- 增量爬取:控制请求频率,避免对服务器造成过大压力
注意:在开始爬取前,务必检查目标网站的robots.txt文件,确认允许爬取的路径。中国大学MOOC的robots.txt通常允许对课程页面的访问,但仍需遵守合理的爬取频率。
4. 核心代码实现
4.1 请求模块实现
首先创建一个安全的请求函数,包含基本的异常处理和请求头设置:
python复制import requests
from time import sleep
def safe_request(url, max_retries=3, delay=1):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}, 尝试 {attempt + 1}/{max_retries}")
if attempt < max_retries - 1:
sleep(delay * (attempt + 1))
return None
4.2 数据解析模块
从HTML中提取JSON数据并解析课程信息:
python复制import re
import json
from bs4 import BeautifulSoup
def parse_course_html(html):
soup = BeautifulSoup(html, 'html.parser')
# 查找包含课程数据的JSON
script_tags = soup.find_all('script')
for script in script_tags:
if 'window.__NUXT__=' in script.text:
json_str = re.search(r'window\.__NUXT__=(.*?);', script.text).group(1)
course_data = json.loads(json_str)
return extract_course_info(course_data)
return None
def extract_course_info(course_data):
try:
course_info = course_data['state']['course']
chapters = []
for chapter in course_info['term']['chapters']:
chapter_info = {
'chapter_id': chapter['id'],
'chapter_name': chapter['name'],
'lessons': []
}
for lesson in chapter['lessons']:
lesson_info = {
'lesson_id': lesson['id'],
'lesson_name': lesson['name'],
'video_url': lesson.get('videoUrl', ''),
'duration': lesson.get('duration', 0)
}
chapter_info['lessons'].append(lesson_info)
chapters.append(chapter_info)
return {
'course_id': course_info['id'],
'course_name': course_info['name'],
'school': course_info['school']['name'],
'teacher': course_info['teacher']['name'],
'chapters': chapters
}
except KeyError as e:
print(f"解析JSON时出错: {e}")
return None
4.3 数据存储模块
将解析后的数据保存为CSV和JSON格式:
python复制import pandas as pd
import json
import os
def save_data(course_info, output_dir='output'):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 保存为JSON(保留完整结构)
json_path = os.path.join(output_dir, f"{course_info['course_id']}.json")
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(course_info, f, ensure_ascii=False, indent=2)
# 转换为扁平化结构保存CSV
csv_data = []
for chapter in course_info['chapters']:
for lesson in chapter['lessons']:
csv_data.append({
'course_id': course_info['course_id'],
'course_name': course_info['course_name'],
'school': course_info['school'],
'teacher': course_info['teacher'],
'chapter_id': chapter['chapter_id'],
'chapter_name': chapter['chapter_name'],
'lesson_id': lesson['lesson_id'],
'lesson_name': lesson['lesson_name'],
'video_url': lesson['video_url'],
'duration': lesson['duration']
})
df = pd.DataFrame(csv_data)
csv_path = os.path.join(output_dir, f"{course_info['course_id']}.csv")
df.to_csv(csv_path, index=False, encoding='utf_8_sig')
print(f"数据已保存到: {json_path} 和 {csv_path}")
5. 完整爬取流程
5.1 主程序实现
将各个模块组合成完整的爬虫程序:
python复制def crawl_mooc_course(course_url):
print(f"开始爬取课程: {course_url}")
# 1. 发送请求获取HTML
html = safe_request(course_url)
if not html:
print("获取HTML内容失败")
return
# 2. 解析HTML提取课程数据
course_info = parse_course_html(html)
if not course_info:
print("解析课程数据失败")
return
# 3. 保存数据
save_data(course_info)
print("爬取完成!")
if __name__ == '__main__':
# 示例课程URL
sample_course_url = "https://www.icourse163.org/course/PKU-1002536002"
crawl_mooc_course(sample_course_url)
5.2 运行与结果
执行上述程序后,将在项目目录下生成output文件夹,包含两个文件:
- JSON文件:保留完整的课程树形结构
- CSV文件:扁平化的课程数据,适合用Excel打开分析
6. 常见问题与解决方案
6.1 请求被拒绝(403错误)
如果遇到403 Forbidden错误,可以尝试:
- 更换User-Agent头
- 增加请求间隔时间
- 使用代理IP(需谨慎,确保合规)
6.2 JSON解析失败
可能原因包括:
- 网站结构变化导致JSON路径改变
- 课程页面使用了不同的数据格式
解决方案:
- 检查HTML中是否包含预期的JSON数据
- 更新解析逻辑以适应变化
6.3 CSV文件乱码
在Excel中打开CSV可能出现乱码,这是因为编码问题。我们已经在代码中使用了utf_8_sig编码,可以解决大多数乱码问题。如果仍然遇到问题,可以:
- 使用文本编辑器(如Notepad++)打开CSV文件,确认内容正常
- 在Excel中使用"数据"→"从文本/CSV"导入功能,手动选择UTF-8编码
7. 进阶优化建议
7.1 并发爬取
对于大量课程爬取,可以使用concurrent.futures实现并发请求:
python复制from concurrent.futures import ThreadPoolExecutor
def crawl_multiple_courses(course_urls, max_workers=3):
with ThreadPoolExecutor(max_workers=max_workers) as executor:
executor.map(crawl_mooc_course, course_urls)
7.2 断点续爬
添加日志记录功能,记录已爬取的课程ID,避免重复爬取:
python复制import pickle
def load_crawled_ids(log_file='crawled_ids.pkl'):
try:
with open(log_file, 'rb') as f:
return pickle.load(f)
except FileNotFoundError:
return set()
def save_crawled_id(course_id, log_file='crawled_ids.pkl'):
crawled_ids = load_crawled_ids(log_file)
crawled_ids.add(course_id)
with open(log_file, 'wb') as f:
pickle.dump(crawled_ids, f)
7.3 定时爬取
结合操作系统的定时任务功能(如Linux的cron或Windows的任务计划程序),可以实现定期更新课程数据。
8. 爬虫伦理与最佳实践
- 尊重robots.txt:始终检查并遵守目标网站的爬虫政策
- 控制请求频率:在请求之间添加适当延迟(如3-5秒)
- 缓存响应:对相同URL避免重复请求
- 错误处理:妥善处理各种网络和解析错误
- 数据使用:仅将爬取数据用于个人学习和研究,不用于商业用途
在实际操作中,我发现中国大学MOOC的课程数据结构相对稳定,但偶尔会有小的变动。建议定期检查解析逻辑,特别是当发现大量解析失败时。另外,对于需要登录才能访问的课程内容,不建议尝试爬取,这既可能违反网站规定,也可能涉及法律风险。