1. 农作物病虫害图谱采集项目背景与价值
去年参与某农业科研机构的数据建设项目时,遇到一个典型需求:需要系统化采集国内主要农作物病虫害图谱数据,用于构建智能识别系统。这类专业数据通常分散在各个农业信息网站,手动收集不仅效率低下,而且难以保证数据格式的统一性。这正是Python爬虫技术大显身手的场景。
农作物病虫害数据采集的特殊性在于:
- 数据呈现典型的"目录-详情"二级结构
- 图片资源占比高且需要保持原分辨率
- 不同网站的反爬策略差异显著
- 需要保持农业术语的准确性
通过二段式采集方案(先抓目录页再抓详情页),我们最终完成了32种主粮作物、156种常见病虫害的数据采集,图片数据量超过8TB。这个过程中积累的实战经验,正是本文要分享的核心内容。
2. 技术选型与环境配置
2.1 核心工具栈选择
经过多个项目的对比测试,我固定使用以下工具组合:
python复制# 基础请求库
requests==2.28.1 # 比urllib3更友好的API设计
requests-html==0.10.0 # 内置PyQuery和异步支持
# 解析工具
bs4==4.11.1 # 容错性最好的HTML解析器
lxml==4.9.1 # XPath解析必备
# 浏览器自动化
selenium==4.1.0 # 处理动态渲染页面
webdriver_manager==3.8.3 # 自动管理浏览器驱动
# 存储方案
pymongo==4.2.0 # 非结构化数据存储
Pillow==9.2.0 # 图片元数据处理
特别提示:农业类网站普遍使用老旧CMS系统,BeautifulSoup的html.parser解析器比lxml更能处理畸形HTML,但XPath定位效率更高,建议混合使用。
2.2 反爬对抗基础配置
农业信息类网站的反爬措施通常包括:
- 基础User-Agent检测
- 请求频率限制
- 关键参数加密
- 验证码拦截
建议在项目根目录创建config.py存放防爬配置:
python复制# 轮询User-Agent池
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
]
# 代理IP配置(需自行维护可用IP池)
PROXIES = {
'http': 'http://proxy.example.com:8080',
'https': 'https://proxy.example.com:8080'
}
# 请求间隔控制(秒)
DELAY = {
'min': 1.5,
'max': 3.2
}
3. 目录页采集实战
3.1 页面结构分析
以某农业信息网为例,其病虫害目录页典型结构如下:
html复制<div class="disease-list">
<div class="disease-item">
<a href="/detail/123.html">
<img src="/uploads/2022/03/abc.jpg">
<h3>小麦赤霉病</h3>
<p>主要危害穗部...</p>
</a>
<span class="category">麦类病害</span>
</div>
<!-- 更多类似条目 -->
</div>
3.2 采集代码实现
python复制def crawl_catalog(url):
headers = {'User-Agent': random.choice(config.USER_AGENTS)}
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, 'html.parser')
items = soup.select('.disease-list .disease-item')
result = []
for item in items:
data = {
'title': item.select_one('h3').get_text().strip(),
'url': urljoin(url, item.select_one('a')['href']),
'cover': urljoin(url, item.select_one('img')['src']),
'desc': item.select_one('p').get_text().strip(),
'category': item.select_one('.category').get_text()
}
result.append(data)
time.sleep(random.uniform(config.DELAY['min'], config.DELAY['max']))
return result
except Exception as e:
print(f"目录页采集失败: {str(e)}")
return []
3.3 分页处理技巧
农业网站分页通常有三种形式:
- 传统页码分页(最容易处理)
- 滚动加载(需要模拟滑动操作)
- 动态参数分页(需解析JS代码)
对于滚动加载页面,可以结合Selenium处理:
python复制def scroll_crawl(url):
driver = webdriver.Chrome()
driver.get(url)
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
# 解析最终页面内容
html = driver.page_source
driver.quit()
return parse_html(html)
4. 详情页深度采集
4.1 典型详情页结构分析
农业病虫害详情页通常包含:
- 病害名称(可能带拉丁学名)
- 危害症状描述(多段落文本)
- 发生规律(时间/环境因素)
- 防治措施(化学/生物防治)
- 图片集合(症状图/病原图/危害图)
4.2 多媒体资源采集
图片采集需要特别注意:
python复制def download_images(img_urls, save_dir):
if not os.path.exists(save_dir):
os.makedirs(save_dir)
for i, url in enumerate(img_urls):
try:
resp = requests.get(url, stream=True)
resp.raise_for_status()
# 从响应头或URL中提取真实文件扩展名
content_type = resp.headers.get('content-type')
ext = '.jpg' if 'jpeg' in content_type else '.png'
save_path = os.path.join(save_dir, f"{i}{ext}")
with open(save_path, 'wb') as f:
for chunk in resp.iter_content(1024):
f.write(chunk)
# 添加图片元数据
img = Image.open(save_path)
img.save(save_path, quality=95)
except Exception as e:
print(f"图片下载失败: {url} - {str(e)}")
4.3 文本信息结构化
农业文本的特殊处理:
python复制def parse_detail(html):
soup = BeautifulSoup(html, 'lxml')
# 使用CSS选择器结合XPath提高定位精度
title = soup.select_one('h1.main-title').get_text()
latin_name = re.search(r'([A-Z][a-z]+\s[a-z]+)', title)
# 症状描述分段处理
symptoms = []
for p in soup.select('.symptoms-section p'):
text = p.get_text().strip()
if text and len(text) > 10: # 过滤空段落和短文本
symptoms.append(text)
# 防治措施表格处理
prevention = []
table = soup.select_one('.prevention-table')
if table:
for row in table.select('tr'):
cols = [td.get_text().strip() for td in row.select('td')]
if len(cols) == 2:
prevention.append({
'method': cols[0],
'description': cols[1]
})
return {
'title': title,
'latin_name': latin_name.group(1) if latin_name else None,
'symptoms': symptoms,
'prevention': prevention,
# 其他字段...
}
5. 反爬对抗进阶策略
5.1 请求特征伪装
农业网站常用的检测点:
- Cookie完整性检查
- Referer来源验证
- 关键参数签名
改进后的请求示例:
python复制def stealth_request(url):
session = requests.Session()
# 初始访问获取必要Cookie
session.get(homepage, headers={
'User-Agent': random.choice(USER_AGENTS),
'Accept-Language': 'zh-CN,zh;q=0.9'
})
# 带Referer的正式请求
resp = session.get(url, headers={
'User-Agent': random.choice(USER_AGENTS),
'Referer': homepage,
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'text/html,application/xhtml+xml'
}, proxies=PROXIES)
return resp
5.2 验证码处理方案
当遇到验证码时,推荐的处理流程:
- 首次触发时自动暂停采集
- 人工介入解决验证码
- 获取有效Cookie/token
- 将凭证回填到爬虫系统
- 继续自动化采集
对于简单图形验证码,可以使用OCR方案:
python复制def simple_ocr_captcha(image_bytes):
import pytesseract
from PIL import Image, ImageEnhance
img = Image.open(io.BytesIO(image_bytes))
img = img.convert('L') # 灰度处理
img = ImageEnhance.Contrast(img).enhance(2.0) # 对比度增强
custom_config = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
text = pytesseract.image_to_string(img, config=custom_config)
return text.strip()
6. 数据存储与质量管控
6.1 MongoDB存储优化
农业数据的特点决定了存储方案:
python复制from pymongo import MongoClient
from pymongo.errors import DuplicateKeyError
class AgriDataStorage:
def __init__(self):
self.client = MongoClient('mongodb://localhost:27017/')
self.db = self.client['agri_disease']
self.collection = self.db['diseases']
# 创建复合索引
self.collection.create_index([
('title', 'text'),
('category', 1),
('latin_name', 1)
])
def save_data(self, data):
try:
# 基于URL去重
data['_id'] = hashlib.md5(data['url'].encode()).hexdigest()
self.collection.insert_one(data)
return True
except DuplicateKeyError:
print(f"重复数据: {data['title']}")
return False
6.2 数据质量检查点
必须实现的检查逻辑:
python复制def validate_data(data):
# 必填字段检查
required_fields = ['title', 'url', 'symptoms']
if not all(data.get(field) for field in required_fields):
return False
# 文本有效性验证
if len(data['symptoms']) < 3 or len(data['symptoms'][0]) < 20:
return False
# 图片URL验证
if data.get('images'):
if not all(url.startswith(('http://', 'https://')) for url in data['images']):
return False
# 防治措施完整性
if not data.get('prevention') or len(data['prevention']) < 2:
return False
return True
7. 项目部署与调度
7.1 Scrapy项目化改造
对于大规模采集,建议改用Scrapy框架:
python复制class DiseaseSpider(scrapy.Spider):
name = "agri_disease"
custom_settings = {
'DOWNLOAD_DELAY': 2,
'CONCURRENT_REQUESTS_PER_DOMAIN': 1,
'ITEM_PIPELINES': {
'pipelines.ImagePipeline': 300,
'pipelines.MongoPipeline': 400,
}
}
def start_requests(self):
for url in self.start_urls:
yield scrapy.Request(
url=url,
callback=self.parse_catalog,
meta={'proxy': random.choice(PROXY_LIST)}
)
def parse_catalog(self, response):
# 目录页解析逻辑
pass
def parse_detail(self, response):
# 详情页解析逻辑
pass
7.2 分布式任务调度
使用Celery实现分布式采集:
python复制from celery import Celery
app = Celery('agri_crawler', broker='redis://localhost:6379/0')
@app.task(bind=True, max_retries=3)
def crawl_task(self, url, is_detail=False):
try:
if is_detail:
return crawl_detail_page(url)
else:
return crawl_catalog_page(url)
except Exception as e:
self.retry(exc=e, countdown=60)
8. 法律合规与伦理考量
8.1 robots.txt合规检查
必须实现的检查逻辑:
python复制from urllib.robotparser import RobotFileParser
def check_robots_permission(base_url):
rp = RobotFileParser()
robots_url = urljoin(base_url, '/robots.txt')
try:
rp.set_url(robots_url)
rp.read()
return rp.can_fetch('*', base_url)
except Exception:
# 当robots.txt不存在时默认允许
return True
8.2 采集伦理实践
农业数据采集的特殊准则:
- 严格控制请求频率(建议≤2请求/秒)
- 避开网站访问高峰时段(早8-10点)
- 不采集农户个人信息
- 对采集的数据注明来源
- 设置明显的爬虫标识User-Agent
建议在代码中添加伦理控制:
python复制ETHICAL_CONTROL = {
'max_pages': 1000, # 单域名最大采集页数
'time_blacklist': [8, 9, 10], # 早上8-10点不采集
'avoid_extensions': ['.pdf', '.doc'], # 不采集文档类资源
'copyright_check': True # 启用版权声明检查
}
9. 常见问题解决方案
9.1 编码问题处理
农业网站常见的编码问题:
python复制def safe_decode(content):
encodings = ['utf-8', 'gbk', 'gb2312', 'big5']
for enc in encodings:
try:
return content.decode(enc)
except UnicodeDecodeError:
continue
# 终极fallback方案
return content.decode('utf-8', errors='ignore')
9.2 页面结构变动监测
实现自动检测机制:
python复制class StructureMonitor:
def __init__(self):
self.selector_stats = defaultdict(int)
def check_selectors(self, selectors):
for name, selector in selectors.items():
if not response.css(selector):
self.selector_stats[name] += 1
if self.selector_stats[name] > 3:
alert(f"选择器失效: {name} -> {selector}")
return False
return True
10. 性能优化实战技巧
10.1 异步采集实现
使用aiohttp加速采集:
python复制import aiohttp
import asyncio
async def async_crawl(urls):
connector = aiohttp.TCPConnector(limit=10)
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
async def fetch(session, url):
try:
async with session.get(url) as response:
if response.status == 200:
return await response.text()
return None
except Exception as e:
print(f"请求失败: {url} - {str(e)}")
return None
10.2 缓存机制设计
使用磁盘缓存避免重复请求:
python复制from diskcache import Cache
class PageCache:
def __init__(self, cache_dir='.cache'):
self.cache = Cache(cache_dir)
def get_page(self, url):
key = hashlib.md5(url.encode()).hexdigest()
if key in self.cache:
return self.cache[key]
content = download_page(url)
if content:
self.cache.set(key, content, expire=86400) # 缓存24小时
return content
return None
11. 项目经验与反思
在实际采集过程中,有几个关键教训值得分享:
-
农业术语准确性:初期没有建立标准术语库,导致不同来源的同类病害被当作不同条目。解决方案是预先构建农作物病虫害标准词表,包含常见名称、学名和别名。
-
图片版权风险:部分图谱图片带有明确版权声明,需要特别处理。我们最终建立了三级分类:
- 可自由使用的公共图片
- 需署名的授权图片
- 禁止采集的版权图片
-
数据更新机制:农业病虫害数据会随研究进展更新,我们设计了基于时间戳的增量采集方案:
python复制def check_update(last_crawl): now = datetime.now() if (now - last_crawl).days > 30: # 每月全量更新 return True elif has_emergency_alert(): # 紧急病虫害预警 return True return False -
存储成本控制:高分辨率图片占用空间大,我们最终采用分级存储:
- 热数据:SSD存储最近3个月数据
- 温数据:HDD存储历史数据
- 冷数据:对象存储归档早期数据
这个项目让我深刻体会到,专业领域爬虫开发不仅是技术问题,更需要理解行业特性。农业数据的季节性、地域性特点,要求采集系统具备特殊的灵活性。比如在小麦收获季节,需要优先采集麦类病害更新;在南方雨季,则要关注水稻病虫害动态。
