校园的社团、实验室招新一般由是校领导会发一个QQ通知,让各个班的同学们进一个招新群。 群里面会有负责人提示大家报名,但是群成员不总是都会报名,我们需要的就是,找到那些,已经进群,但是没有报名的同学,然后私聊提醒一下。
大体思路:获取QQ群成员列表,获取已经报名的人员(从问卷星导出,使用go读取execl),根据两者(我们使用QQ群昵称 “计科203张三” 和 execl中的班级+姓名作为判断标准)即可求出我们想要的差集合并集。
获取QQ群成员列表
参考文章:js解密之QQ的bkn值,获取QQ群成员信息,获取QQ好友列表信息-腾讯云开发者社区-腾讯云
进入网站:https://qun.qq.com/member.html 登录,选择群,打开网络请求
解析参数:
- gc,应该是群id
- st: 开始下标
- end :结束下标
- sort:不知道
- bkn:加密用的应该是
解析response
- adm_num :管理员数量,不带群主
- count: 群成员总数
- mems : 群成员列表
- mems.role : 0是群主,1是管理员,2是群成员
- card: 群昵称
nick:QQ昵称
使用网络抓包即可,导入apifox即可
具体代码
package mainimport ("encoding/json""fmt""github.com/360EntSecGroup-Skylar/excelize""io/ioutil""net/http""strings""time"
)type QQUser struct {Uin int `json:"uin"`Role int `json:"role"`G int `json:"g"`JoinTime int `json:"join_time"`LastSpeakTime int `json:"last_speak_time"`Lv struct {Point int `json:"point"`Level int `json:"level"`} `json:"lv"`Card string `json:"card"`Tags string `json:"tags"`Flag int `json:"flag"`Nick string `json:"nick"`Qage int `json:"qage"`Rm int `json:"rm"`
}
type JSONData struct {Ec int `json:"ec"`Errcode int `json:"errcode"`Em string `json:"em"`Cache int `json:"cache"`AdmNum int `json:"adm_num"`Levelname interface{} `json:"levelname"`Mems []QQUser `json:"mems"`Count int `json:"count"`SvrTime int `json:"svr_time"`MaxCount int `json:"max_count"`SearchCount int `json:"search_count"`Extmode int `json:"extmode"`
}
type WJXUser struct {Class stringName string
}func ReadExcel(filename string) (RegisteredUserList []WJXUser, err error) {f, err := excelize.OpenFile(filename)if err != nil {return}sheets := f.GetSheetMap() //获取Execl表的工作表fmt.Println(sheets)sheet1 := sheets[1] //sheets是一个map,map[1]就是获取到第一个工作表fmt.Println("第一个工作表", sheet1)rows := f.GetRows(sheet1) //获取工作表的所有数据,数据存储在一个二维数组中,二维数组中的每一个一位数组就是一行数据fmt.Println(rows)RegisteredUserList = make([]WJXUser, 0)for i := 1; i < len(rows); i++ {name := strings.ReplaceAll(rows[i][6], " ", "")name = strings.ReplaceAll(name, " ", "")class := strings.ReplaceAll(rows[i][9], " ", "")class = strings.ReplaceAll(class, " ", "")class = strings.ReplaceAll(class, "班", "")RegisteredUser := WJXUser{Name: name, Class: class}RegisteredUserList = append(RegisteredUserList, RegisteredUser)}return RegisteredUserList, err
}
func main() {url := "https://qun.qq.com/cgi-bin/qun_mgr/search_group_members"method := "POST"client := &http.Client{}NotedUserList := make([]QQUser, 0) // 已经进QQ群且已经备注人员 (已经剔除管理员)NoNoteUserList := make([]QQUser, 0) // 已经进QQ群但是未备注人员(已经剔除管理员)countMember := 0 // 群成员总数量cycleIndex := 1 // 刚开始让循环一轮,后面根据群成员总数来确定到底循环多少轮for i := 0; i < cycleIndex; i++ {st := i * 21end := st + 20if i != 0 && i == cycleIndex-1 { // For the fifth iteration, set the end to 86end = countMember}payload := strings.NewReader(fmt.Sprintf("gc=185335516&st=%d&end=%d&sort=0&bkn=336337277", st, end))req, err := http.NewRequest(method, url, payload)if err != nil {return}req.Header.Add("Accept", "application/json, text/javascript, */*; q=0.01")req.Header.Add("Accept-Language", "zh-CN,zh;q=0.9")req.Header.Add("Connection", "keep-alive")req.Header.Add("Origin", "https://qun.qq.com")req.Header.Add("Referer", "https://qun.qq.com/member.html")req.Header.Add("Sec-Fetch-Dest", "empty")req.Header.Add("Sec-Fetch-Mode", "cors")req.Header.Add("Sec-Fetch-Site", "same-origin")req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")req.Header.Add("X-Requested-With", "XMLHttpRequest")req.Header.Add("sec-ch-ua", "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"")req.Header.Add("sec-ch-ua-mobile", "?0")req.Header.Add("sec-ch-ua-platform", "\"macOS\"")req.Header.Add("Cookie", "RK=Gdv1uMjH8g; ptcz=cc0188ccc671556dd56ea8ffb4ae59d282714e8e788826033f41c89d77b61b53; pgv_pvid=4845315920; tgw_l7_route=9d1d4698c4322116c7c255687ec1fe38; traceid=cfd4c1d7fb; uin=o3063360183; skey=@61HarNEbA; p_uin=o3063360183; pt4_token=e6WELkURyGZz0yu3RI3j1UwIQk5z9E7n9dUrbXmq4gE_; p_skey=GQ0pK0oDZsKCwOphrmWqZtFoCE5qsUVEqZfgDLHn7FU_")req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")req.Header.Add("Host", "qun.qq.com")res, err := client.Do(req)if err != nil {fmt.Println(err)return}defer res.Body.Close()body, err := ioutil.ReadAll(res.Body)if err != nil {fmt.Println(err)return}var r JSONDataerr = json.Unmarshal(body, &r)countMember = r.CountcycleIndex = countMember/21 + 1 // 每次查询是21人,不足21次的向上取整一下if err != nil {fmt.Println("Error unmarshalling response:", err)return}for j := 0; j < len(r.Mems); j++ {if r.Mems[j].Role == 2 {if r.Mems[j].Card == "" {NoNoteUserList = append(NoNoteUserList, r.Mems[j])} else {// 数据处理,让数据保持格式为 “计科221张三”r.Mems[j].Card = strings.ReplaceAll(r.Mems[j].Card, " ", "")r.Mems[j].Card = strings.ReplaceAll(r.Mems[j].Card, " ", "")r.Mems[j].Card = strings.ReplaceAll(r.Mems[j].Card, "班", "")NotedUserList = append(NotedUserList, r.Mems[j])}}}time.Sleep(2 * time.Second)}registeredUserList, err := ReadExcel("/Users/yjppjy/Downloads/263778738_按序号_π-Team报名表_54_51.xlsx") // 已经报名人员if err != nil {return}// 根据 NoteUserList中的.Card字段 和 registeredUserList的 Class + Name 作为判断依据,以QQUser为基准,找出来 未进群但是已报名、已进群已报名、已进群未报名的人员// 将已报名人员存储在一个map中,以便快速查找registeredUserMap := make(map[string]WJXUser)for _, user := range registeredUserList {key := user.Class + user.NameregisteredUserMap[key] = user}// 查找已进群已报名、已进群未报名var notedAndRegistered []QQUservar notedAndNotRegistered []QQUserfor _, qqUser := range NotedUserList {key := qqUser.Cardif _, found := registeredUserMap[key]; found {notedAndRegistered = append(notedAndRegistered, qqUser)} else {notedAndNotRegistered = append(notedAndNotRegistered, qqUser)}}// 查找未进群但已报名var notInGroupButRegistered []WJXUserfor key, wjxUser := range registeredUserMap {found := falsefor _, qqUser := range NotedUserList {if qqUser.Card == key {found = truebreak}}if !found {notInGroupButRegistered = append(notInGroupButRegistered, wjxUser)}}// 输出结果//fmt.Println("已进群已报名:")//for _, user := range notedAndRegistered {// fmt.Println(user.Card, user.Nick)//}fmt.Println("已进群未报名:")for _, user := range notedAndNotRegistered {fmt.Println(user.Card, user.Nick)}fmt.Println("未进群但已报名:")for _, user := range notInGroupButRegistered {fmt.Println(user.Class, user.Name)}
}