避开这些坑!用Requests和Re爬51job时遇到的编码、反爬与数据解析难题解决记录
1. 为什么爬取招聘数据比想象中复杂
最近在做一个Python招聘数据分析项目时,发现公开数据集的质量参差不齐,于是决定自己动手爬取51job的数据。本以为是个简单的任务,结果从第一行代码开始就踩了无数坑。这篇文章记录了我遇到的各种问题及其解决方案,希望能帮到同样在爬虫路上踩坑的你。
招聘网站的反爬机制通常比普通网站更严格,这与其商业价值直接相关。51job作为国内头部招聘平台,在数据保护方面做了不少工作。以下是几个最让人头疼的问题:
- 动态渲染 :部分关键数据通过JavaScript动态加载
- 请求限制 :频繁访问会触发验证码或直接封IP
- 数据编码 :响应内容的字符集处理不当会导致乱码
- 结构变化 :页面DOM结构会不定期调整
2. 编码问题:从乱码到清晰数据的蜕变
第一次成功获取响应内容时,看到的是一堆乱码。这个问题看似简单,却困扰了我整整半天。
2.1 字符集检测的陷阱
使用requests获取响应后,直接打印text属性出现乱码:
import requests
url = 'https://search.51job.com/list/010000,000000,0000,00,9,99,python,2,1.html'
response = requests.get(url)
print(response.text[:500]) # 输出乱码
问题出在字符集检测上。51job的响应头中有时不指定charset,而默认的ISO-8859-1解析会导致中文乱码。解决方案是手动指定编码:
response.encoding = 'gbk' # 或'utf-8',视具体情况而定
print(response.text[:500]) # 正常显示中文
2.2 动态内容的编码问题
更复杂的情况是,当页面包含动态加载的内容时,不同部分的编码可能不一致。这时需要:
- 检查响应头的Content-Type
- 分析页面meta标签中的charset声明
- 对特殊字符进行转义处理
一个实用的编码检测函数:
def detect_encoding(content):
encodings = ['utf-8', 'gbk', 'gb2312', 'big5']
for enc in encodings:
try:
return content.decode(enc)
except UnicodeDecodeError:
continue
return content.decode('utf-8', errors='ignore')
3. 反爬机制与应对策略
51job的反爬系统相当完善,以下是几种常见的封锁方式和应对方法。
3.1 请求频率控制
不加控制地快速请求会导致IP被封。合理的做法是:
- 在每个请求间添加随机延迟
- 使用代理IP池轮换
- 设置合理的超时时间
import time
import random
def safe_request(url):
time.sleep(random.uniform(1, 3)) # 1-3秒随机延迟
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
'Referer': 'https://www.51job.com/'
}
return requests.get(url, headers=headers)
3.2 关键数据动态加载
通过分析发现,职位详情等关键信息是通过AJAX加载的。解决方案是:
- 使用浏览器开发者工具抓取真实API接口
- 模拟XHR请求获取JSON数据
- 处理接口参数中的加密字段
提示:动态内容通常可以在Network面板的XHR过滤项中找到,关注返回JSON数据的接口
4. 数据解析的进阶技巧
原始HTML结构复杂多变,直接解析很容易出错。以下是几种更健壮的解析方法。
4.1 正则表达式的精准使用
虽然不推荐大规模使用正则,但在提取特定模式数据时它非常高效。例如提取薪资范围:
import re
text = "薪资:15-20万/年"
pattern = r"(\d+)[^\d]+(\d+)"
match = re.search(pattern, text)
if match:
min_salary, max_salary = match.groups()
4.2 结构化数据处理
当数据以JSON格式返回时,直接解析比处理HTML更可靠:
import json
data_str = 'window.__SEARCH_RESULT__ = {"key":"value"}</script>'
json_str = re.search(r'window\.__SEARCH_RESULT__ = (.*?)</script>', data_str).group(1)
data = json.loads(json_str)
4.3 数据清洗与标准化
招聘数据中的薪资、地点等信息格式不统一,需要标准化:
| 原始数据 | 标准化后 |
|---|---|
| "15-20万/年" | {"min":150000, "max":200000, "unit":"year"} |
| "上海-浦东新区" | {"province":"上海", "district":"浦东新区"} |
| "3年以上经验" | 3 |
5. 数据存储的最佳实践
爬取的数据需要合理存储以便后续分析。CSV是最简单的选择,但有以下注意事项:
- 处理包含逗号或换行的字段
- 统一字符编码(推荐UTF-8 with BOM)
- 添加爬取时间戳作为元数据
import csv
from datetime import datetime
def save_to_csv(data, filename):
with open(filename, 'a', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=data.keys())
if f.tell() == 0: # 如果是新文件,写入表头
writer.writeheader()
data['crawl_time'] = datetime.now().isoformat()
writer.writerow(data)
6. 调试与错误处理
完善的错误处理能大大提高爬虫的稳定性。以下是我的错误处理框架:
- 网络请求错误 :重试机制
- 解析错误 :记录原始数据供后续分析
- 反爬触发 :自动切换代理或暂停
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def robust_request(url):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response
except Exception as e:
log_error(f"Request failed: {str(e)}")
raise
7. 实战中的经验总结
经过多次迭代,我的爬虫终于能稳定运行了。几个关键收获:
- 细节决定成败 :一个小的编码问题可能导致整个项目失败
- 保持低调 :控制请求频率,模拟正常用户行为
- 灵活应变 :网站结构变化时及时调整解析逻辑
- 数据质量 :宁可少爬也要保证数据的准确性和一致性
最后分享一个实用技巧:使用Selenium的headless模式调试动态内容,虽然效率不高,但在破解复杂反爬机制时非常有用。记得合理设置等待时间,避免被检测到自动化行为。
495

被折叠的 条评论
为什么被折叠?



