1. 项目概述:在线课程平台数据采集与分析
这个项目源于我对在线教育行业数据价值的长期观察。作为从业者,我发现课程平台的公开数据中蕴含着大量未被充分挖掘的信息——不同领域的课程热度变化、价格波动规律、用户评价趋势等,这些数据对于教育从业者、内容创作者和学习者都具有重要参考意义。
项目采用Python技术栈构建完整的数据采集与分析管道,核心目标包括:
- 多维度采集课程基础信息(标题、价格、评分等)
- 实现跨学科课程数据的结构化存储
- 建立评分与价格关联分析模型
- 追踪课程热度随时间的变化趋势
技术选型上,我选择了轻量级但功能完备的组合:
- 采集层:requests + lxml/BeautifulSoup
- 存储层:SQLite(适合中小规模数据集)
- 分析层:pandas + matplotlib
- 调度层:原生ThreadPoolExecutor(避免过度设计)
提示:项目代码完全遵循MIT开源协议,但需特别注意数据使用应符合目标平台的robots.txt规定。我在开发过程中将请求频率严格控制在人类浏览速度范围内(约2-3秒/请求)。
2. 技术架构与核心设计
2.1 系统分层设计
整个系统采用经典的四层架构:
code复制[采集层] → [解析层] → [存储层] → [分析层]
↑ ↑ ↑
[反爬对策] [异常处理] [数据清洗]
2.1.1 采集层关键实现
请求封装采用装饰器模式增强健壮性:
python复制def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except (RequestException, Timeout) as e:
if attempt == max_attempts - 1:
raise
time.sleep(delay * (attempt + 1))
return wrapper
return decorator
@retry(max_attempts=5, delay=2)
def fetch_page(url, headers=None):
"""带自动重试机制的请求函数"""
session = requests.Session()
response = session.get(
url,
headers=headers or DEFAULT_HEADERS,
timeout=10
)
response.raise_for_status()
return response
2.1.2 解析层策略
根据目标网站特点采用混合解析方案:
- 列表页:优先使用lxml(XPath性能优势)
- 详情页:BeautifulSoup(HTML容错性更好)
- API接口:直接json解析
python复制def parse_course_list(html):
"""使用lxml解析课程列表页"""
tree = html.fromstring(html)
courses = []
for item in tree.xpath('//div[@class="course-item"]'):
course = {
'title': item.xpath('.//h3/text()')[0].strip(),
'url': urljoin(BASE_URL, item.xpath('./a/@href')[0]),
'price': float(item.xpath('.//span[@class="price"]/text()')[0][1:]),
'students': int(re.sub(r'\D', '', item.xpath('.//span[@class="enroll"]/text()')[0]))
}
courses.append(course)
return courses
2.2 数据存储设计
使用SQLite作为存储后端,表结构设计考虑分析需求:
sql复制CREATE TABLE courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
category TEXT NOT NULL,
price REAL,
rating REAL,
students INTEGER,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE course_stats (
course_id INTEGER REFERENCES courses(id),
date TEXT NOT NULL,
rating REAL,
students INTEGER,
PRIMARY KEY (course_id, date)
);
注意:created_at和updated_at字段采用ISO8601格式(YYYY-MM-DD HH:MM:SS),便于后续时间序列分析
3. 核心实现细节
3.1 反爬虫对策实践
3.1.1 请求头管理
构建动态User-Agent池:
python复制USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...',
# 10+ 其他常见UA
]
def get_random_headers():
return {
'User-Agent': random.choice(USER_AGENTS),
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://www.example.com/'
}
3.1.2 请求频率控制
实现自适应延迟机制:
python复制class RequestThrottler:
def __init__(self, base_delay=2.0, max_delay=10.0):
self.base_delay = base_delay
self.max_delay = max_delay
self.last_request = 0
def wait(self):
elapsed = time.time() - self.last_request
wait_time = max(0, self.base_delay - elapsed)
if wait_time > 0:
time.sleep(wait_time)
self.last_request = time.time()
throttler = RequestThrottler()
def fetch_with_throttle(url):
throttler.wait()
return fetch_page(url)
3.2 数据解析进阶技巧
3.2.1 动态数据提取
处理JavaScript渲染内容:
python复制def extract_dynamic_data(html):
"""从script标签中提取JSON数据"""
script_content = re.search(
r'<script type="application/json" id="__NEXT_DATA__">(.*?)</script>',
html,
re.DOTALL
)
if script_content:
return json.loads(script_content.group(1))
return None
3.2.2 评分标准化处理
不同平台的评分体系转换:
python复制def normalize_rating(raw_rating, scale=5.0):
"""
将不同评分标准统一到0-5分制
:param raw_rating: 原始评分(可能是10分制、百分制等)
:param scale: 原始评分最大值
:return: 标准化后的评分(5分制)
"""
base_rating = float(raw_rating)
return round((base_rating / scale) * 5, 1)
4. 数据分析与可视化
4.1 价格-评分相关性分析
使用pandas进行统计计算:
python复制def analyze_price_rating(df):
"""分析价格与评分的相关性"""
# 数据清洗
clean_df = df[(df['price'] > 0) & (df['rating'] > 0)].copy()
# 价格分段
bins = [0, 50, 100, 200, 500, float('inf')]
labels = ['<50', '50-100', '100-200', '200-500', '500+']
clean_df['price_group'] = pd.cut(clean_df['price'], bins=bins, labels=labels)
# 分组统计
result = clean_df.groupby('price_group').agg({
'rating': ['mean', 'count'],
'students': 'sum'
})
return result.sort_index()
4.2 学习趋势可视化
使用matplotlib绘制时间序列:
python复制def plot_trend(course_id, days=30):
"""绘制单门课程的学习趋势图"""
query = """
SELECT date, students, rating
FROM course_stats
WHERE course_id = ?
ORDER BY date DESC
LIMIT ?
"""
data = pd.read_sql(query, conn, params=(course_id, days))
fig, ax1 = plt.subplots(figsize=(12, 6))
# 学生数量曲线
color = 'tab:blue'
ax1.set_xlabel('Date')
ax1.set_ylabel('Students', color=color)
ax1.plot(data['date'], data['students'], color=color, marker='o')
ax1.tick_params(axis='y', labelcolor=color)
# 评分曲线
ax2 = ax1.twinx()
color = 'tab:red'
ax2.set_ylabel('Rating', color=color)
ax2.plot(data['date'], data['rating'], color=color, marker='x')
ax2.tick_params(axis='y', labelcolor=color)
plt.title(f'Course Trend (Last {days} Days)')
fig.tight_layout()
return fig
5. 工程化实践与优化
5.1 断点续爬实现
基于SQLite的状态管理:
python复制class CrawlState:
def __init__(self, db_file='crawl_state.db'):
self.conn = sqlite3.connect(db_file)
self._init_db()
def _init_db(self):
self.conn.execute('''
CREATE TABLE IF NOT EXISTS crawl_state (
url TEXT PRIMARY KEY,
status TEXT CHECK(status IN ('pending', 'completed', 'failed')),
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
def mark_completed(self, url):
self.conn.execute('''
INSERT OR REPLACE INTO crawl_state (url, status)
VALUES (?, 'completed')
''', (url,))
self.conn.commit()
def get_pending_urls(self, all_urls):
cursor = self.conn.execute('''
SELECT url FROM crawl_state WHERE status = 'completed'
''')
completed = {row[0] for row in cursor}
return [url for url in all_urls if url not in completed]
5.2 并发爬取优化
使用ThreadPoolExecutor实现可控并发:
python复制def concurrent_crawl(urls, workers=4):
"""并发爬取实现"""
with ThreadPoolExecutor(max_workers=workers) as executor:
future_to_url = {
executor.submit(fetch_with_throttle, url): url
for url in urls
}
results = []
for future in as_completed(future_to_url):
url = future_to_url[future]
try:
response = future.result()
results.append((url, response))
state.mark_completed(url)
except Exception as e:
print(f'{url} failed: {str(e)}')
return results
6. 实战经验与避坑指南
6.1 常见问题解决方案
6.1.1 页面结构变更应对
建议实现自动检测机制:
python复制def validate_parser(html, xpath):
"""验证XPath是否仍然有效"""
tree = html.fromstring(html)
try:
result = tree.xpath(xpath)
return len(result) > 0
except:
return False
# 使用示例
if not validate_parser(html, '//div[@class="course-item"]'):
send_alert('XPath可能已失效')
6.1.2 数据质量检查
入库前数据验证:
python复制def validate_course_data(course):
"""验证课程数据完整性"""
checks = [
('title', str),
('price', (int, float)),
('rating', (int, float)),
('students', int)
]
errors = []
for field, types in checks:
value = course.get(field)
if not isinstance(value, types):
errors.append(f'Invalid type for {field}: {type(value)}')
elif field == 'rating' and not (0 <= value <= 5):
errors.append(f'Rating out of range: {value}')
return errors if errors else None
6.2 性能优化技巧
6.2.1 数据库批量写入
使用executemany提升写入性能:
python复制def batch_insert_courses(courses):
"""批量插入课程数据"""
sql = '''
INSERT OR IGNORE INTO courses
(title, category, price, rating, students)
VALUES (?, ?, ?, ?, ?)
'''
data = [
(c['title'], c['category'], c['price'], c['rating'], c['students'])
for c in courses
]
conn.executemany(sql, data)
conn.commit()
6.2.2 内存优化
使用生成器处理大数据集:
python复制def stream_parse_courses(html_generator):
"""流式解析课程数据"""
for html in html_generator:
yield parse_course_list(html)
7. 项目扩展方向
7.1 数据维度扩展
建议增加采集的字段:
- 课程大纲/章节信息
- 教师背景资料
- 用户评价文本
- 课程更新时间线
7.2 技术架构升级
当数据量增大时可考虑:
- 存储层:迁移到PostgreSQL/MySQL
- 采集层:引入Scrapy框架
- 调度层:使用Celery分布式任务队列
- 分析层:集成Jupyter Notebook
7.3 商业分析应用
可构建的分析模型:
- 价格弹性分析
- 课程推荐系统
- 热门领域预测
- 竞品对比分析
在实际部署这个系统时,我发现几个值得注意的经验点:首先,定期(如每周)验证解析规则的有效性可以大幅减少后期维护成本;其次,在数据库设计阶段就考虑好分析需求,能避免后续繁琐的数据转换;最重要的是,保持适度的采集频率不仅是法律要求,长期来看反而能获得更完整、更有价值的时间序列数据。