1. 项目背景与核心价值
图书馆作为城市文化基础设施,每周都会举办大量公益讲座、读书会等活动。这些活动信息分散在各个分馆页面,手动收集效率极低。去年我在策划社区读书活动时,就曾因为信息不全错过了一场重要作家见面会。这个痛点促使我开发了这套自动化采集系统。
这套Python爬虫方案的核心价值在于:
- 实现全市20余家分馆活动信息的统一采集
- 每小时自动更新数据,确保信息时效性
- 结构化存储为CSV格式,可直接用于数据分析
- 代码完全开源,二次开发成本低
实测下来,原本需要3人天的手工收集工作,现在10分钟就能完成,且数据完整度从60%提升到98%。对于活动策划、文化研究等领域的工作者,这能节省大量信息收集时间。
2. 技术架构设计
2.1 整体技术栈选择
系统采用经典的三层架构:
采集层(Scrapy) → 处理层(Pandas) → 输出层(CSV/Excel)选择Scrapy而非Requests+BeautifulSoup组合,主要考虑:
- 内置的异步处理机制更适合多分馆并行采集
- 自动的请求去重和重试机制
- 完善的中间件扩展体系(后续添加代理池更方便)
# Scrapy项目创建示例 import scrapy class LibrarySpider(scrapy.Spider): name = 'library_spider' start_urls = ['http://library.gov.cn/branches']2.2 反爬应对策略
图书馆网站虽没有刻意防爬,但需要遵守的规则:
- 每个分馆页面添加3秒延迟
- User-Agent轮询(准备了6个常见浏览器UA)
- 异常状态码自动重试(配置在settings.py)
# settings.py关键配置 DOWNLOAD_DELAY = 3 RETRY_TIMES = 2 ROBOTSTXT_OBEY = True # 严格遵守robots协议3. 核心数据采集实现
3.1 页面解析方案
不同分馆页面结构差异较大,我们采用组合式解析策略:
统一信息模板(适用于80%分馆):
- 活动标题:
//div[@class="event-title"]/text() - 活动时间:
//span[@class="event-time"]/@data-timestamp - 活动地点:
normalize-space(//div[contains(@class,"location")])
- 活动标题:
定制解析器(针对特殊分馆):
- 朝阳分馆:采用CSS选择器提取
- 海淀分馆:需要处理JavaScript渲染数据
def parse(self, response): # 动态选择解析方式 if 'chaoyang' in response.url: yield self.parse_chaoyang(response) else: yield self.parse_standard(response)3.2 数据清洗要点
原始数据常见问题及处理方案:
| 问题类型 | 处理方式 | Pandas代码示例 |
|---|---|---|
| 时间格式混乱 | 统一转为ISO8601 | df['time'] = pd.to_datetime(df['raw_time']) |
| 地点缺失 | 使用分馆名称补全 | df['location'].fillna(df['branch'], inplace=True) |
| HTML标签残留 | 正则清洗 | df['title'] = df['title'].str.replace(r'<.*?>', '') |
4. 数据存储与导出
4.1 CSV导出优化
常规的to_csv()方法存在两个问题:
- 中文乱码
- 大文件写入慢
优化后的方案:
with open('events.csv', 'w', encoding='utf_8_sig', newline='') as f: # utf_8_sig解决Excel中文乱码 df.to_csv(f, index=False, chunksize=1000) # 分块写入4.2 自动邮件发送
添加邮件通知功能(需配置SMTP):
import smtplib from email.mime.text import MIMEText def send_report(): msg = MIMEText(f"新增{len(df)}条活动数据", 'plain', 'utf-8') msg['Subject'] = '图书馆活动数据更新' smtp = smtplib.SMTP_SSL('smtp.example.com') smtp.login('user@example.com', 'password') smtp.sendmail(from_addr, to_addrs, msg.as_string())5. 部署与调度
5.1 定时任务配置
使用APScheduler实现定时运行:
from apscheduler.schedulers.blocking import BlockingScheduler sched = BlockingScheduler() @sched.scheduled_job('interval', hours=1) def timed_job(): os.system('scrapy crawl library_spider') sched.start()5.2 日志监控方案
建议同时记录两种日志:
- Scrapy运行日志(调试用)
- 业务日志(记录每次采集数据量)
# 在pipeline中添加 class StatsPipeline: def process_item(self, item, spider): spider.logger.info(f"抓取成功:{item['title']}") spider.crawler.stats.inc_value('item_count')6. 常见问题排查
实际运行中遇到的典型问题:
突然抓取失败
- 检查网站改版(60%概率)
- 验证User-Agent是否被限制(30%概率)
- 确认网络连接(10%概率)
数据重复入库
- 添加MD5去重机制:
def get_md5(item): return hashlib.md5(str(item).encode()).hexdigest()内存泄漏
- 限制pipeline的item缓存数量
- 定期调用gc.collect()
重要提示:图书馆网站属于公共服务设施,爬取时务必控制频率,建议单IP请求间隔不低于3秒,避免对服务器造成负担。
这套系统经过三个月的迭代优化,目前稳定运行在本地文化中心的服务器上,每周自动生成活动周报。最大的收获是认识到:好的爬虫项目不仅要会"抓",更要会"养"——持续维护解析规则,及时适应网站改版,才能长期稳定获取数据。