校园招新之获取进QQ群但未报名人员

校园的社团、实验室招新一般由是校领导会发一个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即可

具体代码

Go 复制代码
package main

import (
	"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 string
	Name  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, "&nbsp;", "")
		class := strings.ReplaceAll(rows[i][9], " ", "")
		class = strings.ReplaceAll(class, "&nbsp;", "")
		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 * 21
		end := st + 20
		if i != 0 && i == cycleIndex-1 { // For the fifth iteration, set the end to 86
			end = 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 JSONData
		err = json.Unmarshal(body, &r)
		countMember = r.Count
		cycleIndex = 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, "&nbsp;", "")
					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.Name
		registeredUserMap[key] = user
	}

	// 查找已进群已报名、已进群未报名
	var notedAndRegistered []QQUser
	var notedAndNotRegistered []QQUser

	for _, qqUser := range NotedUserList {
		key := qqUser.Card
		if _, found := registeredUserMap[key]; found {
			notedAndRegistered = append(notedAndRegistered, qqUser)
		} else {
			notedAndNotRegistered = append(notedAndNotRegistered, qqUser)
		}
	}

	// 查找未进群但已报名
	var notInGroupButRegistered []WJXUser
	for key, wjxUser := range registeredUserMap {
		found := false
		for _, qqUser := range NotedUserList {
			if qqUser.Card == key {
				found = true
				break
			}
		}
		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)
	}
}
相关推荐
不爱说话郭德纲16 小时前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星2 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架2 天前
golang高频面试真题
面试·go
郝同学的测开笔记2 天前
云原生探索系列(十四):Go 语言panic、defer以及recover函数
后端·云原生·go
秋落风声3 天前
【滑动窗口入门篇】
java·算法·leetcode·go·哈希表
0x派大星4 天前
【Golang】——Gin 框架中的模板渲染详解
开发语言·后端·golang·go·gin
0x派大星5 天前
【Golang】——Gin 框架中的表单处理与数据绑定
开发语言·后端·golang·go·gin
三里清风_6 天前
如何使用Casbin设计后台权限管理系统
golang·go·casbin
0x派大星6 天前
【Goland】——Gin 框架中间件详解:从基础到实战
开发语言·后端·中间件·golang·go·gin
0x派大星6 天前
【Goland】——Gin 框架简介与安装
后端·golang·go·gin