微信公众号短链实时阅读量、点赞数爬虫(不会Hook可用)

众所周知,微信分享的公众号分享出的一般都是短链,在这个短链下使用浏览器打开并不能获取微信公众的阅读量点赞数等这些信息,如图1所示。

但是实际拥有详细信息的则是这个链接下面,提取链接所需要提交的信息包括经过本人筛选有以下参数,并且携带Cookie,如图2所示:

其中_biz、mid、sn会根据不同的文章发生变动,其余的都是固定值,但都可以从原始连接中取得,且原始链接可以通过F12在谷歌浏览器前端获取,如图3所示:

所以我们现在就应当编写获取真是链接代码:

java 复制代码
private String GetRealAddress(String url) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet(url);

        /// 解析html字符串
        HttpResponse response = httpClient.execute(httpGet);
        String html = EntityUtils.toString(response.getEntity());
        Document doc = Jsoup.parse(html);
        /// 获取GET参数
        String realUrl = doc.select("meta[property='og:url']").attr("content");
        //获取真实链接
        httpClient.close();
        return new URL(realUrl).getQuery();
    }

根据Fiddler软件抓包可以知道,阅读数等详细信息是在 https://mp.weixin.qq.com/mp/getappmsgext/...下,而要携带参数我们已经可以从真实长链中获取。别看图2的请求链接好像很长,但是实质上如果只是为了获取阅读量、点赞数等一些有用信息,只需要带上appmsg_token即可,如图4所示:

既然请求链接和请求参数我们都知道,那么就可以书写代码了:

JAVA 复制代码
  public AjaxResponse getNumberNewWay(String url) throws IOException {
        String realUrl = this.GetRealAddress(url);
        if (StringUtils.hasText(realUrl)) {
            // 使用分隔符 "&" 将参数字符串分割成参数对
            String[] parameterPairs = realUrl.split("&");
            // 创建一个 Map 来存储参数名和参数值
            Map<String, String> parameterMap = new HashMap<>();
            // 遍历参数对,提取参数名和参数值,并放入 Map 中
            for (String parameterPair : parameterPairs) {
                String[] parts = parameterPair.split("=");
                if (parts.length == 2) {
                    String paramName = parts[0];
                    String paramValue = parts[1];
                    parameterMap.put(paramName, paramValue);
                }
            }
            // 获取各个参数的值,因为上面使用"=="分割,所以biz的值后面的"=="也会被分割
            String biz = parameterMap.get("__biz") + "==";
            String mid = parameterMap.get("mid");
            String idx = parameterMap.get("idx");
            String sn = parameterMap.get("sn");
            String appmsgToken = "";
            if (StringUtils.hasText(cookie)){
                //获取appmsgToken
                String[] parts = cookie.split(";");
                for (String part : parts) {
                    if (part.startsWith("appmsg_token=")) {
                        appmsgToken = part.substring("appmsg_token=".length());
                        break;
                    }
                }
            }else {
                return AjaxResponse.error("cookie缺失");
            }

            String originUrl = "https://mp.weixin.qq.com/mp/getappmsgext?";
            String postUrl = originUrl + "appmsg_token=" + appmsgToken + "&x5=0";


            HttpPost httpPost = new HttpPost(postUrl);
            //模拟微信客户端
            httpPost.setHeader("User-Agent", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0Chrome/57.0.2987.132 MQQBrowser/6.2 Mobile");
            httpPost.setHeader("Cookie", cookie);
            httpPost.setHeader("Origin", "https://mp.weixin.qq.com");
            //httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");

            CloseableHttpClient client = HttpClientBuilder.create().build();
            List<NameValuePair> parameters = new ArrayList<>();
            parameters.add(new BasicNameValuePair("is_only_read", "1"));
            parameters.add(new BasicNameValuePair("is_temp_url", "0"));
            parameters.add(new BasicNameValuePair("appmsg_type", "9"));
            parameters.add(new BasicNameValuePair("__biz", biz));
            parameters.add(new BasicNameValuePair("mid", mid));
            parameters.add(new BasicNameValuePair("idx", idx));
            parameters.add(new BasicNameValuePair("sn", sn));
            HttpEntity entity = new UrlEncodedFormEntity(parameters,"utf-8");
            httpPost.setEntity(entity);

            // 发送请求并获取响应
            try {
                HttpResponse response = client.execute(httpPost);
                HttpEntity responseEntity = response.getEntity();
                String StringResult = EntityUtils.toString(responseEntity);

                // 创建一个 ObjectMapper 对象
                ObjectMapper objectMapper = new ObjectMapper();
                JsonNode jsonNode = objectMapper.readTree(StringResult);

                // 提取 read_num 的值
                JsonNode readJson = jsonNode.get("appmsgstat");
                if (readJson != null) {
                    return AjaxResponse.success(readJson.get("read_num").asText());
                }
                return AjaxResponse.error("获取阅读量失败,请检查token是否过期");
            } catch (IOException e) {
                e.printStackTrace();
                return AjaxResponse.error("远程调用失败");
            }
        }
        return AjaxResponse.error("获取真实地址失败");
    }

这里存在一个时效性的问题,由于微信设置的Cookie有时效性,且是由微信客户端自身生成,并且笔者不会Hook。所以就想到了一个另类的办法:通过定时任务刷新微信文章来刷新Cookie->利用Fidder拦截请求获取Cookie->Cookie传入Java代码中获取信息。

由于不会Hook,所以笔者就用了python的pyautogui与cv2库识别微信浏览器的刷新图标,然后定时进行刷新,大体流程是:截一张桌面图像->识别刷新图标位置->鼠标模拟刷新。python核心代码如下:

python 复制代码
def refresh_funtion():
    # 截一张桌面新的图片
    newImg = pyautogui.screenshot()
    newImg.save('C:\\Users\\chen\\Desktop\\newAll.png')
    # 转换为OpenCV的图像格式
    newImg_cv = cv2.cvtColor(cv2.imread('C:\\Users\\chen\\Desktop\\newAll.png'), cv2.COLOR_BGR2RGB)
    # 寻找刷新按钮
    # 如果文本框已经打开则进行刷新
    refreshTemplate = cv2.imread('C:\\Users\\chen\\Desktop\\refresh.jpg')
    # 在屏幕截图中搜索目标图像
    refreshRes = cv2.matchTemplate(newImg_cv, refreshTemplate, cv2.TM_CCOEFF_NORMED)
    # 获取匹配度最高的位置
    refresh_min_val, refresh_max_val, refresh_min_loc, refresh_max_loc = cv2.minMaxLoc(refreshRes)
    # 如果存在则刷新
    # 如果匹配度最高的位置的阈值大于 0.8,则模拟鼠标点击
    if refresh_max_val > 0.8:
        # 计算鼠标点击的位置
        x, y = refresh_max_loc[0] + refreshTemplate.shape[1] // 2, refresh_max_loc[1] + refreshTemplate.shape[0] // 2
        # 模拟鼠标点击
        pyautogui.click(x, y)

这里就需要使用FiddlerScript脚本,在Fiddler软件的FidderScipt中的OnBeforRequest方法内插入图5中代码,将刷新的Cookie传递到Java代码中,再让Java代码根据传递到的Cookie解析并获取微信信息,如图5所示:

本方法仅提供不会Hook的同学的权宜之计,仅供学习参考使用,不可用于违法途径。