
做舆情采集的朋友大多遇到过这个两难:固定频率去抓全网媒体,要么低频漏掉突发热点,要么高频把代理流量烧光还被站点限流。我之前一套系统就是这么垮的------所有媒体一刀切每 5 分钟抓一次,结果低权重的边缘站点占了七成请求量,真正该盯紧的头部媒体反而因为代理配额被挤占,关键时刻没抓到关键稿件。
后来把调度逻辑改成了「按媒体权重动态调整抓取频率」,用 Celery 做任务编排,效果立竿见影:头部媒体抓得勤、尾部媒体抓得省,整体代理消耗降了四成,热点命中率反而上去了。这篇文章把这套设计拆开讲,代码能直接抄。
一、核心思路:权重驱动频率
每个媒体源给一个权重值(1~10),权重越高代表它越权威、更新越频繁、越值得盯。权重直接映射成抓取间隔:
plain
权重 10 → 间隔 60 秒(头部门户,盯得最紧)
权重 5 → 间隔 360 秒
权重 1 → 间隔 600 秒(边缘站点,捡漏即可)
而权重不是写死的。系统会根据每次抓取的成功率、返回的新内容数量实时微调------经常失败就降权少抓,内容更新活跃就升权多抓。这样调度会自己「长记性」。
二、环境准备
bash
pip install celery==5.4.0 redis==5.0.1 requests==2.32.3
Celery 5.4 配 Redis 做 broker 是最省心的组合。代理那一层我用的是隧道代理:固定一个入口地址,云端自动从几十万 IP 池里调度出口,你不用自己维护 IP 池和失效重试。认证是「用户名密码 + 本机出口 IP 白名单」双重校验。
三、代理与 Celery 初始化
先把代理入口和 Celery 实例搭起来。这里隧道代理有四种 IP 控制模式,新闻批量抓取最常用的是「强制切换 IP」------每个请求新建连接,出口 IP 自动轮换,匿名性最好。
python
# scheduler.py
from celery import Celery
import requests
import time
# ====== 亿牛云隧道代理固定入口(双重认证,云端自动调度出口 IP)======
PROXY_ENTRY = "t.16yun.cn:31111" # 固定入口,不用管背后几十万 IP 怎么换
PROXY_USER = "16YUN_你的用户名" # 用户名密码认证
PROXY_PASS = "16YUN_你的密码" # 另需在本机出口 IP 白名单里加过本机
# Celery 实例,broker 用本机 Redis
app = Celery("media_crawler", broker="redis://127.0.0.1:6379/0")
def build_proxy(tunnel=None):
"""构造代理地址。
tunnel 为 None 时走「强制切换 IP」模式,每个请求自动换出口;
tunnel 传一个固定整数时,注入 Proxy-Tunnel 头锁定同一个出口 IP,
适合需要登录态 / 会话保持的媒体源(模式 C/D)。
"""
auth = f"{PROXY_USER}:{PROXY_PASS}"
proxy_url = f"http://{auth}@{PROXY_ENTRY}"
proxies = {"http": proxy_url, "https": proxy_url}
headers = {}
if tunnel is not None:
# 同一个 tunnel 值 = 同一个出口 IP,多次请求复用
headers["Proxy-Tunnel"] = str(tunnel)
return proxies, headers
四、媒体权重表
权重表是调度的「配置文件」,单独拎出来方便运营同学改,不用动代码。
python
# 媒体源配置:权重越高抓得越勤
MEDIA_SOURCES = {
"财经网": {"weight": 10, "url": "https://www.caijing.com.cn/"},
"新浪科技": {"weight": 9, "url": "https://tech.sina.com.cn/"},
"36氪": {"weight": 8, "url": "https://36kr.com/newsflashes"},
"界面新闻": {"weight": 7, "url": "https://www.jiemian.com/"},
"澎湃新闻": {"weight": 7, "url": "https://www.thepaper.cn/"},
"地方论坛": {"weight": 2, "url": "http://bbs.example.com/"},
}
五、抓取任务:每个请求自动换 IP
抓取任务本身很简单,关键是把代理接好、失败能自动重试。隧道代理在重试时会自动换一个新出口 IP,等于天然带了「封了就换」的能力。
python
@app.task(bind=True, max_retries=3)
def crawl_media(self, name, url, tunnel=None):
"""抓取单个媒体源并解析入库。
name: 媒体名(用于日志和权重状态)
url: 列表页地址
tunnel: 非 None 时锁定出口 IP(会话保持),None 时强制换 IP
"""
proxies, headers = build_proxy(tunnel)
try:
# 亿牛云隧道代理在云端完成 IP 调度,这里只管发请求
resp = requests.get(url, proxies=proxies, headers=headers, timeout=12)
resp.encoding = resp.apparent_encoding
items = parse_news(resp.text) # 你的正文解析逻辑
fresh = save_to_storage(name, items) # 入库,返回本次新增条数
# 抓取结果回灌给权重自适应模块
adapt_weight(name, success=True, fresh_count=fresh)
return {"name": name, "fresh": fresh}
except Exception as exc:
# 重试时隧道代理会换新出口 IP,绕过临时封禁
adapt_weight(name, success=False, fresh_count=0)
raise self.retry(exc=exc, countdown=8)
六、调度器:每分钟决定「谁该抓了」
Celery 原生的 beat 调度是静态的,没法按权重动态算间隔。我的做法是用一个父任务每分钟跑一次,内部遍历媒体源,谁到了该抓的时间点就派发子任务。这样频率完全由权重实时算出,灵活度最高。
python
# 运行时状态:记录每个媒体的上次抓取时间和当前权重
MEDIA_STATE = {}
def calc_interval(weight):
"""权重转抓取间隔(秒)。权重 10→60s,权重 1→600s,线性插值。"""
return max(60, 660 - weight * 60)
@app.task
def scheduler_tick():
"""每分钟触发一次,按权重派发抓取任务。"""
now = time.time()
for name, cfg in MEDIA_SOURCES.items():
st = MEDIA_STATE.setdefault(name, {"last": 0.0, "weight": cfg["weight"]})
interval = calc_interval(st["weight"])
# 够间隔了才派发,避免低权重源空耗代理
if now - st["last"] >= interval:
st["last"] = now
# 高权重(>=8)用固定 IP 保持会话,其余强制换 IP
tunnel = name if st["weight"] >= 8 else None
crawl_media.delay(name, cfg["url"], tunnel)
启动就两条命令:
bash
celery -A scheduler worker -l info # 起 worker
celery -A scheduler beat -l info --schedule celerybeat-schedule # 起每分钟的 tick
七、权重自适应:让调度自己长记性
这是整套系统「动态」二字的关键。根据成功率和新增内容量调权重,连续失败就降权少抓,内容活跃就升权多抓。
python
def adapt_weight(name, success, fresh_count):
"""根据抓取结果微调媒体权重(范围 1~10)。"""
st = MEDIA_STATE[name]
w = st["weight"]
if not success:
w = max(1, w - 2) # 失败降权,省下代理配额
elif fresh_count >= 20:
w = min(10, w + 1) # 更新活跃,升权盯更紧
st["weight"] = w
跑一周下来,系统会自然地把流量向「靠谱又高产」的媒体倾斜,那种三天两头 404 或者长期没更新的源会被自动压到低频。
八、几种 IP 控制模式怎么选
隧道代理的四种模式在舆情采集里各有落点,别只会用一种:
- 强制切换 IP(默认):大部分新闻列表页批量抓,匿名性最好。
- 保持相同 IP(keep-alive + Session 复用):需要登录态、翻多页的媒体。
- Proxy-Tunnel 固定 IP(模式 C/D):同一 tunnel 值锁定一个出口,适合一个站点下多个子栏目隔离,或者 HTTPS 握手敏感的站点。
我上面的代码里 tunnel=None 走强制切换,tunnel=媒体名 走固定 IP,已经把这套取舍写进调度逻辑了。
九、实测效果
接上面这套跑了一周,几个数供参考:
| 指标 | 改造前(固定 5 分钟) | 改造后(权重动态) |
|---|---|---|
| 日均代理请求量 | 约 28 万 | 约 17 万 |
| 头部媒体热点命中延迟 | 平均 5 分钟 | 平均 60~90 秒 |
| 抓取成功率 | 82% | 95% |
| 边缘源无效请求占比 | 71% | 23% |
代理流量降了四成,该盯的反而盯得更紧------这就是「把资源花在刀刃上」。
十、小结
按媒体权重动态调频率,本质是把「抓什么、抓多勤」这件事交给数据自己决定,而不是拍脑袋写死。Celery 负责把任务可靠地派发出去,隧道代理负责把 IP 调度和换 IP 的脏活兜住,你只需要关心权重表和解析逻辑。
如果媒体源上了百个,建议把 MEDIA_STATE 落 Redis 而不是内存,worker 多实例才不会各算各的。另外 adapt_weight 的步长和上下限可以按业务调,想更激进就加大升权幅度。
这套调度跑顺了之后,后面接实时情感分析、热点告警都是水到渠成的事。