1. 政府采购网站数据采集的核心挑战
政府采购网站的数据采集和普通电商网站完全不同,最大的区别在于政府采购网站的反爬机制往往更加严格。我做过几十个政府采购网站的爬虫项目,发现它们通常会有以下几个特点:
高频访问限制:政府采购网站对短时间内的大量请求非常敏感,一旦检测到异常流量,会立即封禁IP。比如search.ccgp.gov.cn这个网站,如果连续请求超过5次/分钟,大概率会触发封禁。
动态参数验证:很多政府采购网站会在请求中加入动态生成的token或者时间戳参数,如果请求中缺少这些参数,即使IP没有被封,也会返回错误数据。
复杂页面结构:政府采购网站的HTML结构往往比较复杂,一个采购公告可能分散在多个div层中,而且class命名没有规律可循。
验证码机制:当系统检测到可疑行为时,会弹出图形验证码或者滑动验证码,这对自动化采集是个不小的挑战。
针对这些特点,我们需要设计一个健壮的采集系统。下面这个基础框架是我在实际项目中验证过的:
import requests from bs4 import BeautifulSoup import time import random class GovPurchaseSpider: def __init__(self): self.base_url = "https://search.ccgp.gov.cn" self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } def get_page(self, url, params=None, retry=3): for i in range(retry): try: resp = requests.get(url, headers=self.headers, params=params, timeout=10) if resp.status_code == 200: return resp.text else: time.sleep(random.uniform(1, 3)) except Exception as e: print(f"请求失败: {e}") time.sleep(5) return None2. Requests库的高级使用技巧
很多新手在使用Requests库时,只会用最简单的get方法,这在实际项目中是远远不够的。下面分享几个我在政府采购网站采集中的实战经验:
会话保持非常重要。政府采购网站很多都需要登录后才能查看完整信息,使用Session对象可以自动处理cookies:
session = requests.Session() login_url = "https://example.com/login" login_data = {"username": "yourname", "password": "yourpass"} session.post(login_url, data=login_data) # 登录 # 后续请求会自动携带cookies profile_page = session.get("https://example.com/profile")请求头伪装是另一个关键点。政府采购网站会检测User-Agent、Referer等头信息。我通常会准备一组常见的浏览器头信息随机使用:
user_agents = [ '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', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36' ] headers = { 'User-Agent': random.choice(user_agents), 'Referer': 'https://www.ccgp.gov.cn/', 'Accept-Language': 'zh-CN,zh;q=0.9' }异常处理也需要特别注意。政府采购网站经常会出现连接超时、响应过慢等问题,完善的异常处理可以大大提高采集稳定性:
try: response = requests.get(url, headers=headers, timeout=15) response.raise_for_status() # 检查HTTP状态码 if "验证码" in response.text: raise Exception("触发验证码") except requests.exceptions.Timeout: print("请求超时,正在重试...") except requests.exceptions.RequestException as e: print(f"请求异常: {e}")3. BeautifulSoup解析复杂页面结构
政府采购网站的页面结构往往非常复杂,用对解析方法可以事半功倍。我总结了几种常见情况的处理方案:
多层嵌套结构在采购公告中很常见。比如下面这个典型结构:
<div class="content"> <div class="vT_detail"> <div class="vT_detail_main"> <ul> <li>采购项目编号:XXXXX</li> <li>采购项目名称:XXXXX</li> </ul> </div> </div> </div>对应的解析代码应该是:
soup = BeautifulSoup(html, 'lxml') main_div = soup.find('div', class_='vT_detail_main') if main_div: items = main_div.find_all('li') for item in items: text = item.get_text(strip=True) if '采购项目编号' in text: project_id = text.split(':')[-1]表格数据是另一个常见形式。政府采购网站喜欢用表格展示关键信息:
table = soup.find('table', {'id': 'resultTable'}) if table: rows = table.find_all('tr')[1:] # 跳过表头 for row in rows: cols = row.find_all('td') if len(cols) >= 3: project_name = cols[0].get_text(strip=True) budget = cols[1].get_text(strip=True) deadline = cols[2].get_text(strip=True)动态加载内容越来越普遍。有些政府采购网站会使用AJAX加载数据,这时候需要先分析XHR请求:
import json # 先找到包含动态数据的script标签 script = soup.find('script', text=re.compile('window.__DATA__')) if script: # 提取JSON数据 json_str = script.text.split('=')[1].strip().rstrip(';') data = json.loads(json_str) items = data['list']4. 代理IP系统的工程化实现
政府采购网站对IP的限制非常严格,一个稳定的代理IP系统是项目成功的关键。我设计过不少代理IP方案,下面分享一个经过验证的实现:
代理IP池的基本架构应该包含以下几个模块:
- IP获取模块 - 从代理服务商API获取IP
- 验证模块 - 检查IP是否可用
- 存储模块 - 使用Redis存储可用IP
- 调度模块 - 分配IP给各个爬虫任务
下面是一个简化版的实现:
import redis import requests class ProxyPool: def __init__(self): self.redis = redis.StrictRedis(host='localhost', port=6379, db=0) self.test_url = "http://httpbin.org/ip" def add_proxy(self, proxy): """添加新代理到池子""" if self._check_proxy(proxy): self.redis.sadd('valid_proxies', proxy) return True return False def _check_proxy(self, proxy): """验证代理是否可用""" proxies = { 'http': f'http://{proxy}', 'https': f'http://{proxy}' } try: resp = requests.get(self.test_url, proxies=proxies, timeout=10) if resp.status_code == 200: return True except: return False def get_random_proxy(self): """随机获取一个可用代理""" proxy = self.redis.srandmember('valid_proxies') return proxy.decode() if proxy else None def remove_proxy(self, proxy): """移除失效代理""" self.redis.srem('valid_proxies', proxy)代理使用策略也很重要。在政府采购网站采集时,我建议采用以下策略:
- 按请求切换IP:每个请求使用不同的IP,降低被封风险
- 失败重试机制:当请求失败时自动更换IP重试
- IP冷却机制:使用过的IP暂时搁置,过段时间再使用
实现代码示例:
def make_request(url, proxy_pool, max_retry=3): for i in range(max_retry): proxy = proxy_pool.get_random_proxy() if not proxy: raise Exception("没有可用代理") proxies = { 'http': f'http://{proxy}', 'https': f'http://{proxy}' } try: resp = requests.get(url, proxies=proxies, timeout=15) if resp.status_code == 200: return resp.text else: proxy_pool.remove_proxy(proxy) except Exception as e: print(f"使用代理 {proxy} 请求失败: {e}") proxy_pool.remove_proxy(proxy) raise Exception("超过最大重试次数")5. 工程化部署与优化建议
当采集系统需要长期运行时,工程化部署就变得非常重要。下面分享几个实战中的经验:
分布式架构可以显著提升采集效率。我常用的方案是:
- 使用Redis作为任务队列
- 多个爬虫节点从队列获取任务
- 结果统一存储到MySQL或MongoDB
基础实现代码:
import redis from multiprocessing import Process class CrawlerWorker(Process): def __init__(self, proxy_pool): super().__init__() self.redis = redis.StrictRedis() self.proxy_pool = proxy_pool def run(self): while True: task = self.redis.lpop('task_queue') if not task: time.sleep(5) continue url = task.decode() html = make_request(url, self.proxy_pool) data = parse_page(html) save_to_db(data) def start_workers(num=4): proxy_pool = ProxyPool() workers = [CrawlerWorker(proxy_pool) for _ in range(num)] for w in workers: w.start() for w in workers: w.join()日志系统是另一个关键组件。完善的日志可以帮助快速定位问题:
import logging from logging.handlers import RotatingFileHandler def setup_logger(name): logger = logging.getLogger(name) logger.setLevel(logging.INFO) # 文件日志,自动轮转 file_handler = RotatingFileHandler( 'crawler.log', maxBytes=10*1024*1024, backupCount=5 ) file_formatter = logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) file_handler.setFormatter(file_formatter) # 控制台日志 console_handler = logging.StreamHandler() console_formatter = logging.Formatter('%(levelname)s: %(message)s') console_handler.setFormatter(console_formatter) logger.addHandler(file_handler) logger.addHandler(console_handler) return logger性能优化方面,我有几个实用建议:
- 连接复用:使用requests.Session可以复用TCP连接,减少握手开销
- 异步IO:对于IO密集型任务,可以使用aiohttp替代requests
- 缓存机制:对已经采集过的页面进行缓存,避免重复采集
- 智能休眠:根据网站响应时间动态调整请求间隔
异步采集示例:
import aiohttp import asyncio async def async_fetch(url, session, proxy): try: async with session.get(url, proxy=proxy, timeout=15) as response: if response.status == 200: return await response.text() except Exception as e: print(f"异步请求失败: {e}") return None async def async_crawler(urls): connector = aiohttp.TCPConnector(limit=10) # 限制并发数 async with aiohttp.ClientSession(connector=connector) as session: tasks = [] for url in urls: proxy = proxy_pool.get_random_proxy() task = async_fetch(url, session, proxy) tasks.append(task) return await asyncio.gather(*tasks)