在医疗数据爆炸的时代,药品信息库已成为医生、药师和患者的重要参考。丁香园作为国内领先的医疗服务平台,其药品数据库包含药品成分、用法用量、禁忌症等关键信息。本文将以实战为导向,用通俗易懂的方式讲解如何用Python爬虫抓取这些数据,并解决反爬虫、数据清洗等核心问题。
一、技术选型:为什么选择这些工具?
1. 核心工具包
- Requests:发送HTTP请求的瑞士军刀,支持会话保持和代理设置。
- Lxml:比BeautifulSoup快5倍的HTML解析器,支持XPath表达式精准定位数据。
- Pandas:数据处理的瑞士军刀,支持CSV/Excel导出和清洗。
- ProxyPool:开源代理池管理工具,自动维护高可用代理IP。
2. 为什么不用Scrapy?
对于医疗数据这种结构化页面,Scrapy的分布式架构反而显得笨重。我们采用轻量级方案:Requests获取页面 → Lxml解析 → Pandas存储,30行代码即可完成核心功能。
二、实战步骤:从0到1抓取药品数据
1. 页面结构分析
以丁香园药品库的"阿莫西林胶囊"页面为例,关键数据分布在:
- 药品名称:
<h1 class="drug-name"> - 成分:
<div class="ingredient"> - 适应症:
<div class="indication"> - 用法用量:
<div class="dosage">
通过浏览器开发者工具(F12)查看元素,发现所有数据都在<div class="drug-detail">容器内。
2. 基础爬虫代码
import requests
from lxml import etree
import pandas as pd
def fetch_drug_data(drug_url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
try:
response = requests.get(drug_url, headers=headers, timeout=10)
response.raise_for_status()
html = etree.HTML(response.text)
# 提取数据
name = html.xpath('//h1[@class="drug-name"]/text()')[0].strip()
ingredient = html.xpath('//div[@class="ingredient"]//text()')
ingredient = ''.join([i.strip() for i in ingredient if i.strip()])
# 其他字段提取类似...
return {
'药品名称': name,
'成分': ingredient,
# 其他字段...
}
except Exception as e:
print(f"抓取失败: {e}")
return None
# 示例调用
drug_url = "https://drugs.dxy.cn/drug/123456.htm"
data = fetch_drug_data(drug_url)
if data:
df = pd.DataFrame([data])
df.to_csv('drug_data.csv', index=False, encoding='utf_8_sig')
3. 关键技巧解析
- XPath定位:
//div[@class="ingredient"]//text()表示选取所有class为"ingredient"的div下的文本节点 - 文本清洗:使用列表推导式去除空白字符
- 异常处理:捕获网络请求和解析异常,避免程序中断
三、反爬虫攻防战:如何突破限制?
1. 丁香园的反爬机制
- IP频率限制:同一IP每分钟请求超过10次即触发验证
- 行为指纹:通过Canvas指纹、WebGL指纹识别爬虫
- 动态加载:部分数据通过AJAX异步加载
2. 破解方案
方案1:代理IP轮换
from proxypool import ProxyPool
pool = ProxyPool() # 初始化代理池
def fetch_with_proxy(url):
proxy = pool.get_proxy() # 获取代理
proxies = {
'http': f'http://{proxy}',
'https': f'https://{proxy}'
}
try:
response = requests.get(url, proxies=proxies, timeout=10)
if response.status_code == 200:
return response.text
else:
pool.mark_invalid(proxy) # 标记无效代理
return fetch_with_proxy(url) # 递归重试
except:
pool.mark_invalid(proxy)
return fetch_with_proxy(url)
方案2:请求头伪装
def get_random_headers():
return {
'User-Agent': random.choice([
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15...'
]),
'Accept-Language': random.choice(['zh-CN,zh;q=0.9', 'en-US,en;q=0.8']),
'Referer': 'https://drugs.dxy.cn/'
}
方案3:动态延迟控制
import time
import random
def fetch_with_delay(url):
delay = random.uniform(1, 3) # 随机延迟1-3秒
time.sleep(delay)
return fetch_drug_data(url) # 使用基础爬虫函数
四、数据清洗与存储:让数据可用
1. 常见数据问题
- HTML标签残留:如
<p>用法用量:</p><p>口服,一次2粒</p> - 单位不统一:如"5mg"和"0.005g"
- 缺失值处理:部分药品缺少"禁忌症"字段
2. 清洗方案
def clean_data(raw_data):
# 去除HTML标签
from bs4 import BeautifulSoup
soup = BeautifulSoup(raw_data['适应症'], 'html.parser')
clean_text = soup.get_text(strip=True)
# 单位统一
if 'mg' in raw_data['剂量']:
raw_data['剂量_g'] = float(raw_data['剂量'].replace('mg', '')) / 1000
# 填充缺失值
raw_data['禁忌症'] = raw_data.get('禁忌症', '未提及')
return raw_data
3. 存储方案对比
| 存储方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| CSV | 小规模数据 | 通用性强 | 不支持复杂查询 |
| SQLite | 中等规模 | 无需服务器 | 并发性能有限 |
| MongoDB | 大规模数据 | 灵活Schema | 占用空间较大 |
五、完整项目代码(精简版)
import requests
from lxml import etree
import pandas as pd
import random
import time
from proxypool import ProxyPool
class DrugSpider:
def __init__(self):
self.pool = ProxyPool()
self.base_url = "https://drugs.dxy.cn/drug/{}.htm"
def get_proxy(self):
return self.pool.get_proxy()
def fetch_page(self, drug_id):
url = self.base_url.format(drug_id)
proxy = self.get_proxy()
proxies = {'http': f'http://{proxy}', 'https': f'https://{proxy}'}
try:
headers = self.get_random_headers()
time.sleep(random.uniform(1, 3))
response = requests.get(url, headers=headers, proxies=proxies, timeout=10)
response.raise_for_status()
return response.text
except Exception as e:
print(f"抓取失败: {e}")
return None
def parse_page(self, html):
if not html:
return None
html = etree.HTML(html)
data = {
'名称': html.xpath('//h1[@class="drug-name"]/text()')[0].strip(),
'成分': ''.join([i.strip() for i in html.xpath('//div[@class="ingredient"]//text()') if i.strip()]),
# 其他字段解析...
}
return data
def run(self, drug_ids):
results = []
for drug_id in drug_ids:
html = self.fetch_page(drug_id)
data = self.parse_page(html)
if data:
results.append(data)
df = pd.DataFrame(results)
df.to_csv('drug_data.csv', index=False, encoding='utf_8_sig')
print(f"成功抓取{len(results)}条药品数据")
# 使用示例
spider = DrugSpider()
drug_ids = ['123456', '654321', '789012'] # 实际应从列表获取
spider.run(drug_ids)
六、常见问题Q&A
Q1:被网站封IP怎么办?
A:立即启用备用代理池,建议使用住宅代理(如站大爷IP代理),配合每请求更换IP策略。对于大规模采集,可采用:
- 混合使用数据中心代理和住宅代理
- 设置请求间隔为3-10秒随机值
- 实现代理健康度监测,自动剔除失效代理
Q2:如何获取药品ID列表?
A:可通过以下方式获取:
- 丁香园药品分类页面的分页链接(如
https://drugs.dxy.cn/search?page=2) - 搜索接口API(如
https://drugs.dxy.cn/api/search?keyword=抗生素) - 已有药品数据库的交叉验证
Q3:数据抓取频率应该设置多少?
A:建议遵循以下原则:
- 测试期:每10-30秒/请求
- 正式采集:每3-10秒/请求(根据目标网站规模调整)
- 关键时期:启用分布式爬虫,每个IP分配不同延迟
Q4:如何处理动态加载的数据?
A:两种方案:
-
分析AJAX请求:通过浏览器开发者工具的Network面板,找到数据接口直接请求
-
Selenium模拟浏览器:适用于复杂JavaScript渲染的页面
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument('--headless') # 无头模式 driver = webdriver.Chrome(options=options) driver.get("https://drugs.dxy.cn/drug/123456.htm") html = driver.page_source driver.quit()
Q5:如何避免法律风险?
A:必须遵守:
- 检查目标网站的robots.txt文件(如
https://drugs.dxy.cn/robots.txt) - 控制采集频率,避免对服务器造成负担
- 不采集用户隐私数据(如患者信息)
- 仅用于个人学习研究,商业用途需获得授权
结语
通过本文的实战讲解,你已掌握医疗爬虫的核心技术:从页面解析到反爬虫应对,从数据清洗到存储优化。实际项目中,建议结合具体需求调整策略,例如:
- 医疗研究:重点关注药品相互作用、不良反应等字段
- 价格监控:需定期抓取并对比不同渠道价格
- 药品对比:需要标准化单位并建立映射关系
记住:技术只是手段,合规才是根本。在享受数据红利的同时,务必遵守相关法律法规,让爬虫技术真正服务于医疗健康事业。