Greasy Fork

数英通用验证码

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

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

// ==UserScript==
// @name        数英通用验证码
// @namespace    http://tampermonkey.net/
// @version      2.0
// @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 })) {
                            selector(captchaPath.input).value = await this.request(
                                this.dataURLtoFile(dataURL),
                                this.cssPath(selector(captchaPath.input)) +
                                "$$" +
                                this.cssPath(selector(captchaPath.img)),
                                selector(captchaPath.img).getAttribute("src")
                            );
                            console.log("正在使用用户自定义验证码位置数据获取验证码");
                        }
                    } catch (error) {
                        console.log(error);
                    }
                    return;
                }
                // 自动寻找验证码和输入框
                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 > 1) {
                    // 至少两个2网友推荐才会采用该数据,一定程度上避免他人污染数据
                    // 获取网友共享验证码位置数据
                    if (path) {
                        let inputSelector = path.split("$$")[0];
                        let imgSelector = path.split("$$")[1];
                        if (
                            selector(inputSelector) &&
                            selector(imgSelector) &&
                            selector(imgSelector).getAttribute("src")
                        ) {
                            let dataURL = await this.handleImg(selector(imgSelector));
                            try {
                                if (!this.hasRequest(dataURL, { record: true })) {
                                    let code = await this.request(
                                        this.dataURLtoFile(dataURL),
                                        path,
                                        selector(imgSelector).getAttribute("src")
                                    );
                                    if (code) {
                                        selector(inputSelector).value = code;
                                        if (typeof Vue !== "undefined") {
                                            new Vue().$message.success("获取验证码成功");
                                        }
                                        console.log("正在使用推荐验证码位置数据获取验证码");
                                    } else {
                                        console.error("验证码为空,请检查图片是否正确");
                                    }
                                }
                                return;
                            } 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),
                                path,
                                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();
})();