Greasy Fork

Greasy Fork is available in English.

数英通用验证码

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

当前为 2022-02-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();