初级工程师获取千人QQ群信息

2,339 阅读10分钟
原文链接: mp.weixin.qq.com

PHP,网站动态编程语言。

有一天,你入职了一家很大的公司,人事很快就把你拉入公司千人QQ工作群。上级给你布置了一项任务,他想要知道群里所有人QQ号、昵称和部门的关系。

oh,导出EXCEL员工QQ号信息表,我懂,这很简单。于是你打开QQ群,开始对着群成员列表疯狂复制粘贴进EXCEL,当你复制了几百个的时候,发现这群特么1000多人,还有一半没做完...woc,好像有几个人昵称复制错了Orz

作为一名初级工程师,键盘上Ctrl+c、v就要被你按烂了,你的怨念值在上升。你想到了尝试网上各种导出QQ群成员信息工具。于是找啊找,终于找到了那么一两个,可惜公司内网限制下载,好不容易下载安装,一打开还被提示植入病毒,公司电脑不敢瞎搞,只有老实复制粘贴吧!

旁边工位的老程序员老牛实在看不下去了,这很不Professional。喂,那个小白啊,你来在我这登下你的QQ,8分钟后桌面冒出个7.47MB的excel.xlsx文件,小白激动的打开,哇靠——几万条QQ信息,竟然是全公司全集团的所有人的QQ号啊,超乎想象。

小白:老牛我——爱—你

老牛:滚,不搞基

数天后...

教教我嘛,经过小白多日的软磨硬泡,外带非要请吃饭一周,老牛终于答应教小白了——那个,你就当有腾讯高级工程师给你写好了数据接口,现在需要你来对接,用程序登录授权,访问接口拿到数据,这样能理解了吧。我呢尽力给你讲详细点,但可能你是听不懂的,明白一点是一点。实在看不懂就分享给别人虐吧,请吃饭就不用了

出师 程序的诞生

①发现

对于你加入的任何一个QQ群(即使不是群主或管理员),你都可以看到其他成员的基础信息。例如:

访问  https://qun.qq.com/member.html#gid=所在的群号

看到这个页面,初级工程师想到的第一方案是让程序访问这个页面,获取页面HTML字符串,进行文档结构解析。经过进一步的操作分析,我们采用更好的方案——页面中成员信息就是AJAX异步加载的,前后端分离、接口。继续刷新当前页面,通过火狐浏览器F12可查看信息

随着页面下拉,浏览器AJAX异步发出数据请求,以st为起点,end为终止,每次获取20条数据。

[下拉刷新查看数据-gif]

太棒了,这里的数据就是通过访问接口获取的,不需要从页面HTML解析数据。也就是说我们只要模拟浏览器访问,通过接口就可以获取到JSON类型的数据。思路就是这样,但其实还有很多琐碎的工作需要完成和理解。

(针对本文的简要名词解释)

接口:访问一个页面URL地址,例如xxx.com/api/get_member ,页面返回一段字符串,这段字符串以(参数:值)的JSON格式呈现,返回数据。

②获取请求头

访问:https://qun.qq.com/member.html#

我们通过人工安全登录让浏览器就获得Cookie,程序将直接使用,通过火狐浏览器F12监控页面XHR,提取出请求头,Cookie也在其中。这样程序就可以像人工打开浏览器登录该页面一样,通过Cookie验证身份请求数据。

[获取请求头信息-gif]

完整的请求头信息参考:

POST /cgi-bin/qun_mgr/search_group_members HTTP/1.1Host: qun.qq.comUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0Accept: application/json, text/javascript, */*; q=0.01Accept-Language: zh-CN,en-US;q=0.7,en;q=0.3Accept-Encoding: gzip, deflate, brReferer: https://qun.qq.com/member.htmlContent-Type: application/x-www-form-urlencoded; charset=UTF-8X-Requested-With: XMLHttpRequestContent-Length: 46Cookie: pt2gguin=o2919386060; RK=yCJFeqgk1+; ptcz=4a39cd6d3a1ec996ad47a5bb3d9e2459ee770bbcc16c82bdae849a718dc1b755; pgv_pvid=8705330208; o_cookie=2919386060; pac_uid=1_2919386060; pgv_pvi=9667558400; ptisp=ctc; p_uin=o2919386060; pt4_token=HoickMQ1gBDjqKRxTtjf7RyM3GT-vE7Pny8Hfpiyr9M_; p_skey=s-ieh-QDT-ZrjK*KL5u14xbKwQiY5WhikfrQcGiSA8U_; _qpsvr_localtk=0.382165406103388; pgv_si=s9728745472; uin=o2919386060; skey=@AuTDqeOjtConnection: keep-alive

注意POST之后的地址是需要根据接口不同变化的,另外一些参数我们在程序中是不需要使用的,

Accept-Encoding: gzip, deflate, brContent-Length: 13在程序中去掉,否则Accept-Encoding:会导致返回JSON为加密乱码Content-Length: 限制数值会导致无法返回正确数据

在PHP实际的程序中用到的:

$header =[    'POST /cgi-bin/qun_mgr/search_group_members HTTP/1.1',    'Host: qun.qq.com',    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',    'Accept: application/json, text/javascript, */*; q=0.01',    'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',    'Referer: https://qun.qq.com/member.html',    'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',    'X-Requested-With: XMLHttpRequest',    $Cookie,    'Connection: keep-alive',    'Pragma: no-cache',    'Cache-Control: no-cache',];$json = myCURL($url,$header,$post);

以上信息表示:发起请求是64位win10系统的Firefox浏览器,我们设置将其中Cookie设置为参数$Cookie(键值对字符串),其他信息可以认为是固定的(首行POST地址实际是根据接口变化的)。所以只要获得登录后的Cookie,就能用程序模拟浏览器发出请求并且具有访问接口的权限。

请求头的部分已经完成了,通过F12浏览器开发者工具,也可以看到XHR传递到不同接口的参数,下面将详细说明。

③接口分析

现在我们需要分析上面JSON字段的含义,我们只用到其中两个接口,都需要Cookie,通过手动操作QQ登录后授权带参访问它,获得正确的JSON数据。

1、get_group_list 获取QQ号所在所有群列表2、search_group_members 获取一个QQ群
接口地址:https://qun.qq.com/cgi-bin/qun_mgr/get_group_list接口名:get_group_list请求参数:   bkn:787574550 登录授权后获取base_key接口描述: 获取QQ号所在所有群列表JSON数据格式示例:{    "create": [{        "gc": 169352216,        "gn": "小葵花编程学习NASA站",        "owner": 2919386060    }],    "ec": 0,    "join": [{        "gc": 8275772,        "gn": "自学php编程_mysql数据库",        "owner": 450838675    }}],    "manage": [{        "gc": 190949802,        "gn": "vue + koa2 + node +mongodb",        "owner": 1830305999    }]}JSON说明:create-你创建的群join-你加入的群manage-你管理的群gc-QQ群号gn-QQ群名owner-群主QQ号
接口地址:https://qun.qq.com/cgi-bin/qun_mgr/search_group_members接口名:search_group_members请求参数:  bkn:787574550 登录授权后获取base_key  end:20   分页结束下标     gc:169352216 目标QQ群号  sort:0  排序方式  st:0  分页起始下标接口描述: 获取一个QQ群人数信息和成员列表JSON数据格式示例:{    "count": 200,    "ec": 0,    "max_count": 500,    "mems": [{        "card": "本群无大佬都是",        "flag": 0,        "g": 0,        "join_time": 1505894282,        "last_speak_time": 1522400036,        "lv": {            "level": 1,            "point": 0        },        "nick": "百万强心剂",        "qage": 2,        "role": 0,        "tags": "-1",        "uin": 2919386060    }],    "search_count": 200,    "svr_time": 1522919777,    "vecsize": 1}JSON说明(部分字段):count-总数max_count-最大数量mems-成员列表svr_time-查询时间

经过对字段的分析,在数据库中设计两个表qqGroup、qqMember来对接以上数据,我们尽可能保持字段命名的一致性。COMMENT注释中说明了字段的含义,后面的一些字段是为了方便查阅数据自定义追加的,用空行隔开了。

CREATE TABLE qqMember(    id int(11) NOT NULL AUTO_INCREMENT,    card VARCHAR(100) COMMENT '群名片',    flag INT,    g INT COMMENT '性别|0-男 1-女 255-未知',    join_time INT(11),    last_speak_time INT(11),    lv JSON ,    level INT,    point INT,    nick VARCHAR(200) COMMENT '昵称',    qage INT COMMENT 'Q龄',    role INT COMMENT '角色[0-群主 1-管理员 -群友]',    tags CHAR(10),    uin BIGINT COMMENT 'QQ号',    gc BIGINT COMMENT '群号',        join_date DATETIME,    last_speak_date DATETIME,    add_time DATETIME,    update_time DATETIME,    PRIMARY KEY(id))DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE qqGroup(    gc BIGINT COMMENT '群号',    gn VARCHAR(100) COMMENT '群名',    owner BIGINT COMMENT '群主QQ号',    count INT(11),    max_count INT(11) COMMENT '群人数上限',    search_count INT ,    svr_time INT(11) COMMENT '查询时间',    vecsize INT COMMENT '页数',        create_time DATETIME,    add_time DATETIME,    update_time DATETIME,    PRIMARY KEY(gc))DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

④程序实现思路

首先通过人为手动安全登录QQ,获取bkn和Cookie。其次,我们st设置为0,end直接设置为4000一次获取所有接口数据(虽然不够完美但省去每次20条分页循环),然后需要制定一个QQ群测试一下配置是否有效,然后就可以正式行动。有n个群,就需要反复修改n次QQ群号码。不,继续用程序获取所在QQ群列表,以这个列表为基础数据,循环去请求所有群内成员信息。

⑤程序清单

[程序文件说明]set.php ——配置你的$bkn 和 $Cookie1.php ——获取一个指定群成员信息,并显示在浏览器上2.php ——获取登录QQ已加入所有群的列表,并写入数据库。3.php ——获每个群的基本信息和其中详细成员信息,并写入数据库。myQQ.sql——创建数据库及相关表指令语句mycurl.php ——包含接口设置函数和curl函数[99行]myDB.class.php ——封装的数据库插入操作类[109行],要求PHP版本5.6及以上

set.php中:

<?php$bkn = 1775018621;$Cookie =<<<EODCookie: pt2gguin=o2919386060; RK=yCJFeqgk1+; ptcz=4a39cd6d3a1ec996ad47a5bb3d9e2459ee770bbcc16c82bdae849a718dc1b755; pgv_pvid=8705330208; o_cookie=2919386060; pac_uid=1_2919386060; pgv_pvi=9667558400; uin=o2919386060; skey=@nv4vhiFGL; ptisp=ctc; pgv_info=ssid=s5922538022; rv2=80E8157C04F9D0756003C0EC1A8612A77D337D034DBFCB86FD; property20=ACB752E297687FD7898F7A108649C3EBA6B57298E2F2D935B17F7D2BE74A14A1DC611D5623F7F5D6; p_uin=o2919386060; pt4_token=EYdRg67h8LJ4m-pwrCvsGNpTLRoIh-gz*99TDbJS-Bo_; p_skey=7bCAzPfo4d0jWlWZYuUxrn3Djk9IEMkkEzHfL90La4Y_; _qpsvr_localtk=0.7487029758667233; pgv_si=s8179640320EOD;$st =0;

mycurl.php中:

<?phpfunction myCURL($url,$header,$post){    $ch = curl_init();    curl_setopt($ch,CURLOPT_URL,$url);    curl_setopt($ch,CURLOPT_HEADER,0);//请求头    curl_setopt($ch, CURLOPT_HTTPHEADER, $header); //POST参数    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //不验证证书    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); //不验证证书    curl_setopt($ch,CURLOPT_RETURNTRANSFER,1 );     curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,10);     $res = curl_exec($ch);     return $res;}

已知异常返回JSON说明:

{"ec":1,"em":"no login"}{"ec":3,"em":"param err"}{"ec":4,"em":"basekey err"}{"calllog":"CMLB_39998_1#oidb_0x561_2:0","ec":7,"em":""}根据ec的值:1-没有正确设置Cookie3-post参数错误,参数顺序不符或缺失4-bkn参数错误7-无权限,无法查询一个未加入的群信息关于错误提示码,从外部的信息就只能获取这么多了欢迎补充

到这里所有的编程知识都交给你了。

喝口100ml的纯净水,需要休息好一会。


到这里我也是身感疲惫的,知识点和细节说明非常消耗元气。传道受业解惑,确实任重而道远。专心写文档比程序还难,尤其是程序之后的,想象完成后的神清气爽、如沐春风,为了信仰必须继续,可能吸引到同行小伙伴。中途休一天。

进击—数据会说话➳

⑥数据测试

mysql> SELECT COUNT(*) FROM qqMember;+----------+| COUNT(*) |+----------+|    62590 |+----------+1 row in set
mysql> SELECT SUM(search_count) FROM qqGroup;+-------------------+| SUM(search_count) |+-------------------+| 64260             |+-------------------+1 row in set

我们发现最终的成员信息是少于每个群基本信息求和的,上面就相差了1670条。这个结果其实是意料之中的。对于一个真实成员4581人的群,理论只能查阅其中4000人的信息:

访问 https://qun.qq.com/member.html 群管理

在成员管理界面,滚动条拉倒底恰好是4000,不再变化。

是接口设计如此吗?

所以对于一个超过4000人的群,理论上依靠接口仅能获取其中4000条信息,那我们来实际测试下一个4581人的群:

实际成员:4581理论获取:4000实际插入:3917mysql> SELECT COUNT(DISTINCT(uin))  AS '成员总数' FROM qqMember WHERE gc = 群号;+----------+| 成员总数  |+----------+|     3917 |+----------+1 row in set

为什么会有事3971,接下来尝试几组参数

测试用例:st=0 end = 3971 结果:获取3891条记录,唯一总数3891st=3972 end = 4000 结果:获取26条记录,唯一总数26st=4001 end = 4581 结果:无法获取成员数组st=4000 end = 4581 结果:无法获取成员数组st=3999  end= 4581 结果:获取1条记录,唯一总数3917st=3921 end = 3941 结果:获取20条记录,唯一总数3917st=520 end = 720 结果:获取200条记录,唯一总数3917结论:1、end-st=获取记录条数,是连续的2、0~3971数据不连续,3972~4000数据不连续3、st最大值为3999,end最大值为4000(结合页面)

根据分析,将目光投向第二组测试数据

st=3972 到 end= 4000 ,应该获取28条,实际只有26,缺少2条。

难道是插入出了问题?我们观察到页面

[出现问题]执行失败1366-Incorrect string value: '\xF0\x9F\x8D\x8A' for column 'nick'[经检查原因]UTF-8编码有可能是两个、三个、四个字节。Emoji表情是4个字节,而Mysql的utf8编码最多3个字节,所以数据插不进去。解决方案:将Mysql的编码从utf8转换成utf8mb4。来源:https://blog.csdn.net/java_xiaobin/article/details/51451682

其实我们的数据库和表字符集一开始已经是utf8mb4,网络上说了一大堆的修改,从MySQL配置my.ini到数据库再到改到表字符集。对于我们这个项目,其实只用改动插入类中设置的字符集即可。因为储存字符集已经是utf8mb4,所以只用传递也设置为utf8mb4就能正常插入。

//myDB.class.php//SET NAMES UTF8MB4//指定了客户端和服务器之间传递字符的编码规则为UTF8MB4$query = 'SET NAMES UTF8MB4';//UTF8改为UTF8MB4$mysqli->query($query);

那些缺失的数量都是由于名字含有特殊符号或表情导致的插入错误,满满的4000,这下圆满了。

mysql> SELECT COUNT(DISTINCT(uin))  AS '成员总数' FROM qqMember WHERE gc = 群号;+----------+| 成员总数  |+----------+|     4000 |+----------+

至于怎么获取另外的581人,答复为接口设计如此。个人猜测可能开发版本较早,没有想到后来还有超过4000人的QQ群。对于4000人以上的群,部分成员管理从名单溢出,不知道这算不算BUG。另一方面,日常中很少会进到那个管理页面,在QQ群成员面板都可以进行大部分操作,所以完美改进的必要性似乎也很低。

查询SQL

/* 统计所有成员数量 */SELECT COUNT(*) FROM qqMember ;/* 根据群号统计所有群成员数量 */SELECT SUM(search_count) FROM qqGroup;/* 查看一个指定群不重复成员数量*/SELECT COUNT(DISTINCT(uin))  AS '成员总数'FROM qqMemberWHERE gc = 1234567;/* 查看群成员数量排名前10的群信息 */SELECT gc,    COUNT(*),    COUNT(gc) AS 'gc_total'FROM qqMember GROUP BY gcORDER BY gc_total DESCLIMIT 0,10;

⑧总结

通过对页面的分析,我们知道QQ成员管理是前后端分离开发模式下的产品。前端发起请求,后端响应请求返回JSON数据,页面通过下拉刷新的方式加载数据。我们利用手动登录,拷贝Cookie和相关浏览器信息,让程序模拟浏览器请求数据,最终将公开可见的数据信息,依靠程序能够自动获取。

在这个过程中的很多工作,都是在做黑盒测试,因为我们对接口参数请求规则一无所知,是一个反复尝试求证的过程。另外呢程序其实还有不少需要改进的地方,一个是数据库批量插入,针对单条记录的插入在批量执行时存在重复操作;还有一个是程序是半自动的执行的,需要更加智能化自动获取Cookie的操作。那么,剩下的无尽的探索和发现就由你尽情发挥。have fun

以上。