Python实战:构建中国大学MOOC课程数据爬虫与34万条评论数据集分析
最近在分析在线教育平台数据时,我发现中国大学MOOC平台上的课程评价数据特别有价值。这些真实用户反馈不仅能反映课程质量,还能揭示学习者的偏好和行为模式。今天就来分享一个完整的Python爬虫解决方案,从API分析到数据清洗,最终生成可直接用于研究的结构化数据集。
1. 环境准备与工具选择
在开始爬取数据前,我们需要搭建合适的开发环境。不同于简单的静态网页抓取,MOOC平台的数据大多通过API接口返回JSON格式,这对我们的工具选择提出了特定要求。
核心工具栈配置:
# 基础环境安装 pip install requests pandas numpy tqdm fake-useragent推荐配置:Python 3.8+环境,VS Code或PyCharm作为IDE,配合Jupyter Notebook进行数据探索。
关键库的作用:
requests:处理HTTP请求的核心库pandas:数据清洗和结构化存储tqdm:显示爬取进度条fake-useragent:生成随机请求头
注意:实际操作中建议配置代理IP池,虽然MOOC平台反爬机制相对宽松,但高频请求仍可能触发限制。
我通常会创建一个独立的虚拟环境来管理项目依赖:
python -m venv mooc_scraper source mooc_scraper/bin/activate # Linux/Mac mooc_scraper\Scripts\activate # Windows2. API分析与请求策略
中国大学MOOC平台采用前后端分离架构,所有课程数据都通过API接口动态加载。通过浏览器开发者工具(F12),我们可以轻松找到这些关键接口。
主要API端点分析:
| 接口类型 | URL示例 | 请求方式 | 关键参数 |
|---|---|---|---|
| 课程分类 | .../listChannelCategoryDetail.rpc | POST | csrfKey |
| 课程列表 | .../searchCourseCardByChannelAndCategoryId.rpc | POST | categoryId, pageIndex |
| 课程评价 | .../getCourseEvaluatePaginationByCourseIdOrTermId.rpc | POST | courseId, pageSize |
请求头定制技巧:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.icourse163.org/channel/", "Origin": "https://www.icourse163.org", "Content-Type": "application/x-www-form-urlencoded" }实际请求时需要注意几个关键点:
csrfKey参数需要从首页或登录后的Cookie中提取- 分页参数
pageIndex从1开始计数 - 评价接口的
orderBy=3表示按最新排序
3. 数据爬取实战代码
下面展示完整的爬虫实现,采用模块化设计便于维护和扩展。我们将分三个步骤获取数据:课程分类→课程详情→课程评价。
3.1 课程分类爬取
def get_course_categories(): base_url = "https://www.icourse163.org/web/j/channelBean.listChannelCategoryDetail.rpc" params = { "csrfKey": get_csrf_key() # 需要提前实现的函数 } payload = "includeALLChannels=true&includeDefaultChannels=false" response = requests.post(base_url, params=params, data=payload, headers=headers) data = response.json() # 数据清洗示例 categories = [] for category in data['result']['channelCategoryDetails']: for channel in category['channels']: clean_data = { 'id': channel['id'], 'name': channel['name'], 'desc': channel.get('shortDesc', '') } categories.append(clean_data) return categories3.2 课程详情爬取
采用分页爬取策略,每类课程单独处理:
def get_courses_by_category(category_id, max_pages=50): courses = [] url = "https://www.icourse163.org/web/j/mocSearchBean.searchCourseCardByChannelAndCategoryId.rpc" for page in range(1, max_pages+1): payload = { "mocCourseQueryVo": json.dumps({ "categoryId": -1, "categoryChannelId": category_id, "pageIndex": page, "pageSize": 20 }) } try: response = requests.post(url, data=payload, headers=headers) page_data = response.json() for item in page_data['result']['list']: course = item['mocCourseBaseCardVo'] courses.append({ 'course_id': course['id'], 'name': course['name'], 'school': course['schoolName'], 'teacher': course['teacherName'], 'enroll_count': course['enrollCount'] }) if page >= page_data['result']['query']['totlePageCount']: break except Exception as e: print(f"Error on page {page}: {str(e)}") return courses3.3 课程评价爬取
评价数据量最大,需要特别注意性能和异常处理:
def get_course_reviews(course_id): reviews = [] url = "https://www.icourse163.org/web/j/mocCourseV2RpcBean.getCourseEvaluatePaginationByCourseIdOrTermId.rpc" page = 1 while True: payload = { 'courseId': str(course_id), 'pageIndex': str(page), 'pageSize': '20', 'orderBy': '3' } try: response = requests.post(url, data=payload, headers=headers) data = response.json() for review in data['result']['list']: reviews.append({ 'id': review['id'], 'content': review['content'], 'user': review['userNickName'], 'rating': review.get('mark', 0), 'date': review['gmtModified'] }) if page >= data['result']['query']['totlePageCount']: break page += 1 time.sleep(0.5) # 礼貌性延迟 except Exception as e: print(f"Failed to get page {page}: {str(e)}") break return reviews4. 数据存储与清洗
获取原始数据后,我们需要进行结构化处理和持久化存储。这里推荐使用Pandas进行数据清洗,然后保存为多种格式。
数据清洗关键步骤:
- 处理缺失值:
df['rating'] = df['rating'].fillna(0)- 统一日期格式:
df['date'] = pd.to_datetime(df['date'], unit='ms')- 文本清洗:
import re df['content'] = df['content'].apply(lambda x: re.sub(r'\s+', ' ', x).strip())多格式存储方案:
| 格式 | 优点 | 适用场景 | 代码示例 |
|---|---|---|---|
| CSV | 通用性强 | 数据交换 | df.to_csv('reviews.csv') |
| Parquet | 列式存储 | 大数据分析 | df.to_parquet('reviews.parquet') |
| SQLite | 关系型 | 本地查询 | df.to_sql('reviews', con=engine) |
完整的数据处理流水线示例:
def process_data(raw_data): # 转换为DataFrame df = pd.DataFrame(raw_data) # 类型转换 df['date'] = pd.to_datetime(df['date'], unit='ms') df['rating'] = pd.to_numeric(df['rating']) # 文本处理 df['content'] = df['content'].str.strip() # 去重 df = df.drop_duplicates('id') return df5. 数据分析与可视化
获得清洗后的数据后,我们可以进行一些基础分析,挖掘数据价值。以下是几个典型分析方向:
课程评价词云生成:
from wordcloud import WordCloud import jieba text = ' '.join(df['content'].dropna()) wordlist = ' '.join(jieba.cut(text)) wc = WordCloud( font_path='simhei.ttf', background_color='white', max_words=200 ).generate(wordlist) plt.imshow(wc) plt.axis('off') plt.show()课程评分分布分析:
plt.figure(figsize=(10,6)) sns.countplot(x='rating', data=df) plt.title('评分分布') plt.xlabel('星级') plt.ylabel('数量') plt.show()热门课程识别:
top_courses = df.groupby('course_name')['rating'] \ .agg(['count', 'mean']) \ .sort_values('count', ascending=False) print(top_courses.head(10))6. 项目优化与扩展
基础爬虫完成后,我们可以从以下几个方向进行优化:
性能优化方案:
- 使用
aiohttp实现异步请求 - 采用Redis作为请求队列
- 实现断点续爬功能
反反爬策略:
- 动态User-Agent轮换
- 请求频率随机化
- 重要参数加密处理
数据分析扩展:
- 情感分析(使用SnowNLP或BERT)
- 用户行为模式挖掘
- 课程推荐系统构建
一个简单的异步爬虫改造示例:
import aiohttp import asyncio async def fetch(session, url, params): async with session.post(url, data=params) as response: return await response.json() async def main(): async with aiohttp.ClientSession() as session: tasks = [] for course_id in course_ids: task = fetch(session, API_URL, {'courseId': course_id}) tasks.append(task) results = await asyncio.gather(*tasks) # 处理结果...7. 完整数据集分享
经过两周的爬取和清洗,最终获得的数据集包含:
- 23个课程分类
- 29,196门课程基本信息
- 343,525条课程评价
数据集字段说明:
| 数据表 | 主要字段 | 记录数 |
|---|---|---|
| 课程分类 | id, name, desc | 23 |
| 课程信息 | id, name, school, teacher | 29,196 |
| 课程评价 | id, content, rating, date | 343,525 |
数据集下载链接已整理在项目仓库中,包含:
- 原始JSON数据
- 清洗后的CSV文件
- 数据分析示例Notebook
使用建议:
- 教育研究者:分析课程评价与教学质量关系
- 产品经理:了解用户真实需求和痛点
- 数据科学学习者:作为文本分析练习素材
在真实项目中,这套数据帮助我们发现了几个有趣的现象:
- 评分在4.2-4.5之间的课程最受欢迎
- 用户更关注教师授课风格而非课程内容难度
- 周末的评价情感倾向更为积极