1. 项目概述:城市图书馆活动采集系统
这个Python爬虫项目旨在构建一个自动化采集城市图书馆活动信息的系统。作为一名长期从事数据采集的开发者,我发现许多文化场所的活动信息分散在各个页面,缺乏统一的数据出口。这个系统正是为了解决这个问题而生——通过自动化手段抓取、解析并存储图书馆活动数据,最终输出结构化的CSV文件。
从技术角度看,这个项目涵盖了爬虫开发的完整链路:从请求发送、页面解析到数据清洗和存储。虽然被标记为"⭐"难度级别,但其中包含了许多值得新手学习的工程化思维,比如请求封装、异常处理和字段映射等实用技巧。
2. 合规性检查与爬虫伦理
2.1 遵守Robots协议
在开始编码前,我首先检查了目标网站的robots.txt文件。这是爬虫开发者的首要义务。通过Python的urllib.robotparser可以方便地进行检查:
python复制from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://library.example.com/robots.txt")
rp.read()
can_fetch = rp.can_fetch("*", "https://library.example.com/events")
2.2 请求频率控制
即使robots.txt允许爬取,也需要设置合理的请求间隔。我的经验法则是:
- 列表页:至少3秒间隔
- 详情页:至少5秒间隔
- 高峰期(9:00-18:00)适当延长间隔
实现方式:
python复制import time
import random
def polite_delay():
time.sleep(3 + random.random() * 2) # 3-5秒随机延迟
2.3 数据使用边界
采集到的数据仅用于:
- 个人学习与研究
- 非商业用途的数据分析
- 学术论文中的案例展示
绝对避免:
- 数据转售
- 用于商业推广
- 对原网站造成服务器压力
3. 技术选型与架构设计
3.1 核心工具栈
基于项目需求和开发效率,我选择了以下技术组合:
| 组件 | 工具 | 选择理由 |
|---|---|---|
| 请求库 | requests + retrying | 简单可靠,retrying提供自动重试 |
| 解析库 | lxml + cssselect | 比BeautifulSoup更快的解析速度 |
| 数据清洗 | pandas | 强大的数据清洗能力 |
| 存储 | csv + sqlite | 轻量级,适合小型项目 |
3.2 系统架构
整个系统采用分层设计,各模块职责分明:
code复制LibrarySpider/
├── fetcher/ # 请求层
├── parser/ # 解析层
├── cleaner/ # 数据清洗
├── storage/ # 存储层
└── main.py # 入口文件
这种架构的优势在于:
- 模块间耦合度低
- 便于单独测试每个组件
- 后续扩展其他图书馆网站时,只需替换parser即可
4. 环境准备与依赖安装
4.1 Python版本要求
推荐使用Python 3.8+,因为:
- 对类型提示支持更完善
- 异步IO性能更好
- 兼容主流爬虫库
创建虚拟环境:
bash复制python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
4.2 依赖安装
requirements.txt内容:
code复制requests==2.31.0
lxml==4.9.3
cssselect==1.2.0
pandas==2.0.3
python-dotenv==1.0.0
retrying==1.3.3
安装命令:
bash复制pip install -r requirements.txt
4.3 项目结构规范
建议遵循以下目录结构:
code复制project_root/
├── config/ # 配置文件
├── data/ # 原始数据
├── output/ # 处理后的CSV
├── logs/ # 运行日志
└── utils/ # 工具函数
5. 请求层实现细节
5.1 请求封装
基础请求函数需要考虑以下要素:
python复制from retrying import retry
import requests
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def fetch_page(url, headers=None, timeout=10):
"""
带重试机制的请求函数
:param url: 目标URL
:param headers: 自定义请求头
:param timeout: 超时时间(秒)
:return: 响应文本
"""
default_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
final_headers = {**default_headers, **(headers or {})}
try:
response = requests.get(url, headers=final_headers, timeout=timeout)
response.raise_for_status()
return response.text
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
raise
5.2 关键设计考量
- User-Agent轮换:准备多个常见浏览器的UA,避免单一标识
- 自动重试:对5xx错误和网络波动自动重试
- 超时控制:防止长时间无响应阻塞程序
- 请求间隔:在关键请求之间添加延迟
6. 解析层实现方案
6.1 活动列表解析
典型的图书馆活动列表页解析流程:
python复制from lxml import html
def parse_event_list(html_content):
"""
解析活动列表页
:param html_content: HTML文本
:return: 活动字典列表
"""
tree = html.fromstring(html_content)
events = []
# 使用CSS选择器定位活动条目
for item in tree.cssselect('.event-list .event-item'):
try:
event = {
'title': item.cssselect('.title')[0].text.strip(),
'date': item.cssselect('.date')[0].text.strip(),
'location': item.cssselect('.location')[0].text.strip(),
'detail_url': item.cssselect('a.detail-link')[0].get('href')
}
events.append(event)
except (IndexError, AttributeError) as e:
print(f"解析异常: {e}")
continue
return events
6.2 容错设计要点
- 选择器备用方案:准备多套选择器,主选择器失效时尝试备用方案
- 异常捕获:对可能缺失的字段进行try-catch处理
- 数据校验:检查关键字段是否为空或格式异常
- 日志记录:记录解析失败的条目,便于后续分析
7. 数据清洗策略
7.1 常见清洗场景
图书馆活动数据通常需要以下清洗:
| 原始数据问题 | 清洗方案 | 示例代码 |
|---|---|---|
| 日期格式不统一 | 统一转为YYYY-MM-DD | pd.to_datetime() |
| 地点信息冗余 | 提取关键信息 | str.extract() |
| HTML标签残留 | 去除标签 | BeautifulSoup.get_text() |
| 重复数据 | 基于URL去重 | drop_duplicates() |
7.2 清洗流水线示例
python复制import pandas as pd
from datetime import datetime
def clean_events(raw_events):
"""
数据清洗流水线
:param raw_events: 原始数据列表
:return: 清洗后的DataFrame
"""
df = pd.DataFrame(raw_events)
# 日期标准化
df['date'] = pd.to_datetime(df['date'], errors='coerce')
# 地点清洗
df['location'] = df['location'].str.replace(r'\(.*?\)', '', regex=True)
# URL补全
base_url = "https://library.example.com"
df['detail_url'] = df['detail_url'].apply(
lambda x: x if x.startswith('http') else base_url + x
)
return df.dropna() # 删除无效行
8. 存储层实现
8.1 CSV导出方案
python复制def save_to_csv(dataframe, filename):
"""
保存数据到CSV
:param dataframe: 清洗后的数据
:param filename: 输出文件名
"""
output_path = f"output/{datetime.now().strftime('%Y%m%d')}_{filename}"
dataframe.to_csv(output_path, index=False, encoding='utf-8-sig')
print(f"数据已保存至 {output_path}")
8.2 字段映射表
设计合理的CSV字段结构:
| 字段名 | 类型 | 说明 |
|---|---|---|
| event_id | str | 活动唯一标识 |
| title | str | 活动标题 |
| start_date | date | 开始日期 |
| end_date | date | 结束日期 |
| location | str | 活动地点 |
| category | str | 活动类型 |
| target_audience | str | 目标受众 |
| registration_required | bool | 是否需要报名 |
| detail_url | str | 详情页链接 |
9. 主程序集成
9.1 运行流程
python复制def main():
# 1. 抓取列表页
list_url = "https://library.example.com/events"
html_content = fetch_page(list_url)
# 2. 解析活动列表
raw_events = parse_event_list(html_content)
# 3. 清洗数据
cleaned_events = clean_events(raw_events)
# 4. 存储结果
save_to_csv(cleaned_events, "library_events.csv")
if __name__ == "__main__":
main()
9.2 结果示例
运行后生成的CSV文件内容示例:
code复制title,start_date,end_date,location
"儿童故事会",2023-11-15,2023-11-15,"少儿阅览室"
"编程工作坊",2023-11-18,2023-11-19,"电子阅览室"
10. 常见问题排查
10.1 HTTP 403/429错误
现象:请求被拒绝或收到429 Too Many Requests
解决方案:
- 检查User-Agent是否有效
- 增加请求间隔时间
- 添加Referer头模拟浏览器行为
- 使用代理IP轮换(谨慎使用)
10.2 动态渲染内容缺失
现象:HTML中找不到预期的数据
可能原因:
- 数据通过AJAX加载
- 需要执行JavaScript
解决方案:
- 分析XHR请求,直接调用API接口
- 使用Selenium或Playwright等浏览器自动化工具
10.3 编码问题
现象:中文显示为乱码
解决方案:
python复制# 强制指定编码
response.encoding = response.apparent_encoding
# 或使用chardet检测
import chardet
encoding = chardet.detect(response.content)['encoding']
11. 进阶优化方向
11.1 并发采集优化
使用线程池提高采集效率:
python复制from concurrent.futures import ThreadPoolExecutor
def fetch_multiple_pages(urls):
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(fetch_page, urls))
return results
注意事项:
- 控制并发数量(通常3-5个线程)
- 确保目标服务器能承受压力
- 添加更长的延迟防止封禁
11.2 断点续爬
实现思路:
- 将已采集的URL存入SQLite
- 每次启动时检查哪些URL已经处理
- 只采集新增的URL
python复制import sqlite3
def init_resume_db():
conn = sqlite3.connect('progress.db')
conn.execute('''CREATE TABLE IF NOT EXISTS crawled_urls
(url TEXT PRIMARY KEY)''')
return conn
11.3 日志监控
添加详细日志记录:
python复制import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('spider.log'),
logging.StreamHandler()
]
)
12. 实战经验分享
在开发这个图书馆活动采集系统的过程中,我总结了以下几点关键经验:
-
选择器稳定性:不要依赖复杂的XPath路径,尽量使用class或id等稳定属性。我遇到过因为网站改版导致整个采集失效的情况,后来改用更宽松的选择器配合数据校验,鲁棒性大大提升。
-
时间处理陷阱:图书馆网站的时间格式五花八门,有的用"11月15日",有的用"15/11/2023"。最终我统一使用dateparser库处理各种格式:
python复制import dateparser
dateparser.parse("下周三下午2点") # 自动识别相对时间
-
反爬应对策略:当发现请求频繁被拒时,我采用了以下组合方案:
- 轮换多个User-Agent
- 随机延迟1-10秒
- 使用住宅代理IP(仅限必要情况)
- 模拟浏览器行为(添加Accept、Referer等头)
-
数据质量监控:添加了自动数据校验环节,检查:
- 必填字段是否为空
- 日期是否在合理范围内
- URL是否有效
- 地点信息是否包含图书馆名称
这个项目虽然被标记为初级难度,但完整实现下来,涉及的知识点相当全面。对于想要学习Python爬虫的开发者来说,是一个很好的练手项目。后续可以继续扩展的功能包括:
- 添加邮件通知功能,当有新活动时自动发送通知
- 开发简单的Web界面展示采集结果
- 对接日历API,将活动导入个人日历
最重要的是,在开发过程中要始终牢记爬虫伦理,尊重网站的服务条款,控制采集频率,做一个负责任的网络公民。