1. 项目背景与需求分析
医疗行业的临床诊疗指南是医生日常工作中的重要参考依据。这些指南通常由权威医学组织发布,包含疾病诊断标准、治疗方案推荐等内容。然而在实际工作中,我发现很多医院的指南文档分散在各个科室的电脑里,缺乏统一管理和更新机制。
更麻烦的是,像中华医学会这类官网上的指南目录页,往往采用归档型页面设计——内容按年份和科室分类,没有提供全文搜索功能。当需要查找某个特定疾病的指南时,医生们不得不手动翻找几十个页面,效率极低。
这就是为什么我们需要建立一个指南索引库。通过Python爬虫技术,我们可以:
- 自动抓取官网的指南目录页
- 提取标题、发布时间、科室分类等关键信息
- 构建本地可搜索的数据库
- 为后续的全文抓取建立基础
2. 技术选型与环境准备
2.1 为什么选择Python做医疗数据爬虫
Python在医疗数据抓取领域有独特优势:
- 丰富的库生态系统(Requests、BeautifulSoup等)
- 对非结构化数据处理能力强
- 容易实现自动化流程
- 社区支持完善,遇到问题容易找到解决方案
对比其他语言:
- Java虽然稳定但代码冗长
- Node.js异步性能好但错误处理复杂
- Go语言并发能力强但学习曲线陡峭
2.2 开发环境配置
推荐使用VSCode作为开发环境,配置步骤如下:
- 安装Python 3.8+(太新的版本可能遇到库兼容问题)
- 配置虚拟环境:
bash复制python -m venv guide_spider
source guide_spider/bin/activate # Linux/Mac
guide_spider\Scripts\activate # Windows
- 安装必要库:
bash复制pip install requests beautifulsoup4 pandas sqlalchemy tqdm
注意:医疗网站通常对爬虫比较敏感,建议在虚拟环境中测试,避免影响医院网络正常使用。
3. 网站分析与爬虫设计
3.1 目标网站结构解析
以中华医学会某子站为例,其指南目录页通常具有以下特征:
- URL模式:
/guide/year/科室.html - 分页采用
?page=2形式 - 每条指南的HTML结构:
html复制<div class="guide-item">
<h3><a href="/detail/123.html">标题</a></h3>
<span class="date">2023-05-01</span>
<span class="department">心血管科</span>
</div>
3.2 爬虫架构设计
采用分层设计模式:
- 调度层:控制爬取顺序和频率
- 下载层:处理HTTP请求
- 解析层:提取结构化数据
- 存储层:保存到数据库
- 监控层:记录日志和异常
python复制class GuideSpider:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36'
})
def crawl(self, start_url):
# 实现爬取逻辑
pass
4. 核心爬取逻辑实现
4.1 页面下载与异常处理
医疗网站通常有反爬机制,需要特别注意:
python复制def download_page(url, retry=3):
for i in range(retry):
try:
resp = self.session.get(url, timeout=10)
resp.raise_for_status()
# 检查是否被重定向到验证页
if 'verify' in resp.url:
raise Exception('Triggered anti-spider')
return resp.text
except Exception as e:
print(f"Attempt {i+1} failed: {str(e)}")
time.sleep(2**i) # 指数退避
return None
4.2 数据解析技巧
使用BeautifulSoup时,医疗网页常有这些特殊情况需要处理:
- 日期格式不统一:
python复制from dateutil.parser import parse
def parse_date(date_str):
try:
return parse(date_str).strftime('%Y-%m-%d')
except:
return None # 对无法解析的日期留空
- 科室名称规范化:
python复制DEPARTMENT_MAP = {
'心内': '心血管内科',
'呼吸': '呼吸内科',
# ...
}
def normalize_department(raw):
return DEPARTMENT_MAP.get(raw.strip(), raw)
5. 数据存储方案
5.1 数据库设计
考虑到医疗数据的特殊性,建议使用SQLite+全文搜索扩展:
python复制from sqlalchemy import create_engine, Column, Integer, String, Text
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class ClinicalGuide(Base):
__tablename__ = 'guides'
id = Column(Integer, primary_key=True)
title = Column(String(200), index=True)
publish_date = Column(String(10))
department = Column(String(50))
url = Column(String(255))
abstract = Column(Text)
# 启用全文搜索
__table_args__ = {
'sqlite_autoincrement': True,
'extend_existing': True
}
5.2 批量插入优化
医疗数据量可能很大,使用批量插入提高效率:
python复制def batch_insert(session, items, batch_size=100):
for i in range(0, len(items), batch_size):
batch = items[i:i+batch_size]
session.bulk_save_objects([
ClinicalGuide(**item) for item in batch
])
session.commit()
6. 反爬应对策略
6.1 医疗网站特有的反爬机制
- 请求频率限制:通常设置为每分钟10-20次
- 用户行为检测:如鼠标移动轨迹
- 验证码触发条件:连续访问5-10个页面
6.2 合规爬取方案
- 遵守robots.txt规则:
python复制import urllib.robotparser
rp = urllib.robotparser.RobotFileParser()
rp.set_url("https://www.example.com/robots.txt")
rp.read()
can_fetch = rp.can_fetch("*", "/guide/")
- 添加合理的请求间隔:
python复制import random
def random_delay():
time.sleep(random.uniform(1.5, 3.5))
- 使用医院IP白名单(如有条件)
7. 完整爬虫示例代码
python复制import requests
from bs4 import BeautifulSoup
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import time
import random
from tqdm import tqdm
class ClinicalGuideSpider:
def __init__(self, db_path='clinical_guides.db'):
self.engine = create_engine(f'sqlite:///{db_path}')
self.Session = sessionmaker(bind=self.engine)
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Medical Research Bot/1.0 (+http://example.com)'
})
def create_tables(self):
Base.metadata.create_all(self.engine)
def crawl_department(self, department, years=range(2015, 2024)):
base_url = f"https://example.com/guides/{department}"
data = []
for year in tqdm(years, desc=f"Crawling {department}"):
page = 1
while True:
url = f"{base_url}/{year}?page={page}"
html = self.download_page(url)
if not html:
break
items = self.parse_page(html)
if not items:
break
data.extend(items)
page += 1
self.random_delay()
return data
def download_page(self, url):
try:
resp = self.session.get(url, timeout=15)
resp.raise_for_status()
return resp.text
except Exception as e:
print(f"Failed to download {url}: {str(e)}")
return None
def parse_page(self, html):
soup = BeautifulSoup(html, 'html.parser')
items = []
for item in soup.select('.guide-item'):
title = item.select_one('h3 a').text.strip()
date = self.parse_date(item.select_one('.date').text)
url = item.select_one('h3 a')['href']
items.append({
'title': title,
'publish_date': date,
'department': self.normalize_department(
item.select_one('.department').text
),
'url': f"https://example.com{url}",
'abstract': '' # 后续可补充
})
return items
def random_delay(self):
time.sleep(random.uniform(2, 5))
# 其他工具方法同上...
8. 项目扩展与优化方向
8.1 增量爬取实现
医疗指南会持续更新,需要记录已爬取的URL:
python复制class CrawlState:
def __init__(self, state_file='crawl_state.json'):
self.state_file = state_file
self.state = self._load_state()
def _load_state(self):
try:
with open(self.state_file) as f:
return json.load(f)
except:
return {'last_crawl': None, 'crawled_urls': []}
def is_crawled(self, url):
return url in self.state['crawled_urls']
def mark_crawled(self, url):
if not self.is_crawled(url):
self.state['crawled_urls'].append(url)
self._save_state()
def _save_state(self):
with open(self.state_file, 'w') as f:
json.dump(self.state, f)
8.2 自动摘要生成
使用NLP技术为指南生成摘要:
python复制from sumy.parsers.html import HtmlParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lsa import LsaSummarizer
def generate_abstract(url, sentences_count=3):
try:
parser = HtmlParser.from_url(url, Tokenizer("chinese"))
summarizer = LsaSummarizer()
summary = summarizer(parser.document, sentences_count)
return " ".join(str(s) for s in summary)
except:
return ""
我在实际医疗数据爬取中发现几个关键经验:
- 医院网站周末维护少,但工作日上午访问量大会变慢
- 科室名称在不同医院可能有不同叫法,需要建立映射表
- 指南PDF文件的下载链接经常变更,需要定期检查
- 最好在本地保存HTML快照,便于后续复查
