简介:Telegram社群运营者常需处理敏感词过滤、定时提醒与广告推送等重复事务,这套自动化工具包正是面向此类场景的轻量级解决方案。工具覆盖敏感词监控、定时消息群发、图片发送与多任务配置管理,既能自动拦截违规内容,也可按设定时间向群组推送提醒或推广信息,适合有一定Node.js基础的社群运营人员或开发者快速部署使用。压缩包共8个文件,以js脚本、txt词库、json与csv配置数据为主,另附docx与md格式说明文档,整体仅37KB,目录小巧而清晰。目前已有70人学习/下载。通过阅读脚本与配置样例,可以直接借鉴多任务调度、敏感词匹配与Bot API调用等实现思路;配套说明文档对配置项和任务启停有简明提示,即使首次接触Telegram Bot也能据此定制自己的社群机器人,省去从零搭建的繁琐过程。
1. 一套Telegram机器人自动化工具的拆解:从敏感词过滤到多任务调度
一个五百人社群里,凌晨两点弹出三条广告链接,早上八点管理员才看到。手动禁言、移出、再盯群,这类重复劳动占据社群运营每天至少一个小时。如果把“谁说了不该说的话”“几点该发什么提醒”“这条通知要发给哪几个群”交给机器人去判断和执行,运营只需要在配置文件里改改词库和定时规则。基于Telegram机器人开发的群组管理自动化工具,核心就两件事:把敏感词监控系统做成实时过滤管道,把定时消息群发扩展成支持图片发送的多任务调度器。敏感词过滤不只是字符串匹配,定时群发也不只是sleep加send——前者要处理词库维护和误杀率,后者要面对任务配置管理、调度一致性和失败重试。这套方案适合社群运营负责人和想用Bot API做自动化接入的开发者,读完能直接从最小实现起步,逐步改造成自己的管理后台。
2. 机器人怎么“看到”群消息:轮询机制与管理员的首次握手
2.1 长轮询与Webhook:个人项目默认选哪种
Telegram Bot API不提供长连接推送,机器人获取消息只有两条路:长轮询getUpdates和Webhook。长轮询是客户端向服务端发起长时间挂起的HTTP请求,有新消息立即返回,没消息就挂到超时再空手返回;Webhook则是Telegram主动把更新POST到你的公网HTTPS地址。个人项目或中小型运营群,优先用长轮询,原因是零公网依赖、不需要域名和HTTPS证书、断线自恢复逻辑简单。等做到多实例部署或消息量上来,再迁移Webhook不迟——两者的消息处理器代码可以完全复用,差别只在启动方式。
轮询这个机制在工程上有一个必踩的坑:offset确认。Telegram只会把每一条update投递给一个客户端,你通过getUpdates拿到消息后,必须把最后一次处理过的update_id透过参数offset告诉Telegram,否则下一次轮询会重复收到同一条消息。python-telegram-bot这类库在处理完整个回调后自动确认offset,不会出现重复,但如果你自己裸写HTTP接口,这个细节很容易漏。
2.2 最小可运行Bot:先让机器人活起来
用python-telegram-bot库搭建一个能对话的最小机器人:
import asyncio from telegram import Update from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text("机器人已上线,敏感词过滤和定时任务正常运行。") async def main() -> None: app = ApplicationBuilder().token("123456:BOT_TOKEN_HERE").build() app.add_handler(CommandHandler("start", start)) await app.run_polling(drop_pending_updates=True) if __name__ == "__main__": asyncio.run(main())这段代码的核心是ApplicationBuilder构建应用实例,add_handler注册命令处理器,run_polling启动事件循环。drop_pending_updates=True表示丢弃机器人离线期间积压的旧消息,否则群成员会收到一堆过期的/start响应。token从@BotFather创建机器人时获取,格式是数字ID加冒号加字母串。
要让机器人能处理群消息,还需要在@BotFather里执行/setprivacy,把Privacy Mode关掉。Privacy Mode开启时,机器人只在被命令提及、回复、@mention时才能看到消息内容,敏感词监控根本收不到群聊文本。这一步不做,后面的过滤器全是空的。
2.3 消息到达后的处理链路:Update到Handler再到Callback
群组管理工具的所有功能都建立在同一个处理管道上:Telegram服务器把新消息封装成Update对象,Application根据注册的Handler类型进行分发,匹配成功的消息进入你的Callback函数。CommandHandler匹配以/开头的命令,MessageHandler匹配普通消息,可以附加过滤器条件(比如只处理文本、只处理图片、只处理@提到机器人的消息)。
这里有一个关键设计:敏感词监控和定时群发不要混在同一个入口函数里。敏感词过滤挂在MessageHandler上,每一条进来的消息都要过一遍词库。定时发送则挂在JobQueue调度器上,按配置触发独立的发送协程。两者靠一个共享的配置对象通信,互不阻塞。这样设计的好处是,即使某一次定时群发由于网络超时卡住了,也不会拖慢消息过滤的响应速度——过滤链路对群成员来说必须是实时的,毫秒级响应,容不得排队。
3. 敏感词过滤的工程实现:词库结构、匹配算法与误杀控制
3.1 词库和规则用JSON组织,别写死在代码里
敏感词监控系统最怕的是改一个词要重发一次代码。词库和规则必须独立于程序逻辑,放在配置文件里,格式用JSON或YAML都行,我一般用JSON,免去缩进歧义。一个兼顾简单和可扩展的词库结构长这样:
{ "groups": { "-1001234567890": { "mode": "strict", "keywords": ["广告", "加微信"], "regex_patterns": ["[a-z]{5,}\\.(com|cn|xyz)", "\\d{11}"], "action": "delete_and_warn", "warn_message": "消息含违规内容,已删除。" } }, "global": { "keywords": ["菠菜", "刷单"], "regex_patterns": ["t\\.me/[a-zA-Z0-9_]+"], "exclude": ["西瓜", "红包"] } }这个结构里,groups按群ID配置不同策略,global是全局兜底规则,exclude字段是白名单——匹配到敏感词但命中白名单时不触发处理。regex_patterns用来拦截有规律但无法穷举的变体,比如五天有效期的短域名、连续11位数字的微信号。这种“关键词加正则加白名单”的三层结构明显优于单一关键词表,因为社群里的广告文案会刻意做变体,比如“加徽信”“扣1送”这类形近字避检。
3.2 匹配顺序和触发逻辑:先粗筛再精判
敏感词匹配的核心函数可以写成这样:
import re import json def load_rules(path: str) -> dict: with open(path, "r", encoding="utf-8") as f: return json.load(f) RULES = load_rules("rules.json") def filter_result(text: str, chat_id: int) -> dict | None: chat_id_str = str(chat_id) for word in RULES["global"]["exclude"]: if word in text: return None for word in RULES["global"]["keywords"]: if word in text: return {"rule": "global", "matched": word, "action": "delete_and_warn"} for word in RULES["global"]["regex_patterns"]: if re.search(word, text): return {"rule": "global_regex", "matched": word, "action": "delete_and_warn"} if chat_id_str in RULES["groups"]: group_rules = RULES["groups"][chat_id_str] for word in group_rules["keywords"]: if word in text: return {"rule": "group", "matched": word, "action": group_rules["action"]} for pattern in group_rules["regex_patterns"]: if re.search(pattern, text): return {"rule": "group_regex", "matched": pattern, "action": group_rules["action"]} return None先检查白名单接着检查全局敏感词再检查群级词库,这个顺序是有道理的。白名单完全匹配时直接放行,避免“西瓜”被“瓜”这个单字词命中;全局词库前置可以拦截那些所有群都不该出现的通用广告;群级词库放在最后,是因为不同群的语境差异很大——游戏群里的“代练”可能要被过滤,相亲群里的“代练”反而是正常讨论。位次靠后的规则更灵活,也因为走了前面的过滤开销更低。
3.3 处理动作和误杀回退:给管理员留一条后悔通道
消息命中敏感词后,动作应该是异步多步的:删除原消息、发送警告提示、记录日志到指定管理群。删除用await message.delete(),警告提示用await chat.send_message(warn_message),但要注意警告消息不要点击即删,留几秒钟让群成员意识到规则的存在。
误杀在高并发群普遍存在。避免误杀的一个有效做法是“二次确认窗口期”:机器人首次命中敏感词时不直接删消息,而是对消息发送者执行一次禁言30秒,同时把消息原文转发到管理群,由管理员确认。如果管理员回复“放行”,就解除禁言;回复“确认”,才执行删除。写操作比读操作慢,把高频删除降级为低频人工确认,反而提升了整体响应速度。群里真正对体验有影响的是“长期没人管”,而不是“偶尔漏一条”。
4. 定时消息群发与图片发送:调度器选型、投递一致性与文件复用
4.1 JobQueue调度而不是自己写sleep循环
定时消息群发如果自己维护一个while循环加sleep,进程重启一次就得重算所有定时点,并且难以执行“每周一到周五早九点”这类cron规则。python-telegram-bot自带JobQueue,底层基于APScheduler,支持run_daily、run_daily加days筛选、run_repeating、run_once四种注册方式,调度规则成熟,重启后可以反查下一次触发时间。
一段同时支持文字和图片的群发任务代码:
from datetime import time, datetime from telegram import Bot from telegram.ext import ContextTypes, JobQueue async def send_scheduled_message(context: ContextTypes.DEFAULT_TYPE) -> None: job = context.job chat_id = job.data["chat_id"] text = job.data["text"] image_path = job.data.get("image_path") if image_path: with open(image_path, "rb") as photo: await context.bot.send_photo( chat_id=chat_id, photo=photo, caption=text, parse_mode="HTML" ) else: await context.bot.send_message(chat_id=chat_id, text=text, parse_mode="HTML") def register_jobs(job_queue: JobQueue, tasks: list[dict]) -> None: for task in tasks: job_queue.run_daily( send_scheduled_message, time=time(hour=task["hour"], minute=task["minute"]), days=tuple(int(d) for d in task["days"]), data=task, name=task["name"] )job.data里塞了发送目标、正文、图片路径、可见范围规则,send_scheduled_message执行时再取出来用。days参数传Monday到Sunday对应的数字,0是周一,6是周日。run_daily注册的是墙上时钟时间,不随进程重启丢失——只要进程重启后重新加载配置注册一遍即可,该补发的漏发逻辑在后面单独处理。
4.2 图片发送用file_id缓存,别重复传文件
Telegram发送图片有两种方式:传multipart文件流,或者传已经存在于Telegram服务器上的file_id。相同图片重复发送时,第二次应该直接传file_id而非原始文件,因为file_id的发送不需要重新上传,速度更快、流量消耗更低。
首次发送后拿到message.photo最后一个尺寸的file_id,让它入缓存:
async def send_with_file_id_cache(bot: Bot, chat_id: int, image_key: str, cache: dict, text: str = "") -> None: cached_id = cache.get(image_key) if cached_id: await bot.send_photo(chat_id=chat_id, photo=cached_id, caption=text) return with open(f"assets/{image_key}", "rb") as photo: sent = await bot.send_photo(chat_id=chat_id, photo=photo, caption=text) cache[image_key] = sent.photo[-1].file_id每次运行前按目录扫描构建一次file_id缓存:先尝试从数据库或Redis读,没有就发第一次并把返回的file_id存下来。file_id在Telegram侧是全局复用的,另一个机器人拿不到你的file_id,但同机器人内部跨群发送没有限制。注意事项:file_id绑定的是机器人、图片尺寸和上传方式,普通尺寸和压缩尺寸的file_id不通,所以缓存key要含尺寸标识。
4.3 定时任务的持久化和补发策略
进程重启后,JobQueue里的任务会丢失,需要重新注册。补发逻辑按“执行时间戳”判断:配置里记录每条任务上次成功投递的时间,重启加载时检查该时间距离当前是否超过任务周期,如果已错过则立即触发一次补发,不过只补最近一次,不补中间累积的多条。这个策略比“全部补发”更温和,比“不补发”更可靠,适合运营向的提醒推送场景。注意JobQueue本身不提供持久化,任务状态要自己维护在SQLite或Redis里。
5. 多任务配置管理:执行失败时的任务状态、暂停恢复与热重载
5.1 任务配置的结构化设计:每个任务都是独立可开关的单元
多任务配置管理的核心思路是把每条群发任务的元数据定义成一个字典,后续对任务的增删改只动配置文件,不需要改代码。一个任务配置片段:
{ "name": "daily_news_report", "enabled": true, "chat_ids": ["-1001234567890", "-1009876543210"], "trigger": { "type": "daily", "hour": 9, "minute": 30, "days": [0, 1, 2, 3, 4, 5] }, "payload": { "text": "今日运营日报已发布", "image_path": "assets/news_today.png" } }enabled字段是关键开关,允许运营在不需要删除配置的情况下暂停某条任务。对已注册的Job,暂停动作是job.schedule_removal();恢复动作是删除后按原配置重新注册一次。chat_ids支持数组,意味着同一条内容可以同时推送到多个群,Telegram的sendMessage天然支持按chat_id逐群发送,没有批量接口,所以要遍历发送并且对失败逐项计账。
5.2 热重载机制:配置文件改动后/TODO_RELOAD动态生效
配置改动不等于立即生效。热重载一般有两种实现:监听文件mtime变化自动加载,或者用/reload命令手动触发。后者在运营场景里更可控,避免配置文件还在编辑中时触发了部分加载。命令处理器如下:
async def reload_config(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if str(update.effective_user.id) not in ADMIN_IDS: return for job in context.job_queue.jobs(): job.schedule_removal() new_task_list = load_config("tasks.json") register_jobs(context.job_queue, new_task_list) await update.message.reply_text(f"配置已重载,当前活跃任务数:{len(new_task_list)}")这里有一个容易踩的坑:schedule_removal不是同步删除,而是在当前周期结束后才真正移除任务。如果删除后立即用同一个任务名重新注册,可能出现新老Job同时存在的冲突。稳妥的做法是重载后先等待一个极短的间隔,或给任务名加版本后缀,保证同名任务不重叠。命令的权限校验不能省,需要预先配置管理员ID列表,否则任何群员都能通过/reload把定时任务全部重置。
5.3 任务运行状态的观测:日志里记录什么才会有用
一段可观测的日志格式如下:
2025-06-01 09:30:00.123 | INFO | job=daily_news_report | action=trigger | chat=-1001234567890 | status=ok | msg_id=12345 | duration_ms=128 2025-06-01 09:30:00.456 | INFO | job=daily_news_report | action=trigger | chat=-1009876543210 | status=error | reason=chat_id_invalid | duration_ms=223每条日志里必须包含任务名、目标chat_id、动作类型、状态、错误原因和时间开销。chat_id_invalid这个错误在群组管理里极其常见——群组被删除或机器人在该群被移出后,sendMessage返回400 Bad Request。日志里看到这个错误比看到timeout更有价值,它意味着配置里的目标群已经不存在了,需要从配置中清理。发送失败的聊天在连续失败3次后要从任务目标列表里临时摘除,等待下个周期再测试恢复,防止每次触发都做无意义的重试拉低整体效率。
6. 防守升级:几条让自动化工具不惹祸的边界约束
6.1 消息去重:同一个更新别触发两次动作
群组里经常出现机器人因为Webhook和轮询同时开着,或者轮询offset没有正确确认,把同一条消息处理两遍的情况,后果是敏感词警告重复发、定时补发重复推。处理办法是在收到Update时做一次基于update_id的幂等判断:用一个字典存最近2000条update_id,命中就丢弃。由于update_id是单调递增的,超过字典容量时只需要从最小键开始裁剪,保持窗口始终是最近一段时间的消息窗口。
6.2 限流应对:429错误要读Retry-After头
Telegram Bot API对消息发送有限频控制,当单位时间内发送量超过阈值时会返回429,错误响应里带Retry-After字段,告诉你要等多少秒。直接在异常处理里读这个字段:
except telegram.error.TimedOut as e: bot.logger.error(f"timed_out, job={job_name}, chat={chat_id}") except Exception as e: if "429" in str(e) or "Too Many Requests" in str(e): retry_after = e.retry_after if hasattr(e, "retry_after") else 10 await asyncio.sleep(retry_after)把retry_after读出来sleep足够的时间再重试,比直接退避空等有效得多。另外多群群发时不能按配置顺序依次发送,应该随机打乱聊天列表后再发送,避免固定顺序导致前几个群一直先收到消息,也给限频留出错峰空间。对多个群的群发操作,我一般会把发送任务拆成小批次,每批5条间隔0.5秒,而不是40个群一口气全部发完。
6.3 图片素材加载失败时,定时任务要降级而不是抛异常
图片路径配错或图片文件被误删除时,定期任务会在open时抛FileNotFoundError。降级策略是捕获这个异常后改为发送纯文字版本,并在日志显著位置标记text_fallback_triggered。对运营而言,推送任务比图片本身重要,图片缺失只是不优雅,通知丢失才是事故。在send_scheduled_message函数的图片打开操作上下手,确保任何分支都不让异常传播到JobQueue导致后续任务全部中断。
验证整套自动化工具时,一个有效的自测思路是:用一个测试群启用定时任务,把触发时间调到下一分钟,观察日志里的trigger行是否如期出现;再发一条含敏感词的消息,确认删除动作在2秒内完成且警告文本正确。把这些检查项固化成/health命令的输出,每次修改代码后跑一遍,比人工等一个完整周期更快发现回归问题。
本文还有配套的精品资源,点击获取