第一次尝试爬取SteamDB免费游戏数据时,我天真地以为用Requests库就能轻松搞定。结果发现访问https://steamdb.info/upcoming/free/时,页面会自动跳转到一个验证页面。这就是典型的反爬机制——通过JavaScript计算表单数据并跳转,没有有效Cookie根本无法获取真实数据。
经过多次尝试,我发现这个反爬机制有几个特点:
这让我意识到需要混合使用Selenium和Requests:
实测下来,纯Requests方案的成功率不到10%,而混合策略能达到99%以上。这种动态切换的思路后来被我应用到其他需要登录的网站爬取中,效果都很稳定。
核心代码其实很简单,但魔鬼藏在细节里。我最初用ChromeDriver,后来发现Firefox在无头模式下更稳定:
python复制def update_cookie():
option = webdriver.FirefoxOptions()
option.add_argument('--headless')
driver = webdriver.Firefox(options=option)
driver.get('https://steamdb.info/upcoming/free/')
# 关键:等待特定元素加载完成
WebDriverWait(driver, 15).until(
lambda d: d.find_element_by_id('live-promotions'))
with open('cookie.txt', 'w') as f:
for cookie in driver.get_cookies():
f.write(f"{cookie['name']},{cookie['value']}\n")
踩过的坑包括:
完善的错误处理是服务稳定的关键。我的重试逻辑分三级:
python复制def get_html():
session = requests.Session()
try:
# 读取本地Cookie文件
with open('cookie.txt') as f:
cookies = {name:value for name,value in
[line.strip().split(',') for line in f]}
response = session.get(url, cookies=cookies)
if response.status_code == 200:
return response
# 状态码异常时更新Cookie
if update_cookie():
return get_html() # 递归重试
except Exception as e:
log_error(f"请求失败: {str(e)}")
return None
初期直接用CSV存储数据,后来发现查询效率太低。改用SQLAlchemy后设计了两个表:
python复制class Steamfree(db.Model):
__tablename__ = 'steam_free_games'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200)) # 游戏名称
link = db.Column(db.Text) # Steam链接
start_time = db.Column(db.DateTime) # 免费开始时间
end_time = db.Column(db.DateTime) # 免费结束时间
class Subscription(db.Model):
__tablename__ = 'subscriptions'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True)
confirmed = db.Column(db.Boolean, default=False)
特别注意的点:
选用APScheduler作为任务调度器,主要考虑它:
配置示例:
python复制from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
scheduler.add_job(
func=update_game_data,
trigger='interval',
hours=6,
misfire_grace_time=300
)
scheduler.add_job(
func=send_daily_digest,
trigger='cron',
hour=10,
minute=0
)
为了让邮件更专业,我使用了HTML模板:
html复制<div style="font-family: Arial, sans-serif; max-width: 600px;">
<h2 style="color: #1a5276;">今日Steam免费游戏</h2>
{% for game in games %}
<div style="margin-bottom: 20px; border-bottom: 1px solid #eee;">
<h3>{{ game.name }}</h3>
<p>类型: {{ game.type }}</p>
<p>免费时间: {{ game.start }} 至 {{ game.end }}</p>
<a href="{{ game.link }}" style="color: #2980b9;">查看详情</a>
</div>
{% endfor %}
<p style="font-size: 12px; color: #7f8c8d;">
不想再接收此类邮件?<a href="{{ unsubscribe_url }}">退订</a>
</p>
</div>
实现订阅/退订功能时特别注意了:
核心视图函数:
python复制@app.route('/subscribe', methods=['POST'])
def subscribe():
email = request.form.get('email')
if not validate_email(email):
return "无效邮箱地址", 400
# 检查是否已订阅
existing = Subscription.query.filter_by(email=email).first()
if existing:
return "该邮箱已订阅", 409
# 发送确认邮件
token = generate_token(email)
send_confirmation_email(email, token)
return "确认邮件已发送,请查收", 200
整个项目从最初几十行的爬虫脚本,逐步演进为包含前后端的完整服务。最大的收获是认识到系统设计时预留扩展性的重要性,比如后来新增的游戏类型过滤功能,就因为有良好的代码结构而能快速实现。