爬虫调度系统设计:用 Celery 实现按媒体权重动态调整的抓取频率控制

做舆情采集的朋友大多遇到过这个两难:固定频率去抓全网媒体,要么低频漏掉突发热点,要么高频把代理流量烧光还被站点限流。我之前一套系统就是这么垮的------所有媒体一刀切每 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 的步长和上下限可以按业务调,想更激进就加大升权幅度。

这套调度跑顺了之后,后面接实时情感分析、热点告警都是水到渠成的事。

相关推荐
Code额2 小时前
Python 连接 DeepSeek API,OpenAI
开发语言·python·ai·ai编程
Suhan423 小时前
SQLAlchemy的两种查询query()查询、stmt=select()查询
数据库·python·sql·oracle
AAA@峥3 小时前
Python 基础开篇:认识 Python、版本选择、环境部署与首个 HelloWorld
开发语言·python
Metaphor6923 小时前
使用 Python 快速在 Word 文档中插入图片
python·word
2601_966871404 小时前
Python 爬虫 + 数据挖掘实战:数据抓取与深度挖掘分析全流程
爬虫·python·数据挖掘
青禾8374 小时前
Linux 系统信息、权限管理与 Python 并发编程(协程与线程)完全指南
linux·python
曲无忆4 小时前
密码学三大核心技术解析
python·密码学
qq21084629534 小时前
在python中什么是 self 和 cls?
开发语言·前端·python