最近在帮朋友做一个本地生活服务类的数据分析项目,需要大量抓取分类信息平台上的商家数据。58同城作为国内最大的分类信息平台之一,自然成为重点数据来源。但在实际对接过程中发现,虽然平台提供了item_search接口,但官方文档对很多关键细节语焉不详,网上能找到的教程也大多停留在基础调用层面。
经过两周的踩坑和调试,终于梳理出一套完整的接口对接方案。这个攻略会从接口权限申请开始,逐步讲解签名生成、参数构造、数据解析等全流程,重点解决以下几个实际问题:
首先访问58开放平台官网,点击"开发者入驻"。这里有个坑点:个人开发者账号无法申请item_search这类核心接口权限,必须使用企业账号。需要准备:
特别提醒:营业执照上的公司名称必须与银行开户信息完全一致,我们曾因"有限公司"和"有限责任公司"的细微差异被驳回三次。
登录企业账号后,在控制台找到"生活服务API"分类,提交item_search接口的申请。需要填写详细的用途说明,建议包含:
审核通常需要3个工作日,期间可能会接到平台的回访电话。我们当时被问到数据存储方案和隐私保护措施,建议提前准备。
通过审核后,平台会分配:
建议的密钥存储方案:
python复制# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class APIConfig:
APP_KEY = os.getenv('58_APP_KEY') # 从环境变量读取
APP_SECRET = os.getenv('58_APP_SECRET')
TOKEN = os.getenv('58_ACCESS_TOKEN')
API_VERSION = 'v1'
官方文档给出的签名公式是:
code复制sign = md5(appKey + params + timestamp + appSecret)
但实际开发中会遇到三个关键问题:
修正后的Python实现:
python复制import hashlib
import urllib.parse
import time
def generate_sign(params: dict, app_key: str, app_secret: str) -> str:
# 过滤空值但保留key
filtered_params = {k: v for k, v in params.items() if v is not None}
# ASCII码升序排序
sorted_params = sorted(filtered_params.items(), key=lambda x: x[0])
# 拼接参数字符串
param_str = '&'.join(
f"{k}={urllib.parse.quote_plus(str(v)) if v else ''}"
for k, v in sorted_params
)
timestamp = str(int(time.time()))
raw_str = app_key + param_str + timestamp + app_secret
return hashlib.md5(raw_str.encode('utf-8')).hexdigest()
python复制import requests
def search_items(keyword: str, city: str, page: int = 1):
base_url = "https://api.58.com/service/open/item/search"
params = {
"keyword": keyword,
"city": city,
"page": page,
"pagesize": 50,
"sort": "price_asc"
}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Bearer {APIConfig.TOKEN}"
}
sign = generate_sign(params, APIConfig.APP_KEY, APIConfig.APP_SECRET)
params['sign'] = sign
params['timestamp'] = int(time.time())
response = requests.post(base_url, data=params, headers=headers)
return response.json()
| 参数名 | 必填 | 说明 | 优化建议 |
|---|---|---|---|
| city | 是 | 城市编码 | 使用行政区域代码而非城市名 |
| pagesize | 否 | 每页条数 | 实测最大支持100条 |
| sort | 否 | 排序方式 | price_asc/price_desc/time_asc/time_desc |
| quality | 否 | 信息质量 | 推荐设置quality=high过滤低质数据 |
平台对分页有严格限制:
我们的解决方案:
典型成功响应:
json复制{
"code": 200,
"message": "success",
"data": {
"total": 1245,
"list": [
{
"itemId": "123456789",
"title": "出租XX小区两居室",
"price": 4500,
"unit": "元/月",
"location": {
"district": "朝阳区",
"businessCircle": "国贸"
},
"pubDate": "2023-05-20 10:30:00"
}
]
}
}
常见错误码及应对:
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| 400 | 参数错误 | 检查签名生成逻辑 |
| 403 | 权限不足 | 刷新AccessToken |
| 429 | 频率限制 | 降低请求频率 |
| 500 | 服务端错误 | 联系平台技术支持 |
由于平台存在信息刷新机制,同一商品可能多次出现。我们采用的去重方案:
推荐使用住宅代理而非数据中心IP,配置示例:
python复制PROXY_POOL = [
"http://user:pass@proxy1.example.com:8080",
"http://user:pass@proxy2.example.com:8080"
]
def get_proxy():
import random
return {"http": random.choice(PROXY_POOL)}
使用aiohttp提高吞吐量:
python复制import aiohttp
import asyncio
async def async_search(session, params):
async with session.post(API_URL, data=params) as response:
return await response.json()
async def batch_search(keywords):
connector = aiohttp.TCPConnector(limit=30)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = []
for kw in keywords:
params = build_params(kw)
tasks.append(async_search(session, params))
return await asyncio.gather(*tasks)
推荐使用MongoDB存储非结构化数据,建立以下索引:
现象:频繁返回400错误
排查步骤:
现象:返回数据量少于预期
解决方案:
实测有效的策略:
经过三个月的生产环境验证,这套方案日均稳定采集50万+条数据,成功率保持在98%以上。最关键的体会是:与其不断更换爬虫策略,不如老老实实按平台规则办事,通过优化业务逻辑而非技术对抗来实现目标。