简介:这套工具包面向需要核查指定QQ号社交关联的用户与有一定爬虫基础的开发者,解决批量获取某个QQ号已加入全部群组结构化信息的痛点。工具由两个脚本组成,一个负责快速判断目标账号是否有效并初步探测活跃群关系,另一个进一步抓取群聊ID、群名称、成员数及创建时间,所有结果以JSON格式保存在本地,方便后续导入Excel或数据库做二次整理。压缩包共6个文件,约7KB,除两个核心脚本外还包含依赖清单、README说明与辅助配置文件;README中提供环境配置、使用流程及常见报错处理,SJT-code目录另附协议解析参考代码。依据描述,工具依赖相关爬虫库,需用Cookie或扫码登录后的会话信息完成身份验证。目前已有94人学习下载,适合个人账号自查或授权范围内数据整理,且须符合腾讯软件许可及反不正当竞争法要求。
1. 这个标题说的事,先拆掉那层"玄学"
这个标题的关键词是"一键""指定QQ号"和"全部群组列表"。先说结论:如果你以为它是输入任意一个QQ号就能把对方加过的群全部列出来,那这脚本做不出来,也不该做。QQ没有开放"按QQ号查群关系"这类接口,任何框架拿不到没有登录态、没有关系链的数据。真正能落地的是两种合法场景:一是这个QQ号已经在本机保持登录态,你通过本地服务把它的会话数据读出来;二是这个号是你托管管理的机器人号,扫码授权后拿群列表。除此之外,走错一步就会被封号、被风控,或者被无良工具割韭菜。这篇文章要讲的就是:在授权边界内把"一键获取群列表"真正跑通,顺便把群名、成员数、缓存、定时对比都做成一份能直接复现的Python脚本。
2. 为什么不是"随便一个QQ号都能查":QQ的数据边界与两条可行路线
2.1 理解QQ客户端侧的数据归属逻辑
任何一个QQ客户端,登录之后能看到的群列表都来自当前登录账号的会话数据,而不是服务端按需查询的开放接口。你在手机和PC端能看到的"我的群聊""创建群聊""加入的群",本质上是本地消息与关系链缓存,加上服务端对该会话的增量同步结果。服务端并不会因为你在程序里调用了一个接口,就允许你传入任意QQ号去查询对方的关系链,这是腾讯侧的安全模型,不是技术做不到,而是协议不提供。
这一点直接决定了脚本的上限:你能拿到的群列表,必然属于"当前登录的账号"。如果标题里说的"指定QQ号"就是你自己手里的号、你负责管理的学生号、公司配发的员工号,那完全可行;如果"指定QQ号"指的是随便一个路人的号,那趁早换方案。常见的替代做法是"共同群反查":你先拿到自己的群列表,再逐群拉成员,筛出目标QQ号出现在哪些群里,从而知道你和对方共同在哪些群。这个思路后面第5章会给出完整实现,它是合规且可做的。
想明白这个边界之后,选型就简单了。QQ客户端本身不给外部程序提供稳定的HTTP接口,所以主流做法是依赖两类东西:一类是基于新版QQ客户端(NT架构)的本地服务框架,把客户端的协议能力包装成本地HTTP/WebSocket接口;另一类是标准的机器人协议实现,通过扫码或设备授权方式登录,然后对外提供统一的动作调用。前者更适合"本机已有登录号"的场景,后者更适合"要稳定跑在服务器上"的场景。
2.2 两条可行路线选型:本地框架与托管协议的取舍
先把两条路线摆出来对比,你再决定用哪条。路线A叫"本地会话读取",核心是:在一台Windows/Linux机器上安装某个基于NTQQ的本地服务程序,用要查的QQ号扫码登录,然后程序会监听一个本地端口,Python脚本向这个端口发HTTP请求,拿到群列表、群成员、消息等结构化数据。它的优点是部署快、数据全、不依赖外部服务器;缺点是登录态有失效周期,需要定期扫码续期,而且它只能服务"这台机器上登录的这个号"。
路线B叫"机器人托管接入",核心是:用一个专门跑机器人号的框架,把QQ号登录后接入到标准协议SDK,脚本同样通过HTTP调用拿群列表。它的优点是稳定、支持多账号实例、适合长期跑任务;缺点是部分实现会要求设备锁验证,新号登录容易被风控拦截,且对个人号频繁拉接口一样有风险。
如果你只是"拿自己的号跑一次脚本"做数据盘点,选路线A,十到二十分钟就能跑通。如果你要给某公司管几十个员工号做群合规检查,选路线B,但要把登录风控考虑进去。两条路线在Python脚本这一层其实长得一模一样,都是HTTP请求加JSON解析,所以下面的代码两种场景通用。这也是这个方向值得投入的原因:协议壳会变,HTTP接口的调用模式几乎不会变。
2.3 与直连协议对比:为什么不自己写协议
有经验的开发者可能会问:能不能不用框架,直接用Python模拟QQ客户端的协议包,自己握手、自己拉群列表?我的建议是别碰。原因有两个:一是QQ的私有协议变动频繁,签名算法涉及设备信息、imei、版本号、会话密钥的联合签名,没有稳定的公开资料,自己逆向的成本极高且版本一更新就废;二是协议侧明文规定不允许非官方客户端登录,用自写协议登录账号,账号被冻结是小概率事件里的大概率结果。
所以这个方向的正解是"站在框架之上写业务脚本"。你把协议握手、加密、签名、心跳这些事情全部交给本地服务完成,Python只负责调用接口、解析数据、做业务处理。这样做的好处有三个:第一,代码量小,核心脚本一百行以内;第二,稳定性高,框架更新跟得上客户端版本,你不用担心签名算法变化;第三,权限边界清晰,框架的权限模型就是"登录号本人",你做出来的东西既合规又不容易被封。下面第3章就按这个思路,从部署到脚本完整走一遍。
3. 把"一键获取群列表"跑起来:本地服务部署与Python调用
3.1 部署本地本地服务:从下载到扫码登录
这里的部署目标是:让要查的QQ号在本地完成一次登录,并对外暴露一个HTTP接口。常见做法是使用一个基于NTQQ的本地服务程序(这类程序一般以绿色压缩包或单可执行文件发布),把服务端和一个简易配置写入同一个目录。我先说通用步骤,再给启动命令。
第一步:在要部署的机器上准备好运行环境。Windows下一般是直接运行exe;Linux下需要先确认系统库和依赖,很多实现要求GLIBC版本较新。我一般建议先用Windows本机跑通逻辑,再挪到服务器上长期运行。第二步:解压后找到配置文件(常见命名是config.json或settings.json),把HTTP服务器的端口、鉴权token、是否开启WebSocket这三项主要配置写进去。第三步:启动程序,出现二维码后使用目标QQ号扫码登录,登录成功后终端会打印连接信息,同时HTTP服务开始监听本地端口。
这里用一段启动命令示意,具体程序名我用通用占位符代替:
# Linux 服务器上启动本地服务(示意) cd /opt/qq-local-service # 首次启动会生成配置目录,然后弹出登录二维码 ./qq-local-service --headless # 若需要指定配置文件和端口 ./qq-local-service --config ./config.json --http-port 8899命令里的--headless表示无界面模式,适合服务器;Windows本机不需要加这个参数,直接双击或命令行运行即可看到二维码。--http-port是本地HTTP接口的监听端口,默认值每种实现不一样,常见是3000、6099、8899这几个,建议显式指定为8899,避免和本机其他服务冲突。登录成功之后,程序会打印一句日志,说明HTTP服务已经ready,同时给出一个访问地址,类似http://127.0.0.1:8899。
登录态是接下来所有请求的前提,这里有两个关键点:第一,扫码登录之后不要关闭终端窗口,关闭进程等于断开会话;第二,服务端和客户端在同一台机器时,请求地址写127.0.0.1就够了,不要暴露到公网。如果确实需要远程访问,一定要配置鉴权token,不然任何人请求到这个端口都能拿到你的群列表和消息,这是一条非常严重的安全底线。
3.2 Python脚本:从"手动查"到"一键拿列表"
服务拉起来之后,Python脚本只需要做一件事:向本地HTTP接口发送请求,解析返回的群列表数据。标准机器人协议里对应动作叫get_group_list,它的语义就是"获取当前登录账号加入的群列表"。下面给出完整脚本,包含请求、解析、导出三个部分:
import json import requests # 本地服务监听地址和鉴权token,按你的实际配置修改 BASE_URL = "http://127.0.0.1:8899" TOKEN = "your-token" headers = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", } def get_group_list(no_cache: bool = False) -> list: """调用 OneBot 标准的 get_group_list,返回群列表。""" payload = { "action": "get_group_list", "params": { "no_cache": no_cache, # True 时强制走服务端拉取,False 走本地缓存 }, } resp = requests.post(f"{BASE_URL}/api", json=payload, headers=headers, timeout=10) resp.raise_for_status() data = resp.json() if data.get("status") != "ok": raise RuntimeError(f"接口返回异常: {data.get('message')}") return data.get("data", []) if __name__ == "__main__": groups = get_group_list(no_cache=True) # 按群号排序,并格式化输出 groups.sort(key=lambda g: g.get("group_id", 0)) for g in groups: print( f"群号: {g['group_id']} | 群名: {g['group_name']} | " f"人数: {g['member_count']}/{g['max_member_count']}" ) print(f"\n共获取到 {len(groups)} 个群")逻辑说明:payload里的action字段是协议动作名,params是该动作的参数;不同本地服务实现可能要求字段嵌套方式略有差异,但以action加params这种结构最为通用。timeout=10非常重要,群列表较大时服务端返回可能超过3秒,没有超时会导致脚本卡死。resp.raise_for_status()会在HTTP层面返回非200时直接抛异常,避免把错误页面当成JSON解析。
参数说明:no_cache这个参数比较容易被忽略。默认情况下本地服务会优先返回内存里的缓存群列表,拉取速度很快,但可能漏掉刚加入的新群;设置成True时,服务端会强制同步一次,保证结果是最新的。代价是响应时间会多零点几秒到两三秒,如果你的群数量上千,建议第一次拉取用True,后续轮询用默认的False,减少服务端压力。
3.3 群名编码、特殊字符与导出Excel
拿到群列表之后,下一个常见需求是把结果导出成表格。这里有两个坑:第一,群名里可能包含表情、特殊符号、XML卡片残留文本;第二,控制台直接打印时Windows默认编码可能报UnicodeEncodeError。第一个坑的处理方式是,在导出前把群名中的不可见字符和残留标签清理掉;第二个坑的处理方式是,在Python脚本开头强制stdout使用UTF-8编码,或者直接把结果写入文件而不是打印到控制台。
下面给出一段清洗和导出的补充代码,可以接在上一段脚本后面:
import re import csv def clean_group_name(name: str) -> str: """清理群名中的空白控制符和 xml 残留标签。""" if not name: return "" # 去掉常见控制字符与HTML/XML标签片段 name = re.sub(r"[\x00-\x1f\x7f]", "", name) name = re.sub(r"<[^>]+>", "", name) return name.strip() def export_groups(groups: list, path: str = "group_list.csv") -> None: """导出群列表为 CSV,使用 UTF-8-SIG 避免 Excel 中文乱码。""" with open(path, "w", encoding="utf-8-sig", newline="") as f: writer = csv.writer(f) writer.writerow(["群号", "群名", "人数", "上限"]) for g in groups: writer.writerow([ g["group_id"], clean_group_name(g["group_name"]), g["member_count"], g["max_member_count"], ]) export_groups(groups)编码这块是血泪经验:直接以utf-8编码写CSV,然后用Excel打开,中文群名全部乱码;改成utf-8-sig之后,Excel会正确识别BOM标记,乱码问题一次解决。群名清洗时不要使用过于激进的规则,不要把中文标点也删了,只处理控制字符和标签碎片就够用。另外,CSV里群号建议以文本形式存储,避免Excel把它当成科学计数法显示。
4. 实战避坑与排查:为什么脚本总在"空列表"和"登录失效"之间翻车
4.1 现象:接口调用成功,但返回的data是空数组
这是最常遇到的翻车现场。请求发出去了,HTTP 200,status是ok,但data是一个空数组。第一次遇到的人往往会怀疑脚本写错了,其实问题基本不在脚本,而在本地服务的登录状态。很多框架在扫码登录成功后,本地缓存尚未完成群列表同步,尤其是新安装的实例,首次登录后需要等十几秒到一分钟不等,群数据才会从服务端增量同步到本地。
原因:本地服务启动后,会先建立基本连接,再异步拉取会话数据;你脚本请求得太快,服务端还没拿到群列表,自然返回空数组。解决方式也很简单:写脚本时加一个重试机制,检测到空列表后等待5到10秒再请求一次,最多重试三次。另外也可以用no_cache=True强制服务端同步,减少空返回的概率。
4.2 现象:登录态频繁失效,二维码扫完没过多久又要重扫
第二个高发问题是登录态不稳定。典型表现是:上午扫码登录成功,下午脚本再跑就报login expired或token invalid,二维码过期重扫。这里要分清两个层面:如果是本机使用、断断续续跑脚本,登录态一般能保持好几天;如果部署在服务器上且长时间无人操作,框架会为了安全策略主动缩短会话有效期,触发设备验证。
原因:服务器环境的IP、设备特征与常用登录环境差异大,风控会判定为异常登录;另外部分本地服务默认开启"内存会话模式",重启进程后登录态不落盘,机器一重启账号就掉线。解决方式:优先检查框架是否支持会话持久化,把登录凭证保存到本地文件;同时尽量保持进程不重启;如果是长期任务,建议改成机器人托管方式,避免反复扫码。为了减少扫码次数,我一般会在服务器上跑一个守护进程,定期检测登录状态,掉线时发提醒,人工补扫。
4.3 现象:想把A号的群列表存下来,脚本却拿成了B号的
这个现象特别容易让需求方产生误解。标题里写的是"指定QQ号",但实际脚本拿到的是"当前登录账号"的群列表。如果你同时在本地登录了多个账号,或者之前测试时登录过另一个号,接口返回的其实是当前活跃会话对应的那个号的数据,而不是你心里想的那个号。
原因:本地服务实例和登录账号是一对一绑定关系,token鉴权只认会话,不认QQ号。解决方式:在调用get_group_list时不要依赖默认会话,而是要确认当前连接对应哪个账号;如果框架支持多账号,则需要在请求参数里显式传入self_id字段,指明确切QQ号。更稳妥的做法是每个账号使用一个独立端口和独立token,脚本和账号一一对应,彻底避免串号。这一点在交付脚本给同事使用时尤其重要。
4.4 现象:群列表数量比自己在客户端看到的少
第三种常见情况是,客户端“我的群聊”显示有200个群,接口只返回190个。排除同步延迟后,原因大多出在"群类型"过滤上。本地服务默认返回的群列表里,可能不包括已退出的群、被折叠的异常群、以及部分不允许机器人感知的加密群;而客户端界面上显示的总数,往往包含了一些历史会话或临时会话入口。
解决方式:先确认接口返回结构里有没有类型字段,比如gid_type或group_type;其次在客户端侧数数时,要把"发起群聊""临时会话"这类非正式群排除掉再对比。如果短缺数量较大,大概率是登录态刚过期又被自动重连,会话数据只恢复了一部分,此时重启框架并重新同步一次。不要手动去补这些缺的群,强行补拉容易触发风控。
4.5 现象:服务跑在服务器上,脚本在另一台机器上连不上
最后一个坑是网络层面的。你在本机跑通之后,想把它搬到某台Linux服务器上,Python脚本放在另一台机器,结果脚本报Connection refused或超时。最常见的错误是,请求地址还写着127.0.0.1,但脚本所在机器和服务器不是同一台,自然连不上。
解决方式:先把服务端的监听地址从127.0.0.1改成0.0.0.0,并开放对应端口;然后在本机脚本里把BASE_URL改成服务器的实际IP。这里必须再次强调,开放远程访问的同时一定要配置token鉴权,并且最好把端口限制在可信IP范围内,使用防火墙规则只允许你的业务机器访问。另一个隐蔽问题是服务器代理或NAT环境下的端口映射,如果服务跑在容器里,还要注意把容器端口映射到宿主机。
5. 进阶:把群列表变成持续资产,并用"共同群反查"合规落地指定QQ号
5.1 定时轮询群变更:新增、解散、人数波动一次看全
一次性拿个列表只是入门,实际工作里价值更大的是"持续监控"。比如你要确认某个管理群在今天是否有人新建了分群,或者某个员工号是否悄悄退出了合规群,这就要对群列表做定时快照与差异对比。实现思路很简单:每次获取群列表后与上一次结果做集合差,新增的群和消失的群分别输出。
import time import requests def fetch_group_ids(base_url: str, token: str) -> set: """复用前面的 get_group_list,只提取群号集合。""" groups = get_group_list(base_url, token) return {g["group_id"] for g in groups} def monitor_group_change(interval: int = 300) -> None: """每 interval 秒做一次快照对比,打印新增/解散的群。""" base_url = "http://127.0.0.1:8899" token = "your-token" previous = fetch_group_ids(base_url, token) print(f"初始化完成,当前群数量: {len(previous)}") while True: time.sleep(interval) try: current = fetch_group_ids(base_url, token) except Exception as err: print(f"拉取失败: {err}") continue new_groups = current - previous removed = previous - current if new_groups: print(f"[新增] {len(new_groups)} 个群: {new_groups}") if removed: print(f"[减少] {len(removed)} 个群: {removed}") previous = current逻辑说明:fetch_group_ids里对get_group_list做了二次封装,只返回群号集合,这样内存占用极小,哪怕有上千个群也能快速做差集。monitor_group_change先初始化一个基准快照,然后循环轮询。interval参数控制轮询间隔,建议300秒以上,过于频繁的拉取一方面没有业务意义,另一方面会增加账号被风控关注的概率。
这段脚本实际用起来会有一个痛点:群解散或被移除时,接口不会返回原因,你需要自己结合前一天的快照判断是群解散还是自己退了群。所以在部署监控前,建议先在本地导出一份初始快照,包含群号、群名、人数、创建时间等字段,作为历史留档。这个快照表就是后续所有分析的底稿。
5.2 合规实现"指定QQ号":用共同群反查替代越权查询
最后回到标题里的"指定QQ号"。如果你要查的不是自己账号的群列表,而是想知道"某个QQ号加入了哪些群",在授权框架内唯一稳的做法是共同群反查。原理是:只有你和对方在同一个群里时,你才能通过群成员列表看见对方;把所有群成员拉出来和目标QQ号比对,命中哪个群,就说明对方在这个群。这个方案不越权、不碰对方登录态,拿到的也是确定性的结论。
def is_member_in_group(base_url: str, token: str, group_id: int, target_qq: int) -> bool: """判断目标QQ号是否在某群中,基于 get_group_member_list。""" payload = { "action": "get_group_member_list", "params": {"group_id": group_id}, } resp = requests.post( f"{base_url}/api", json=payload, headers={"Authorization": f"Bearer {token}"}, timeout=15, ) resp.raise_for_status() data = resp.json() if data.get("status") != "ok": return False members = data.get("data", []) # 返回的成员字段一般是 user_id,即QQ号 return any(m.get("user_id") == target_qq for m in members) def find_common_groups(group_ids: list, target_qq: int) -> list: """在已知群列表中找出目标QQ号所在的群。""" result = [] for gid in group_ids: if is_member_in_group(BASE_URL, TOKEN, gid, target_qq): result.append(gid) return result这段代码的逻辑很直接,但有两个参数要特别注意:第一,get_group_member_list返回的成员列表默认只有昵称和QQ号,大群可能超过500人,为了减少接口压力,可以看接口文档是否支持no_cache和分页参数;第二,目标QQ号如果设置了好友验证或隐身状态,群成员列表里依然能看到他,因为群内身份对群成员是可见的,所以这个反查结果在"你们共同所在的群"这个范围内是准确的。
使用这个功能时的边界要拿捏住:它能回答"我和他共同在哪些群",不能回答"他加入过但我没加入的群"。有些需求方会不理解这一点,你需要提前说明白:接口能拿到的数据边界止步于此,再往后就是越权了。我之前就被同事催过"再想想办法",但在这件事上,合规边界不是技术水平的体现,硬碰只会把自己账号搭进去。
把基础脚本、定时监控和共同群反查串起来之后,你就拥有了一套完整的"群列表资产管理工具":一键导出Excel、定时追踪变更、定向确认人员所属群。这套东西单次拉群表只值一场手工统计的时间,但加上监控和反查,就变成能长期用的运营基建。最后落一个习惯:每次改完脚本,先把群列表全量导出一份备份,再跑增量逻辑,数据永远是先留档再处理。希望这篇笔记能帮你把这个方向一次跑通,少踩几个我已经踩过的坑。
本文还有配套的精品资源,点击获取