schedule任务需在守护线程中运行并设stop_event控制生命周期,避免阻塞主线程;并发请求应改用aiohttp调优连接池与DNS缓存;失败任务持久化推荐SQLite加UNIQUE约束;日志须用RotatingFileHandler管理且禁用StreamHandler。

用 schedule 做简单周期调度,但别让它跑在主线程里
直接在主线程里调用 schedule.run_pending() 并用 time.sleep() 循环,会导致整个程序被阻塞,无法响应中断(比如 Ctrl+C)、日志刷新延迟、异常无法及时捕获。更糟的是,一旦某个爬取任务卡死或抛出未捕获异常,整个调度就停摆了。
正确做法是把调度逻辑放到独立线程中运行,并设置守护属性,同时用 threading.Event 控制生命周期:
import schedule
import threading
import time
<p>stop_event = threading.Event()</p><p>def job():
try:</p><h1>你的爬取逻辑,含异常兜底</h1><pre class="brush:php;toolbar:false;"> pass
except Exception as e:
print(f"Job failed: {e}")
def run_scheduler(): while not stop_event.is_set(): schedule.run_pending() time.sleep(1)
启动调度线程
scheduler_thread = threading.Thread(target=run_scheduler, daemon=True) scheduler_thread.start()
程序退出前触发停止
try: while True: time.sleep(3600) # 主线程保持活跃,比如每小时打个日志 except KeyboardInterrupt: stop_event.set() scheduler_thread.join(timeout=2)
用 aiohttp + asyncio 处理并发请求,但注意 DNS 和连接池限制
同步的 requests 在定时任务里容易拖慢整体节奏,尤其当多个目标站点响应不稳定时。改用 aiohttp 可显著提升吞吐,但默认配置下常遇到 ClientConnectorError 或 DNS 解析超时——这不是代码写错了,而是没调优连接池和 DNS 缓存。
关键配置点:
-
connector = aiohttp.TCPConnector(limit=20, limit_per_host=5, ttl_dns_cache=300):避免单域名压垮对方,也防自己被限频 - 显式传入
timeout=aiohttp.ClientTimeout(total=15),防止个别请求无限挂起 - DNS 缓存必须设,否则每次请求都走系统 resolver,Windows/macOS 下尤其慢
示例片段:
import asyncio
import aiohttp
<p>async def fetch(session, url):
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
return await resp.text()
except Exception as e:
print(f"Fetch {url} failed: {e}")
return None</p><p>async def batch_fetch(urls):
connector = aiohttp.TCPConnector(limit=20, limit_per_host=5, ttl_dns_cache=300)
timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
tasks = [fetch(session, u) for u in urls]
return await asyncio.gather(*tasks)
</p>
持久化失败任务用 sqlite3 而不是文件追加,且加唯一约束
很多人用文本文件记录失败 URL,结果重复写入、无索引、并发写崩、查重靠 grep——这在每小时跑几百次的任务里很快失控。SQLite 轻量、线程安全(配合 WAL 模式)、支持事务,更适合做轻量级任务状态中心。
建表时务必加 UNIQUE 约束,避免同一 URL 因重试反复插入:
CREATE TABLE IF NOT EXISTS crawl_failures (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL UNIQUE,
error TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
插入时用 INSERT OR IGNORE,更新用 INSERT OR REPLACE,避免异常打断流程。别手写 SELECT COUNT(*) 判断是否存在——那是性能黑洞。
用 logging.handlers.RotatingFileHandler 管理日志,禁用 StreamHandler 上生产
开发时打印到控制台很爽,但上线后没人盯着终端。不设轮转的日志文件会越滚越大,某天磁盘爆掉才发现日志占了 40GB。更隐蔽的问题是:多进程/多线程共用一个 FileHandler 实例可能丢日志(Python logging 默认非进程安全)。
稳妥做法:
- 用
RotatingFileHandler,maxBytes=10*1024*1024(10MB),backupCount=5 - 禁用所有
StreamHandler,哪怕只有一行logger.addHandler(logging.StreamHandler())都得删 - 日志格式里加上
%(asctime)s %(name)s %(levelname)s %(message)s,别省%(name)s——不同模块日志混在一起时全靠它区分
定时任务最怕“看似在跑,其实卡死”,而日志是唯一可信线索。漏掉一条关键 error,排查时间翻三倍。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











