1. 项目背景与核心价值
快递查询是电商时代刚需功能,但手动逐个查询物流信息效率极低。作为Python开发者,我经常需要批量追踪数十个快递单号,传统方式不仅耗时还容易出错。通过Python爬虫自动化这一过程,可以将查询效率提升10倍以上。
这个项目本质上是通过模拟浏览器请求,从快递公司官网或第三方平台抓取物流数据。相比人工查询,自动化方案有三大优势:
- 支持批量处理上千单号,查询速度仅受限于网络带宽
- 可设置定时自动刷新,实时监控物流状态变化
- 数据直接存入数据库或Excel,便于后续分析统计
2. 技术方案选型
2.1 主流爬虫框架对比
在Python生态中,爬虫方案主要分为三类:
| 方案类型 | 代表库 | 适用场景 | 本项目适用性 |
|---|---|---|---|
| 基础请求库 | requests | 简单页面抓取 | ★★★☆☆ |
| 无头浏览器 | selenium | 复杂JS渲染页面 | ★★☆☆☆ |
| 爬虫框架 | scrapy | 大规模分布式爬取 | ★★★★☆ |
经过实测,快递查询页面结构相对简单,但需要处理验证码和动态参数。最终选择requests+BeautifulSoup组合,理由如下:
- 学习曲线平缓,适合快速开发
- 性能优于无头浏览器方案
- 足够应对主流快递公司官网接口
2.2 快递API接口分析
国内主流快递公司都提供官方查询接口,典型的有两种形式:
-
网页端接口(如顺丰官网)
- 需要模拟完整HTTP请求流程
- 示例请求参数:
python复制params = { 'txtOrder': 'SF123456789', 'language': 'zh-cn', 'token': '动态生成的加密字符串' } -
开放平台API(如快递100)
- 需要申请开发者key
- 返回标准JSON格式数据
- 示例请求:
python复制import requests url = "https://api.kuaidi100.com/query" params = { 'type': 'shunfeng', 'postid': 'SF123456789', 'temp': '0.123456', # 随机数防缓存 'key': '你的API_KEY' }
提示:优先考虑开放平台API,虽然需要注册但稳定性更好。网页抓取方案可能随时失效。
3. 核心代码实现
3.1 基础查询功能
以中通快递为例,完整实现代码如下:
python复制import requests
from bs4 import BeautifulSoup
import re
def query_zto(waybill):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://www.zto.com/'
}
# 第一步:获取动态token
session = requests.Session()
index_page = session.get('https://www.zto.com', headers=headers)
token = re.search(r'name="_token" value="(.*?)"', index_page.text).group(1)
# 第二步:提交查询请求
api_url = 'https://www.zto.com/gateway/waybillQuery'
form_data = {
'_token': token,
'billCode': waybill,
'lang': 'zh-CN'
}
response = session.post(api_url, data=form_data, headers=headers)
# 第三步:解析结果
soup = BeautifulSoup(response.json()['data'], 'html.parser')
records = []
for item in soup.select('.track-list li'):
time = item.select('.time')[0].get_text(strip=True)
desc = item.select('.desc')[0].get_text(strip=True)
records.append(f"{time} {desc}")
return records
关键点说明:
- 使用Session保持会话状态
- 通过正则提取动态token
- 处理返回的HTML片段数据
- CSS选择器精准定位物流信息
3.2 多快递公司适配
通过策略模式实现多快递公司支持:
python复制class ExpressQuery:
def __init__(self):
self.strategies = {
'zto': self._query_zto,
'sf': self._query_sf,
'yto': self._query_yto
}
def query(self, company, waybill):
if company not in self.strategies:
raise ValueError(f"不支持的快递公司: {company}")
return self.strategies[company](waybill)
def _query_zto(self, waybill):
# 中通查询实现
pass
def _query_sf(self, waybill):
# 顺丰查询实现
headers = {
'Cookie': '需要先通过selenium获取'
}
# 特殊处理顺丰的加密参数
4. 反爬应对策略
4.1 常见反爬手段
快递网站常用的反爬措施包括:
| 反爬类型 | 应对方案 | 实现示例 |
|---|---|---|
| IP限制 | 使用代理IP池 | requests.get(url, proxies={'http': '1.2.3.4:8080'}) |
| 验证码 | 第三方打码平台 | 接入图鉴、超级鹰等API |
| 参数加密 | 逆向分析JS代码 | 使用PyExecJS执行加密函数 |
| 请求频率限制 | 随机延迟+错峰查询 | time.sleep(random.uniform(1,3)) |
4.2 动态Cookie获取方案
对于需要登录的快递公司(如顺丰),可采用混合方案:
python复制from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def get_sf_cookie():
chrome_options = Options()
chrome_options.add_argument('--headless')
driver = webdriver.Chrome(options=chrome_options)
driver.get('https://www.sf-express.com')
driver.find_element('id', 'loginName').send_keys('你的账号')
driver.find_element('id', 'password').send_keys('你的密码')
driver.find_element('id', 'loginSubmit').click()
cookies = {}
for cookie in driver.get_cookies():
cookies[cookie['name']] = cookie['value']
driver.quit()
return cookies
注意:此方案仅作为备用方案,因为selenium性能较低。建议优先尝试破解接口加密逻辑。
5. 数据存储与可视化
5.1 数据库设计
建议使用SQLite存储查询结果:
python复制import sqlite3
from datetime import datetime
def init_db():
conn = sqlite3.connect('express.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS logistics
(id INTEGER PRIMARY KEY AUTOINCREMENT,
company TEXT,
waybill TEXT,
status TEXT,
update_time DATETIME)''')
conn.commit()
conn.close()
def save_record(company, waybill, records):
conn = sqlite3.connect('express.db')
c = conn.cursor()
latest_status = records[-1] if records else '无记录'
c.execute("INSERT INTO logistics VALUES (NULL,?,?,?,?)",
(company, waybill, latest_status, datetime.now()))
conn.commit()
conn.close()
5.2 可视化展示
使用PyQt5构建图形界面:
python复制from PyQt5.QtWidgets import (QApplication, QTableWidget,
QTableWidgetItem)
class ExpressTracker(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.table = QTableWidget()
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(
['快递公司', '运单号', '最新状态', '更新时间'])
# 从数据库加载数据
conn = sqlite3.connect('express.db')
c = conn.cursor()
c.execute("SELECT * FROM logistics ORDER BY update_time DESC")
rows = c.fetchall()
self.table.setRowCount(len(rows))
for i, row in enumerate(rows):
for j, col in enumerate(row[1:]): # 跳过id列
self.table.setItem(i, j, QTableWidgetItem(str(col)))
6. 项目优化方向
6.1 性能优化技巧
- 异步请求加速:
python复制import aiohttp
import asyncio
async def async_query(url, params):
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as response:
return await response.text()
# 批量查询示例
tasks = [async_query(url, params) for params in params_list]
results = await asyncio.gather(*tasks)
- 缓存机制:
python复制from datetime import timedelta
from django.core.cache import cache # 也可使用redis
def get_cached_query(waybill):
key = f"express_{waybill}"
result = cache.get(key)
if not result:
result = query_express(waybill)
cache.set(key, result, timeout=timedelta(minutes=30))
return result
6.2 异常处理规范
完善的错误处理应包括:
python复制def safe_query(waybill):
try:
result = query_express(waybill)
if not result:
raise ValueError("空响应")
return {
'status': 'success',
'data': result
}
except requests.exceptions.RequestException as e:
return {
'status': 'error',
'code': 'NETWORK_ERROR',
'message': f"网络请求失败: {str(e)}"
}
except Exception as e:
return {
'status': 'error',
'code': 'UNKNOWN_ERROR',
'message': str(e)
}
7. 常见问题排查
7.1 典型错误解决方案
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 返回"验证码错误" | IP被限制 | 更换代理IP或增加延迟 |
| 返回乱码 | 编码设置错误 | response.encoding = 'utf-8' |
| 连接超时 | 目标服务器限制 | 捕获超时异常并重试 |
| 参数校验失败 | 签名算法变更 | 重新分析网页JS代码 |
7.2 调试技巧
- 使用mitmproxy抓包分析:
bash复制mitmproxy -p 8080
# 设置requests代理为http://127.0.0.1:8080
- 保存临时响应数据:
python复制with open('debug.html', 'w', encoding='utf-8') as f:
f.write(response.text)
- 打印关键请求信息:
python复制print(f"Request URL: {response.request.url}")
print(f"Request Headers: {response.request.headers}")
print(f"Request Body: {response.request.body}")
这个项目最让我意外的是,看似简单的快递查询背后竟有如此复杂的反爬机制。经过三个版本的迭代,目前稳定运行的方案结合了requests直接请求、selenium模拟登录、异步并发查询三种技术,日均能处理5000+单号查询。建议在正式环境中加入监控告警机制,当查询失败率超过5%时自动通知维护。
