Greasy Fork

数英通用验证码

数字和字母通用验证码,识别率可达95%以上。

目前为 2022-02-19 提交的版本。查看 最新版本

// ==UserScript==
// @name        数英通用验证码
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  数字和字母通用验证码,识别率可达95%以上。
// @author       哈士奇

// @include        http://*
// @include        https://*
// @license        MIT

// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_listValues
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_log
// @grant        GM_getResourceText
// @grant        GM_getResourceURL
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_getTab
// @grant        GM_saveTab
// @grant        GM_getTabs
// @grant        GM_notification
// @grant        GM_setClipboard
// @grant        GM_info
// @grant        GM_xmlhttpRequest
// @connect      *
// @require      https://cdn.jsdelivr.net/npm/[email protected]
// @require      https://unpkg.com/element-ui/lib/index.js
// @resource elementUIcss https://unpkg.com/element-ui/lib/theme-chalk/index.css

// @run-at document-end
// ==/UserScript==

(function () {
  // GM_setValue('tipsConfig',"")
  var elementUIcss = GM_getResourceText("elementUIcss");
  GM_addStyle(elementUIcss);

  function getStyle(el) {
    // 获取元素样式
    if (window.getComputedStyle) {
      return window.getComputedStyle(el, null);
    } else {
      return el.currentStyle;
    }
  }

  function init() {
    //简化各种api和初始化全局变量
    CUR_URL = window.location.href;
    DOMAIN = CUR_URL.split("//")[1].split("/")[0];
    selector = document.querySelector.bind(document);
    selectorAll = document.querySelectorAll.bind(document);
    getItem = localStorage.getItem.bind(localStorage);
    setItem = localStorage.setItem.bind(localStorage);
  }

  class BaiDuCloud {
    // 百度网盘自动填写验证码
    constructor() {
      this.findBaiduCloudLink();
      this.getCode();
      // this.autoDownLoad() // 根据需要自主选择是否开启自动点击下载网盘文件
    }

    findBaiduCloudLink() {
      let text = document.body.innerText.replace(/\x0A/gm, "");
      // let panLink = text.match(/(pan.baidu.com[^\u4e00-\u9fa5]+)(提取码|密码).?\s?(\w{4})/gm);
      let panLink = text.match(
        /pan.baidu.com[^\u4e00-\u9fa5]+\s*\n?[\u4e00-\u9fa5]+.*?([a-z\d]{4})/gm
      );
      if (!panLink) {
        return;
      }
      panLink.forEach((el) => {
        let link = el.match(/(pan.baidu.com[^\u4e00-\u9fa5]+)/gm)[0];
        let code = el.match(/([a-z\d]{4}$)/gm)[0];
        let key = "https://" + link.trim();
        GM_gstValue(key, code);
      });
    }

    getCode() {
      if (!CUR_URL.includes("pan.baidu.com") || !CUR_URL.includes("share")) {
        return;
      }
      let sourceId = CUR_URL.split("surl=")[1];
      let url = "https://pan.baidu.com/s/1" + sourceId;
      let code = GM_getValue(url);
      if (code && selector("#accessCode") && selector("#accessCode")) {
        selector("#accessCode").value = code;
        selector("#submitBtn .submit-a").click();
        setTimeout(() => {
          if (selector("#mpz1nz1")) {
            GM_log("哈士奇小助手:检测到提取码错误,当前密码:" + code);
          }
        }, 1000);
      } else {
        GM_log("哈士奇小助手:没有找到可使用的网盘验证码");
      }
    }

    autoDownLoad() {
      // 到网盘下载界面后是否自动点击下载
      if (!CUR_URL.includes("pan.baidu.com") || !CUR_URL.includes("/s")) {
        return;
      }
      let open = GM_getValue("autoDownload");
      let filterUnuseLink = GM_getValue("filterUnuseLink");
      if (!open || filterUnuseLink) {
        return;
      }
      let downBtn = selectorAll("a.g-button")[1];
      let hasFileList = selector("#shareqr");
      let text = document.body.innerText;
      if (hasFileList) {
        // 有文件列表,需要先勾选
        selector(".zbyDdwb").click();
        downBtn.click();
      } else if (
        document.body &&
        (text.includes("不存在") ||
          text.includes("删除") ||
          text.includes("无法"))
      ) {
        setTimeout(() => {
          window.close();
        }, 3000);
      } else if (downBtn) {
        //单个文件直接下载
        downBtn.click();
      }
      let timer = setInterval(() => {
        if (
          selector(".module-yun-tip") &&
          selector(".module-yun-tip").innerText.includes("启动网盘")
        ) {
          clearInterval(timer);
        }
      }, 1000);
    }
  }

  class Captcha {
    // 识别网页中的验证码
    constructor() {
      this.imgCache = [];
      this.inputTags = [];
      this.recommendPath = {};

      window.addEventListener("load", async () => {
        this.manualLocateCaptcha();
        try {
          this.recommendPath = await this.getRecommendPath();
        } catch (error) {}
        this.findCaptcha();
      });
    }
    dataURLtoFile(dataURL, filename = "captcha.jpg") {
      //  base64转图片文件
      var arr = dataURL.split(","),
        mime =
          (arr[0].match(/:(.*?);/) && arr[0].match(/:(.*?);/)[1]) ||
          "image/png",
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], filename, { type: mime });
    }
    getRecommendPath() {
      let requestUrl =
        "http://101.43.206.185:7000/cssPath?href=" +
        location.href.split("?")[0];
      try {
        return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: "get",
            url: requestUrl,
            onload: (res) => {
              if (res.status === 200 && res.response) {
                let data = JSON.parse(res.response);
                if (data.path && data.recommendTimes) {
                  resolve(data);
                }
              }
              resolve({});
            },
            onerror: function (err) {
              console.log("推荐路径请求失败:" + err);
              resolve({});
            },
          });
        });
      } catch (error) {
        console.log(error);
        Promise.resolve({});
      }
    }
    getCaptchaFeature(el) {
      // 获取验证码特征
      let checkList = [];
      checkList.push(el.getAttribute("id"));
      checkList.push(el.className);
      checkList.push(el.getAttribute("alt"));
      checkList.push(el.getAttribute("src"));
      checkList.push(el.getAttribute("name"));
      return checkList;
    }
    cssPath = (el) => {
      // 获取元素css path
      if (!(el instanceof Element)) return;
      var path = [];
      while (el.nodeType === Node.ELEMENT_NODE) {
        var selector = el.nodeName.toLowerCase();
        if (el.id) {
          selector += "#" + el.id;
          path.unshift(selector);
          break;
        } else {
          var sib = el,
            nth = 1;
          while ((sib = sib.previousElementSibling)) {
            if (sib.nodeName.toLowerCase() == selector) nth++;
          }
          if (nth != 1) selector += ":nth-of-type(" + nth + ")";
        }
        path.unshift(selector);
        el = el.parentNode;
      }
      return path.join(" > ");
    };

    manualLocateCaptcha() {
      let imgs = [];
      let inputTags = [];
      let cssPathStore = {};
      let finish = false;
      this.vue = new Vue();
      this.isIframe = top !== self;
      var onTagClick = (e) => {
        let el = e.target;
        let tagName = el.tagName;
        if (tagName.toLowerCase() === "input") {
          let type = el.getAttribute("type");
          if (type && type !== "text") {
            this.vue.$message.error(
              "提醒:当前点击输入框type=" + type + ",请选择文本输入框"
            );
          } else {
            cssPathStore.input = this.cssPath(el);
            this.vue.$message.success("您已成功选择输入框");
          }
        } else {
          cssPathStore.img = this.cssPath(el);
          this.vue.$message.success("您已成功选择验证码图片");
        }
        if (cssPathStore.input && cssPathStore.img) {
          GM_setValue("cssPath" + location.href, JSON.stringify(cssPathStore));
          imgs.forEach((img) => {
            img && img.removeEventListener("click", onTagClick);
          }, false);
          inputTags.forEach((input) => {
            input.removeEventListener("click", onTagClick);
          }, false);
          setTimeout(() => {
            this.vue.$message.success("选择完毕,赶快试试吧");
          }, 3000);
          finish = true;
        }
      };
      var onMenuClick = (e) => {
        if (this.isIframe) {
          alert("当前脚本处于iframe中,暂不支持该操作,快让作者优化吧");
          return;
        }
        finish = false;
        cssPathStore = {};
        GM_deleteValue("cssPath" + location.href);
        this.vue.$alert("接下来请点击验证码图片和输入框", "操作提示", {
          confirmButtonText: "确定",
          callback: () => {
            setTimeout(() => {
              imgs.forEach((img) => {
                img && img.removeEventListener("click", onTagClick);
              }, false);
              inputTags.forEach((input) => {
                input.removeEventListener("click", onTagClick);
              }, false);
              if (!finish) {
                this.vue.$notify.success({
                  title: "提示",
                  message: "已退出手动选择验证码模式。",
                  offset: 100,
                });
              }
            }, 20000);
          },
        });

        // alert("请点击验证码和输入框各一次。");
        imgs = [...selectorAll("img")];
        inputTags = [...selectorAll("input")];
        imgs.forEach((img) => {
          img.addEventListener("click", onTagClick);
        }, false);
        inputTags.forEach((input) => {
          input.addEventListener("click", onTagClick);
        }, false);
      };
      GM_registerMenuCommand("手动选择验证码和输入框", onMenuClick);
    }
    handleImg(img) {
      return new Promise((resolve, reject) => {
        try {
          // 图片没设置跨域,可采用图片转canvas转base64的方式
          let dataURL = null;
          if (!img.src.includes(";base64,")) {
            let canvas = document.createElement("canvas");
            canvas.width = img.naturalWidth;
            canvas.height = img.naturalHeight;
            let ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight);
            dataURL = canvas.toDataURL("image/png");
          } else {
            dataURL = img.src;
          }
          resolve(dataURL);
        } catch (error) {
          console.error("error:" + error);
          // 这块处理比较复杂,待优化
          // 图片设置跨域,重新请求图片内容后转base64,相当于替用户点击了“换一张图片”
          // if (this.times >= 1) {
          //   return;
          // }
          // if (typeof Vue !== "undefined") {
          //     new Vue().$notify.success({
          //         title: "温馨提示",
          //         message: "当前验证码结果可能和图片显示不一致,请放心提交。",
          //         offset: 100,
          //     });
          // }

          // this.times++;
          // GM_xmlhttpRequest({
          //     method: "get",
          //     url: img.src,
          //     responseType: "blob",
          //     onload: (res) => {
          //         if (res.status === 200) {
          //             let blob = res.response;
          //             let fileReader = new FileReader();
          //             fileReader.onloadend = (e) => {
          //                 let base64 = e.target.result;
          //                 resolve(base64);
          //             };
          //             fileReader.readAsDataURL(blob);
          //         } else {
          //             console.log("图片转换blob失败");
          //             console.log(res);
          //             reject();
          //         }
          //     },
          //     onerror: function(err) {
          //         console.log("图片请求失败:" + err);
          //         reject();
          //     },
          // });
        }
      });
    }
    hasRequest(dataURL, config = {}) {
      let imgClips = dataURL.slice(dataURL.length - 100, dataURL.length);
      if (this.imgCache.includes(imgClips)) {
        return true;
      }
      if (config.record) {
        this.imgCache.push(imgClips);
      }
      return false;
    }
    request(file, path, src) {
      try {
        if (!file) {
          console.error("缺少file参数");
          return Promise.reject();
        }

        return new Promise((resolve, reject) => {
          let host = location.href;
          let href = location.href.split("?")[0];
          if (self === top) {
            host = location.host;
          }
          let formData = new FormData();
          let detail = {
            path,
            src,
            host,
            href,
          };
          formData.append("img", file);
          formData.append("detail", JSON.stringify(detail));
          // let requestUrl = "http://192.168.31.184:7000/captcha";
          let requestUrl = "http://101.43.206.185:7000/captcha";
          GM_xmlhttpRequest({
            method: "post",
            url: requestUrl,
            data: formData,
            onload: function (response) {
              if (response.status === -1) {
                console.error("获取验证码失败:" + response);
                reject();
              } else {
                let data = response.response;
                if (data.length < 50) {
                  data = JSON.parse(data);
                  if (data.code) {
                    resolve(data.code);
                  } else {
                    let date = new Date().getDate();
                    let tipsConfig = {
                      date,
                      times: 1,
                    };
                    let cache =
                      GM_getValue("tipsConfig") &&
                      JSON.parse(GM_getValue("tipsConfig"));
                    if (cache && cache.times > 3) {
                    } else {
                      if (!cache) {
                        GM_setValue("tipsConfig", JSON.stringify(tipsConfig));
                      } else {
                        cache.times = cache.times + 1;
                        GM_setValue("tipsConfig", JSON.stringify(cache));
                      }
                      if (typeof Vue !== "undefined") {
                        new Vue().$message.error(data.msg);
                      }
                    }

                    console.error("获取验证码失败:" + data.msg);
                    reject();
                  }
                } else {
                  console.error("获取验证码失败:" + data);
                  reject();
                }
              }
            },
            onerror: function (err) {
              console.error(err);
              reject();
            },
          });
        });
      } catch (error) {
        console.log(error);
      }
    }
    async findCaptcha() {
      let timer = setInterval(async () => {
        // 先读取用户手动设置的验证码配置
        let cache = GM_getValue("cssPath" + location.href);
        let captchaPath = cache && JSON.parse(cache);
        if (
          captchaPath &&
          captchaPath.input &&
          captchaPath.img &&
          selector(captchaPath.input) &&
          selector(captchaPath.img) &&
          selector(captchaPath.img).getAttribute("src")
        ) {
          let dataURL = await this.handleImg(selector(captchaPath.img));
          try {
            if (!this.hasRequest(dataURL, { record: true })) {
              let code = await this.request(
                this.dataURLtoFile(dataURL),
                this.cssPath(selector(captchaPath.input)) +
                  "$$" +
                  this.cssPath(selector(captchaPath.img)),
                selector(captchaPath.img).getAttribute("src")
              );
              if (code) {
                selector(captchaPath.input).value = code.trim();
                console.log("正在使用用户自定义验证码位置数据获取验证码");
                return;
              } else {
                console.error("验证码为空,请检查图片是否正确");
              }
            }
          } catch (error) {
            console.log(error);
          }
        }
        // 自动寻找验证码和输入框
        let captchaMap = [];
        let imgs = [...selectorAll("img")];
        imgs.forEach((img) => {
          let checkList = [
            ...this.getCaptchaFeature(img),
            ...this.getCaptchaFeature(img.parentNode),
          ];
          checkList = checkList.filter((item) => item);
          let isInvalid =
            ["#", "about:blank"].includes(img.getAttribute("src")) ||
            !img.getAttribute("src");

          for (let i = 0; i < checkList.length; i++) {
            if (
              /.*(code|captcha|验证码|login|点击|verify|yzm|yanzhengma).*/im.test(
                checkList[i].toLowerCase()
              ) &&
              img.width > 30 &&
              img.width < 150 &&
              img.height < 80 &&
              !isInvalid
            ) {
              captchaMap.push({ img: img, input: null });
              break;
            }
          }
        });
        captchaMap.forEach((item) => {
          let imgEle = item.img;
          let parentNode = imgEle.parentNode;
          for (let i = 0; i < 4; i++) {
            // 以当前可能是验证码的图片为基点,向上遍历四层查找可能的Input输入框
            if (!parentNode) {
              return;
            }
            let inputTags = [...parentNode.querySelectorAll("input")];
            if (inputTags.length) {
              let input = inputTags.pop();
              let type = input.getAttribute("type");
              while (type !== "text" && inputTags.length) {
                if (type === "password") {
                  break;
                }
                input = inputTags.pop();
                type = input.getAttribute("type");
              }
              let inputWidth = getStyle(input).width.replace(/[^0-9]/gi, "");
              // let inputHeight = getStyle(input).height.replace(/[^0-9]/gi, "");
              if (!type || (type === "text" && inputWidth > 50)) {
                // 兼容各种奇葩情况
                item.input = input;
                break;
              }
              if (type === "password") {
                // 验证码一般在密码框后面,遍历到密码框了就大概率说明没有验证码
                break;
              }
            }
            parentNode = parentNode.parentNode;
          }
        });
        // console.log(captchaMap);
        const { path, recommendTimes = 0 } = this.recommendPath;
        // 获取网友共享验证码位置数据
        if (!captchaMap.length || recommendTimes > 0) {
          if (path) {
            let inputSelector = path.split("$$")[0];
            let imgSelector = path.split("$$")[1];
            if (
              selector(inputSelector) &&
              selector(imgSelector) &&
              selector(imgSelector).getAttribute("src")
              && selector(inputSelector).getAttribute("type") === 'text'
            ) {
              let dataURL = await this.handleImg(selector(imgSelector));
              try {
                if (!this.hasRequest(dataURL, { record: true })) {
                  let code = await this.request(
                    this.dataURLtoFile(dataURL),
                    this.cssPath(selector(inputSelector)) +
                      "$$" +
                      this.cssPath(selector(imgSelector)),
                    selector(imgSelector).getAttribute("src")
                  );
                  if (code) {
                    selector(inputSelector).value = code;
                    if (typeof Vue !== "undefined") {
                      new Vue().$message.success("获取验证码成功");
                    }
                    console.log("正在使用共享验证码功能获取验证码");
                    return;
                  } else {
                    console.error("验证码为空,请检查图片是否正确");
                  }
                }
              } catch (error) {
                console.log(error);
                // if (typeof Vue !== "undefined") {
                //     new Vue().$message.error("获取验证码失败");
                // }
              }
            }
          }
        }
        captchaMap = captchaMap.filter((item) => item.input);
        captchaMap.forEach(async (item) => {
          let dataURL = await this.handleImg(item.img);
          try {
            if (!this.hasRequest(dataURL, { record: true })) {
              let code = await this.request(
                this.dataURLtoFile(dataURL),
                this.cssPath(item.input) + "$$" + this.cssPath(item.img),
                item.img.getAttribute("src")
              );
              if (code) {
                item.input.value = code;
                if (typeof Vue !== "undefined") {
                  new Vue().$message.success("获取验证码成功");
                }
                console.log("正在使用自动寻找验证码功能获取验证码");
              } else {
                console.error("验证码为空,请检查图片是否正确");
              }
            }
          } catch (error) {
            console.log(error);
            // if (typeof Vue !== "undefined") {
            //     new Vue().$message.error("获取验证码失败");
            // }
          }
        });
      }, 1000);
      window.addEventListener("beforeunload", () => {
        window.clearInterval(timer);
      });
    }
  }

  function main() {
    window.addEventListener("DOMContentLoaded", function () {
      init();
      new BaiDuCloud();
      new Captcha();
    });
  }

  main();
})();