首页 文章 精选 留言 我的

精选列表

搜索[python],共10000篇文章
优秀的个人博客,低调大师

Python 实现 X 新推文推送到飞书/企业微信群:轮询、SQLite 去重与 Webhook 机器人

## 一、需求场景 很多团队都有"盯 X"的需求: - 市场团队关注竞品官方账号,一有新功能发布就要知道; - 研发团队关注几个开源项目维护者的动态; - 公关团队需要第一时间发现品牌名称被大量讨论的情况。 这些信息如果靠人工刷,既容易遗漏又占用精力。更好的方式是让程序自动发现新推文,并直接推送到团队日常使用的飞书或企业微信群。 本文实现一个轻量服务,满足以下要求: 1. 支持按**账号**和**关键词**两种方式订阅; 1. 同一条推文**只推送一次**,服务重启后也不会重复推送; 1. 首次启动时**不推送历史内容**,避免刷屏; 1. 同时支持**飞书**和**企业微信**机器人,并遵守它们各自的频率和长度限制; 1. 发送失败的消息能够**自动重试**。 ## 二、整体架构 ``` 定时触发(每 N 分钟) └→ 轮询:对每个订阅调用搜索接口,取最新推文 └→ SQLite:INSERT OR IGNORE 去重,新推文状态记为 pending └→ 发送器:取出 pending 记录,合并成消息 ├→ 飞书自定义机器人(签名 + 消息卡片) └→ 企业微信群机器人(markdown) └→ 成功标记为 sent,失败保留 pending 等待下轮重试 ``` 这里把"发现新推文"和"发送消息"拆成两个阶段,中间用 SQLite 中的状态字段衔接。这样做的好处是:即使发送环节失败(比如机器人被限流),推文也已经入库,下一轮会自动重发,不会丢失。 ## 三、订阅配置与轮询 本文示例使用第三方接口 TwexAPI 的高级搜索接口获取推文。它支持 X 原生搜索语法,`sortBy` 设为 `Latest` 时按时间倒序返回结果。账号订阅可以用 `from:` 运算符实现,多个账号用 `OR` 连接,这样一次请求就能覆盖多个账号。 ``` # config.py 示例代码 import os BASE_URL = "https://api.twexapi.io" API_HEADERS = {"Authorization": f"Bearer {os.environ['DATA_API_KEY']}"} FEISHU_WEBHOOK = os.environ.get("FEISHU_WEBHOOK") # https://open.feishu.cn/open-apis/bot/v2/hook/xxx FEISHU_SECRET = os.environ.get("FEISHU_SECRET") # 开启签名校验后获得的密钥 WECOM_WEBHOOK = os.environ.get("WECOM_WEBHOOK") # https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx SUBSCRIPTIONS = [ {"name": "竞品官号", "query": "(from:account_a OR from:account_b) -filter:replies"}, {"name": "品牌舆情", "query": "\"YourBrand\" -filter:retweets"}, ] POLL_INTERVAL = 300 # 秒 ``` ``` # poller.py 示例代码 import time import requests from config import BASE_URL, API_HEADERS def fetch_latest(query: str) -> list[dict]: """只取第一页:轮询场景下,两次轮询之间的新推文通常不会超过一页。""" for attempt in range(3): r = requests.post(f"{BASE_URL}/twitter/advanced_search/page", headers=API_HEADERS, json={"searchTerms": [query], "sortBy": "Latest"}, timeout=30) if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", 10))) continue r.raise_for_status() return [t for t in r.json().get("data") or [] if t] return [] ``` 轮询只取第一页(最多 20 条)。如果某个关键词在 5 分钟内的新推文经常超过 20 条,说明它的热度已经不适合逐条推送,应该改为"每小时汇总"的形式,或者在查询中加上 `min_faves:` 之类的条件,只推送有一定互动量的内容。 ## 四、SQLite 去重与首次运行基线 去重最可靠的方式是以 `tweet_id` 作为主键,借助数据库的唯一约束实现幂等写入: ``` # store.py 示例代码 import sqlite3 import time DDL = """ CREATE TABLE IF NOT EXISTS tweets ( tweet_id TEXT PRIMARY KEY, sub_name TEXT NOT NULL, author TEXT, text TEXT, url TEXT, created_at TEXT, status TEXT NOT NULL DEFAULT 'pending', -- pending / sent / skipped retries INTEGER NOT NULL DEFAULT 0, inserted REAL NOT NULL ); CREATE INDEX IF NOT EXISTS idx_status ON tweets(status); CREATE TABLE IF NOT EXISTS subs_seen (sub_name TEXT PRIMARY KEY); """ class Store: def __init__(self, path="tweets.db"): self.db = sqlite3.connect(path) self.db.executescript(DDL) def is_first_run(self, sub: str) -> bool: return self.db.execute("SELECT 1 FROM subs_seen WHERE sub_name=?", (sub,)).fetchone() is None def save(self, sub: str, tweets: list[dict]) -> int: """写入新推文;首次运行的订阅直接标记为 skipped,作为基线。""" status = "skipped" if self.is_first_run(sub) else "pending" n = 0 with self.db: for t in tweets: u = t.get("user") or {} cur = self.db.execute( "INSERT OR IGNORE INTO tweets VALUES (?,?,?,?,?,?,?,0,?)", (t["tweet_id"], sub, u.get("screen_name"), t.get("full_text") or t.get("text") or "", f"https://x.com/{u.get('screen_name')}/status/{t['tweet_id']}", t.get("created_at"), status, time.time())) n += cur.rowcount self.db.execute("INSERT OR IGNORE INTO subs_seen VALUES (?)", (sub,)) return n if status == "pending" else 0 def pending(self, limit=10) -> list[tuple]: return self.db.execute( "SELECT tweet_id, sub_name, author, text, url FROM tweets " "WHERE status='pending' AND retries str: string_to_sign = f"{timestamp}\n{secret}" code = hmac.new(string_to_sign.encode("utf-8"), digestmod=hashlib.sha256).digest() return base64.b64encode(code).decode("utf-8") def build_card(title: str, items: list[tuple]) -> dict: elements = [] for _id, sub, author, text, url in items: elements.append({"tag": "markdown", "content": f"**@{author}**:{text[:300]}\n[查看原文]({url})"}) elements.append({"tag": "hr"}) return { "schema": "2.0", "header": {"title": {"tag": "plain_text", "content": title}, "template": "blue"}, "body": {"elements": elements[:-1]}, } def send_feishu(title: str, items: list[tuple]) -> bool: payload = {"msg_type": "interactive", "card": build_card(title, items)} if FEISHU_SECRET: ts = int(time.time()) payload.update(timestamp=str(ts), sign=gen_sign(ts, FEISHU_SECRET)) r = requests.post(FEISHU_WEBHOOK, json=payload, timeout=10) data = r.json() if data.get("code") != 0: print("feishu error:", data) # 19021 签名失败;11232 触发限流 return False return True ``` 飞书自定义机器人的几个限制需要特别注意: - **频率限制**:单个机器人每分钟 100 次、每秒 5 次。文档还建议避开整点和半点发送,否则可能因为系统压力触发 `11232` 限流错误; - **请求体大小**:不能超过 20KB,所以单张卡片里放的推文数量要有上限,正文也要截断; - **判断成功的方式**:HTTP 状态码为 200 并不代表发送成功,要看响应体中的 `code` 是否为 0。 ## 六、企业微信群机器人 ### 1. 创建 在企业微信群中添加"消息推送"(原"群机器人"),即可获得 Webhook 地址:`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx`。地址中的 `key` 就是凭证,务必妥善保管。 ### 2. 发送 markdown 消息 企业微信群机器人支持 text、markdown、markdown_v2、图片、图文、文件、语音和模板卡片等消息类型。对于推文推送,markdown 类型是比较合适的选择:支持加粗、链接、引用和三种内置字体颜色。 ``` # wecom.py 示例代码 import requests from config import WECOM_WEBHOOK MAX_BYTES = 4000 # 文档限制 4096 字节,预留余量 def build_markdown(title: str, items: list[tuple]) -> str: parts = [f"### {title}"] for _id, sub, author, text, url in items: block = f"\n**@{author}** {sub}\n> {text[:200]}\n[查看原文]({url})" if len("\n".join(parts + [block]).encode("utf-8")) > MAX_BYTES: parts.append("\n……更多内容见下一条消息") break parts.append(block) return "\n".join(parts) def send_wecom(title: str, items: list[tuple]) -> bool: payload = {"msgtype": "markdown", "markdown": {"content": build_markdown(title, items)}} r = requests.post(WECOM_WEBHOOK, json=payload, timeout=10) data = r.json() if data.get("errcode") != 0: print("wecom error:", data) return False return True ``` 企业微信的限制: - **长度**:markdown 内容不超过 4096 字节(注意是字节,UTF-8 编码下一个汉字占 3 个字节),所以代码中按字节而不是按字符数判断; - **频率**:每个机器人每分钟不能超过 20 条消息。这个限制比飞书严格得多,因此**合并发送**几乎是必需的; - **判断成功**:看响应中的 `errcode` 是否为 0。 另外,markdown 消息中引用块 `>` 只对当前行生效。如果推文正文中有换行,建议先把换行替换为空格,再放进引用块,否则排版会乱。 ## 七、把各部分串起来 ``` # main.py 示例代码 import time from config import SUBSCRIPTIONS, POLL_INTERVAL, FEISHU_WEBHOOK, WECOM_WEBHOOK from poller import fetch_latest from store import Store from feishu import send_feishu from wecom import send_wecom def clean(items): return [(i, s, a, (t or "").replace("\n", " "), u) for i, s, a, t, u in items] def run_once(store: Store): for sub in SUBSCRIPTIONS: n = store.save(sub["name"], fetch_latest(sub["query"])) if n: print(f"[{sub['name']}] 新推文 {n} 条") time.sleep(1) batch = clean(store.pending(limit=8)) # 每次最多合并 8 条 if not batch: return title = f"X 动态提醒({len(batch)} 条)" ok = True if FEISHU_WEBHOOK: ok &= send_feishu(title, batch) if WECOM_WEBHOOK: ok &= send_wecom(title, batch) store.mark([b[0] for b in batch], ok) if __name__ == "__main__": store = Store() while True: try: run_once(store) except Exception as e: # 单轮失败不影响下一轮 print("round failed:", e) time.sleep(POLL_INTERVAL) ``` 这里有一个简化处理:同时配置两个渠道时,只要有一个发送失败,整批都会在下一轮重发,成功的那个渠道可能收到重复消息。如果对重复比较敏感,可以在表中为每个渠道分别记录发送状态。 ## 八、部署与运维 1. **运行方式**:可以直接用 `systemd` 守护上面的循环进程,配置 `Restart=always`;也可以去掉 `while` 循环,改为每 5 分钟由 cron 执行一次 `run_once`,配合 `flock` 防止重叠; 1. **密钥管理**:Webhook 地址、签名密钥和数据接口密钥都通过环境变量注入,不写入代码,也不提交到仓库。飞书和企业微信的文档都特别提醒,不要把 Webhook 地址公开到 GitHub 或博客等地方; 1. **轮询间隔**:间隔越短,消息越及时,但调用量也越大。对于官号动态,5~10 分钟通常就足够了; 1. **数据清理**:定期删除较早的记录,例如只保留 30 天,既能控制数据库大小,也符合"最小保留"原则; 1. **监控**:统计连续失败次数,超过阈值时通过另一个渠道告警,避免推送服务"静默失效"。 ## 九、可以继续扩展的方向 - **关键词高亮与分级**:命中核心词时使用红色卡片模板(飞书 `template` 改为 `red`),并 @ 相关负责人; - **互动阈值过滤**:只推送点赞或转发超过一定数量的推文,降低噪音; - **大模型摘要**:对长推文或串推先生成一句话摘要再推送; - **翻译**:把英文推文翻译成中文,方便团队阅读。 ## 十、合规提示 只订阅和推送公开内容;推送时附原文链接,而不是大段复制原文;不针对个人做持续监控;遵守数据来源平台的服务条款、企业内部的信息安全规定和相关法律法规。 ## 十一、小结 1. 把"发现新推文"和"发送消息"解耦,中间用 SQLite 记录状态,可以同时解决去重、重启恢复和失败重试三个问题; 1. 首次运行基线可以避免新增订阅时刷屏; 1. 飞书签名的关键是"拼接字符串作为 HMAC 密钥、对空消息签名",判断成功要看响应体中的 `code`; 1. 企业微信每分钟 20 条、markdown 4096 字节的限制,决定了必须合并发送并按字节截断。 > 说明:文中代码为示例代码,飞书和企业微信的消息格式及限制以其官方文档的最新版本为准。 ## 参考资料 - 飞书自定义机器人使用指南:[https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot) - 企业微信消息推送配置说明:[https://developer.work.weixin.qq.com/document/path/91770](https://developer.work.weixin.qq.com/document/path/91770) - 示例数据接口文档:[https://docs.twexapi.io](https://docs.twexapi.io)

优秀的个人博客,低调大师

Python 的 import 到底做了什么?首次 2 毫秒与缓存 1 微秒,中间隔着一个 sys.modules

**结论先行**:同一个模块,**首次 import 花 2048 微秒,第二次只要 1.1 微秒——差了约 1800 倍**。中间隔着的不是魔法,是 `sys.modules` 这个全局缓存。所以"函数里写 import 会不会很慢"这类担心基本是多余的:实测函数内重复 import 10 万次,每次只要 193 纳秒。本文用实机数据把 import 的完整流程拆一遍,顺带讲清 `__pycache__` 和循环导入这两个高频坑。 ## 1. 先看差距:2048µs vs 1.1µs ```javascript import sys, time del sys.modules['json'] # 强制清掉缓存 t0 = time.perf_counter_ns(); import json; t1 = time.perf_counter_ns() print((t1-t0)/1000, 'us') # 2048.6 us ≈ 2 毫秒 t0 = time.perf_counter_ns(); import json; t1 = time.perf_counter_ns() print(t1-t0, 'ns') # 1100 ns ≈ 1.1 微秒 ``` 实测(CPython 3.13.14):首次 import json 花 **2048.6µs**,第二次 **1100ns**,**差约 1800 倍**。首次的钱花在哪?找文件、读源码、编译成字节码、执行模块顶层代码、注册缓存——一整套流程;第二次只干一件事:去 `sys.modules` 字典里查个 key。 ## 2. sys.modules:import 的第一站,也是唯一的缓存 `sys.modules` 是一个普通字典,key 是模块名,value 是模块对象。解释器启动后这里已经躺了一百多个模块: ```javascript import sys print(len(sys.modules)) # 我这台机器上是 107 print('json' in sys.modules) # import 之后 True ``` **任何 **`import x`**,第一步永远是 **`sys.modules`** 里查 **`x`。查到了就直接返回引用,后面的事全省了。这就是为什么第二次 import 只要 1 微秒——一次字典查找的成本。理解这一点,就理解了 import 性能的 90%。 ## 3. 首次 import 的完整流水线(查不到缓存才走这里) 缓存 miss 之后,CPython 按顺序做这几件事: 1. **找**:遍历 `sys.meta_path` 里的 finder(内置模块、冻结模块、路径查找器),路径查找器再按 `sys.path` 逐个目录翻; 2. **读**:找到 `.py` 文件后读源码; 3. **编译**:源码编译成字节码(这步最贵,毫秒级); 4. **缓存字节码**:默认把编译结果写进 `__pycache__/xxx.cpython-313.pyc`,下次跳过第 3 步; 5. **执行**:新建模块对象,执行模块顶层代码(顶层语句只在这一步跑一次); 6. **注册**:挂进 `sys.modules`,之后的 import 全部命中缓存。 拿一个真实模块实测(200 个函数定义的 .py,本测试环境关闭了字节码缓存,正好暴露纯编译成本): ```javascript # 首次:读源码 + 编译 + 执行 → 6153.2µs # 清掉 sys.modules 后再 import:3288.0µs(没有 .pyc 可省,只能重编译) ``` **默认环境下第二次磁盘级 import 会读 **`__pycache__`** 里的 .pyc,省掉编译那一步。** 我这个沙箱把 `sys.dont_write_bytecode` 设成了 True,`.pyc` 没写出来,所以两次都得重新编译——这组"反常"数据反而把 .pyc 缓存的价值衬托得很清楚:有它,省的是毫秒级的编译;没它,每次启动都要付一遍。 ## 4. 函数里写 import 到底贵不贵?193 纳秒 很多人说"import 要写在文件顶部,函数里写会慢"。实测一下: ```javascript def f(): import json return json import timeit print(timeit.timeit("f()", globals={'f': f}, number=100000)) # 0.019s ``` **函数内 import 10 万次,总共 19ms,每次 193 纳秒。** 因为 `json` 早就在 `sys.modules` 里,函数内的 import 就是一次字典查找加一次局部变量绑定。真正要注意的不是性能,而是另外两件事:**循环导入**和** import 时机副作用**(顶层代码只在首次执行一次,靠函数内 import 延迟触发是常见技巧)。 ## 5. 循环导入:唯一会把 import 玩坏的场景 ```javascript # a.py import b X = 'a' # b.py import a # a 还没执行完,sys.modules 里的 a 是"半成品" print(a.X) # AttributeError ``` 原理用本文的机制一句话说清:**import a 时,a 先被塞进 **`sys.modules`** 再执行顶层代码**;执行到一半 b 又去 import a,此时缓存里虽有 a,但它的顶层代码还没跑完,`X` 还不存在——拿到的是一个"半成品"模块。解法就三条:把 import 下沉到函数里(利用 193ns 的廉价缓存查找)、抽公共模块、或者改用 `import a` 而非 `from a import X`(推迟到用的时候才取属性)。 ## 6. 对照小结(收藏这张表) |操作|实测耗时|背后动作| | --- | --- | --- | |首次 import json|2048.6µs|找文件+读+编译+执行+注册| |缓存命中 import|1100ns|sys.modules 查字典,快约 1800 倍| |函数内重复 import|193ns/次|同上,可放心写| |无 .pyc 时重编译(200 函数模块)|3288µs|.pyc 缓存被禁用的代价| |`import a` vs `from a import X`|查找相同|后者多一次属性绑定,且扛循环导入更差| **小结**:import 的性能账,本质是"一次完整流水线 + 之后全是字典查找"。`sys.modules` 管内存级缓存(微秒级),`__pycache__` 管磁盘级缓存(省编译)。**函数内 import 不可怕,循环导入才是真要命的那个。** 你项目里写过函数内的 import 吗?是 deliberate 的延迟加载,还是被循环导入逼的?评论区聊聊。 > 实测环境:CPython 3.13.14(64 位 Windows)。注:本测试环境 `sys.dont_write_bytecode=True`,故 .pyc 相关数字反映的是"无字节码缓存"场景,与默认环境的差异已在文中标注。

优秀的个人博客,低调大师

Python 列表推导比 for+append 快多少?实测 100 到 10 万,结论和你想的不一样

「写列表别用 for+append,用列表推导更快」——这话对一半。我在 CPython 3.13.14 上跑了一组实测,结论有点反常识。 ## 实测:三种写法同台比 对比三种:列表推导 `[i*2 for i in range(n)]`、预分配后索引赋值、裸 `append` 循环。单位微秒,取每组最快一轮: |规模 n|列表推导|预分配循环|裸 append 循环| | --- | --- | --- | --- | |100|3.6µs|3.5µs(0.96x)|4.4µs(1.21x)| |1,000|53.4µs|53.0µs(0.99x)|63.7µs(1.19x)| |10,000|562µs|595µs(1.06x)|640µs(1.14x)| |100,000|6,723µs|6,912µs(1.03x)|7,055µs(1.05x)| ## 结论一:推导和「预分配循环」几乎一样快 很多人以为列表推导是某种魔法加速,其实它底层就是「在 C 层先算好长度、一次性分配、再填值」。如果你用 `[0]*n` 先把位置占好,再用 `lst[i]=...` 索引赋值,速度和推导几乎持平(有的规模还略快一点)。 ## 结论二:真正慢一丁点的是「裸 append」 裸 `append` 循环比推导慢 5%~20%,而且**规模越小差距越明显**(n=100 时慢 21%,n=10 万时只慢 5%)。原因很简单:每次 `append` 都是一次方法调用,外加「容量够不够、不够就扩容」的检查;而推导和预分配把这件事一次性做完了。 ## 结论三:别神话推导,也别怕 append - 数据量小(几百以内):三种写法随便选,差距在微秒级,可读性优先。 - 数据量大(十万、百万):优先列表推导,或者预分配索引赋值;尽量少用裸 `append` 循环。 - **真正差距大的不是这里**——是它隔壁的事:用 `+=` 在循环里拼字符串(前面一篇聊过,比 `join` 慢几十倍),那个才是该优化的重灾区。列表这一块,推导和 append 差距其实很小。 ## 附:推导和生成器别搞混 如果只要遍历、不想要整个列表躺在内存里,把方括号换成圆括号 `(i*2 for i in range(n))` 就是生成器表达式——它**不分配**整个列表,内存占用 O(1),迭代到哪算到哪。要「全在内存里就位」用列表推导,要「流式处理」用生成器。 一句话总结:**列表推导 ≈ 预分配循环 > 裸 append(慢一点点)**。把「推导秒杀 append」改成「两者差不多,append 别裸着跑百万次」更准确。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册