分类: Javascript

window.CustomEvent浏览器自定义事件

因为厂商、版本等的差异导致的兼容性问题,CustomEvent这个对象并不是所有浏览器都支持,官方给了一个这样的方法用作兼容

(function(){
    try{
        // a : While a window.CustomEvent object exists, it cannot be called as a constructor.
        // b : There is no window.CustomEvent object
        new window.CustomEvent('T');
    }catch(e){
        let CustomEvent = function(event, params){
            params = params || { bubbles: false, cancelable: false, detail: undefined };
            let evt = document.createEvent('CustomEvent');
            evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
            return evt;
        };
        CustomEvent.prototype = window.Event.prototype;
        window.CustomEvent = CustomEvent;
    }
})();

至于构造和触发事件的方法的话,我们可以自己写个

let publishCustomEvent = async ( eventName, eventParam , domElement) => {
    if ( domElement instanceof EventTarget){
        let customEvent = new CustomEvent(eventName, {detail:eventParam});
        domElement.dispatchEvent(customEvent);
        console.log(eventName+" pubed")
    }
}

至于监听方的就addEventListener方法了,这个太普遍了,就不再赘述了。

【油猴脚本】关于用油猴脚本爬取考试题库这件事

公司每隔一段时间都有一些规范性的内容的考试,略头疼,有一点还行的是事先都提供了题库,于是本着自己动手丰衣足食的精神,用React写了个题库搜题的小Web应用

搜题小应用倒是好写,这个省去不说。但是题库的录入就有点费事了,从一开始写搜题Web应用到最终这个脚本诞生,中间经历了几个阶段

1.手动录入题目,费事费力,是个重体力活

2.手机给题目截图,用图片的方式展示搜题结果,工作量依旧很大,一开始设想的是图片保存后接一个ORC服务,来实现关键文字信息提取,但是接入ORC服务这依旧是一个费力的过程,还不说ORC服务是否需要付费,和手机上截图的背景对准确性产生的影响

3.在经历了前面两次考试之后,刷题背题的过程中才发现是页面文字是可以复制出来的,于是这一代的就开始了一题题的复制题目,存在手机备忘录,然后再在电脑上取得一整个所有题目的大又长的字符串,再做一些字符串解析的工作,解析之后就可以使用了。看起来不错,不过一题题的复制,依旧还是一个不轻松的工作,而且后面的字符串解析也实现起来有点点麻烦,各个字段,题目之间的分割特征不是很明显

4.抓包软件的应用,在实现3的时候其实想到了可以使用抓包软件来实现,不过最终没有走上这一步,而且如果要实现这一步的话,最终每次接口请求的数据也都需要人工收集

5.经过观察确定,这个题库本质其实是个web页面,那么既然是web页面了,必然还是可以在电脑上用浏览器打开的吧,假如服务端没有做一些特别的限制的话,经过几次尝试果然,确实可以打开,那么我们的油猴脚本就有大展身手的机会了

基本思路:

实现一个自己的XMLHttpRequest方法,替换掉window上原本的XMLHttpRequest方法,并对特定的事件进行记录,那么我们需要的数据也就水到渠成的可以截取下来。中间还用到了CustomEvent 。具体的就可以看下面代码了,从一开始的初始版本可以截取信息,到最终用起来顺手,还是经过了几个版本的迭代的

// ==UserScript==
// @name         题库小柯基(科技)
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  try to take over the world!
// @author       You
// @match        https://xxxxxx.com?*
// @grant        none
// ==/UserScript==

//需要存下来的数据
window.list = [];
//题库问题唯一编码列表.用来去重以防重复存储
window.subjectCodeList = []
//中断执行标记
window.ifInterrupt = true
//总页数
let totalCount = () => document.getElementsByClassName("examNumbers")[0].children[3].innerText
//当前页码
let currentNum = () => document.getElementsByClassName("examNumbers")[0].children[1].innerText
let isEnd = () => (currentNum() - 0) >= (totalCount() - 0)
let clearList = () => {
    window.list = []
    window.subjectCodeList = []
}
let goNext = () => {
    window.ifInterrupt = false
    !isEnd() && document.getElementsByClassName('nextBtnClass')[0].click()
}

/**
 * 在页面上创建一个按钮,用来触发实现对应需要的功能
 * @param func
 * @param btnText
 * @param top
 */
let createButton = (func, btnText, top) => {
    let btn = document.createElement("button")
    btn.style.position = "fixed"
    btn.style.right = 0
    btn.style.top = top
    btn.style.padding = "10px"
    btn.style.zIndex = 99999
    btn.innerText = btnText
    btn.addEventListener("click", func)
    document.body.append(btn)
}

createButton(clearList, "清除缓存", "120px")
createButton(goNext, "开始抓取", "80px")
createButton(() => window.ifInterrupt = true, "暂停", "160px")

;(function () {
    if (typeof window.CustomEvent === "function") return false;

    function CustomEvent(event, params) {
        params = params || {bubbles: false, cancelable: false, detail: undefined};
        let evt = document.createEvent('CustomEvent');
        evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
        return evt;
    }

    CustomEvent.prototype = window.Event.prototype;
    window.CustomEvent = CustomEvent;
})();
;(function () {
    function ajaxEventTrigger(event) {
        let ajaxEvent = new CustomEvent(event, {detail: this});
        window.dispatchEvent(ajaxEvent);
    }

    let oldXHR = window.XMLHttpRequest;

    function newXHR() {
        let realXHR = new oldXHR();
        realXHR.addEventListener('abort', function () {
            ajaxEventTrigger.call(this, 'ajaxAbort');
        }, false);
        realXHR.addEventListener('error', function () {
            ajaxEventTrigger.call(this, 'ajaxError');
        }, false);
        realXHR.addEventListener('load', function () {
            ajaxEventTrigger.call(this, 'ajaxLoad');
        }, false);
        realXHR.addEventListener('loadstart', function () {
            ajaxEventTrigger.call(this, 'ajaxLoadStart');
        }, false);
        realXHR.addEventListener('progress', function () {
            ajaxEventTrigger.call(this, 'ajaxProgress');
        }, false);
        realXHR.addEventListener('timeout', function () {
            ajaxEventTrigger.call(this, 'ajaxTimeout');
        }, false);
        realXHR.addEventListener('loadend', function () {
            ajaxEventTrigger.call(this, 'ajaxLoadEnd');
        }, false);
        realXHR.addEventListener('readystatechange', function () {
            ajaxEventTrigger.call(this, 'ajaxReadyStateChange');
        }, false);
        let send = realXHR.send;
        realXHR.send = function (...arg) {
            send.apply(realXHR, arg);
            realXHR.body = arg[0];
            ajaxEventTrigger.call(realXHR, 'ajaxSend');
        }
        let open = realXHR.open;
        realXHR.open = function (...arg) {
            open.apply(realXHR, arg)
            realXHR.method = arg[0];
            realXHR.orignUrl = arg[1];
            realXHR.async = arg[2];
            ajaxEventTrigger.call(realXHR, 'ajaxOpen');
        }
        let setRequestHeader = realXHR.setRequestHeader;
        realXHR.requestHeader = {};
        realXHR.setRequestHeader = function (name, value) {
            realXHR.requestHeader[name] = value;
            setRequestHeader.call(realXHR, name, value)
        }
        return realXHR;
    }

    window.XMLHttpRequest = newXHR;

})();
window.addEventListener("ajaxReadyStateChange", function (e) {
    let xhr = e.detail;
    if (xhr.readyState == 4 && xhr.status == 200) {
        // xhr.getAllResponseHeaders()  响应头信息
        // xhr.requestHeader            请求头信息
        // xhr.responseURL              请求的地址
        // xhr.responseText             响应内容
        // xhr.orignUrl                 请求的原始参数地址
        // xhr.body                     post参数,(get参数在url上面)
        let url = xhr.orignUrl
        //只需关注我们需要的url,其他忽略
        if ("/url-witch-need-to-be-listen.json" == url) {
            //最终监听到的接口返回的信息
            let json = JSON.parse(xhr.responseText);
            let {subjectCode} = json
            if (window.subjectCodeList.includes(subjectCode)){
                return
            }
            window.list.push(createQuestionItem(json))
            window.subjectCodeList.push(subjectCode)
            console.log(window.list)
            //判断是否到了最后一页,以及是否需要中断本次执行
            if (!window.ifInterrupt && !isEnd()) {
                //随机一个5秒内的时间,点击下一页按钮的操作。触发下一次请求
                setTimeout(() => {
                    document.getElementsByClassName('nextBtnClass').length &&
                    document.getElementsByClassName('nextBtnClass')[0].click()
                }, Math.random() * 5000)
            }
            if (isEnd()) {
                console.warn("结束啦")
                //最终创建一个新的window展示抓取下来的内容,方便后续录入题库操作
                let newWindow = window.open('', '获取结果', 'height=300,width=400,top=0,left=0,toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no')
                newWindow.document.body.innerText = JSON.stringify(window.list)
            }
        }
    }
});

let createQuestionItem = ( questionInfo ) => {
    let type, question, answerArr, answer, desc;
    return { type, question, answerArr, answer, desc, questionInfo }
}

油猴脚本,公众号文章图片显示

常看公众号文章的时候,遇到一个恼人的问题,图片不会加载出来,需要手动点击一下,(公司网络相关策略,不能登微信,但是可以打开公众号文章链接)。另外也是网络原因,公众号内的https链接的图片无法打开,于是萌生了自己写个油猴脚本,自动加载图片的想法。

简单看了下页面dom结构。思路很简单,遍历下img标签,取到data-src属性,这里存了真实的图片url,然后替换掉原来的src属性里的值,这里默认是一个灰色占位图片。触发方法也很简单。每次scroll事件的时候触发。那么,直接开始撸代码

// ==UserScript==
// @name         公众号文章宽屏&图片
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://mp.weixin.qq.com/s*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let func = ()=>{
        let arr = document.getElementsByTagName("img");
        for(let i = 0; i < arr.length-1; i++){
            let src = arr[i].getAttribute("src");
            let dataSrc = arr[i].getAttribute("data-src")
            dataSrc = dataSrc!=null?dataSrc.replace("https://","http://"):'';
            if(src != undefined && src.startsWith("http")){
                arr[i].setAttribute("src",src.replace("https://","http://"))
            }
            if(src != undefined && src.startsWith("data:")){
                arr[i].setAttribute("src",dataSrc)
            }
        }
    }

    window.addEventListener("scroll",func)
    document.getElementsByClassName("rich_media_area_primary_inner")[0].style.width = "100%"
    document.getElementsByClassName("rich_media_area_primary_inner")[0].style.maxWidth = "1080px"
})();

后续:买了个BeeArt会员,想在公司电脑上用下画点图之类的,但是网站直接打不开,看了下报错信息,IndexedDB的问题,由于公司电脑上的Chrome是公司预装的,怀疑是版本太旧的原因,重新安装了89版本的Chrome,果然解决。但是。。。新问题出现了,原先替换https为http链接的图片无法显示了

Mixed Content: The page at 'https://mp.weixin.qq.com/s/L39ufU9YCVIJ9g1WCVQAGQ' was loaded over HTTPS, but requested an insecure element 'http://mmbiz.qpic.cn/mmbiz_png/aaVJqS7LaMJQfIuic9SSvvZFP1hqaKk43iaDicQj9R6dNjpTcf7DgkXdk2v4CxSTUZMxpqkWRCia8jUiaAK2iaPicl9kA/640?wx_fmt=png'. This request was automatically upgraded to HTTPS, For more information see https://blog.chromium.org/2019/10/no-more-mixed-messages-about-https.html

一直报这个问题,看了下网络,即使是用的http的图片链接,但是实际请求的还是https链接,查证了一下原来在某个新版本的Chrome中,  开始阻止混合内容 。https的网页中资源链接,默认都转成https的请求。

解决方法:点击地址栏前面的“△不安全”或者锁形图标,有个网站设置选项,找到“不安全内容”,选择运行,这样就又恢复了原来的情况,可以正常访问了。我是个人特例情况,所以我是这么解决的

正确方法:网站在域名做了SSL之后,那么相应配套的资源链接也应该全部切换到SSL,全站HTTPS势在必行

App内置浏览器和被调用的html页面之间的js交互

之前一段时间公司花费了很长一段时间投入开发的App终于面世了,Android和IOS的各有一版,同时开发。Android App已经在各大App应用商店上架了,而IOS的还是依旧在一次次的卡审核,先默哀个!~

在最初的版本中,因为进度问题,部分页面采取了嵌套html页面的方法来处理(不过后来已经改成了全部原生App页面了),所以就多少会涉及到一些App和嵌套页面之间的JS交互问题。这里的JS交互主要指的是App调用html内的JS方法和页面内JS调用App的方法两个部分

相对而言,在Android的App内置浏览器嵌套的html页面中执行App的内置方法比ios的更加容易处理,就目前而言在Android上基本不用过多的特殊处理。而在IOS上就不一样了,IOS App中,App是可以直接调用被嵌套页面的JS方法的,但是反过来想要通过嵌套的Html页面来调用IOS App的开发就行不通了。这时候我们需要借助一个WebViewJavascriptBridge来实现处理了,附上WebViewJavascriptBridge的Git地址
Continue reading

PHP+JS 身份证号码有效性验证

PHP验证身份证号码有效性,可选是否验证用户性别

* 新的18位身份证号码各位的含义:
* 1-2位省、自治区、直辖市代码; 11-65
* 3-4位地级市、盟、自治州代码;
* 5-6位县、县级市、区代码;
* 7-14位出生年月日,比如19670401代表1967年4月1日;
* 15-17位为顺序号,其中17位男为单数,女为双数;
* 18位为校验码,0-9和X,由公式随机产生。
* 举例:
* 130503 19670401 0012这个身份证号的含义: 13为河北,05为邢台,03为桥西区,出生日期为1967年4月1日,顺序号为001,2为验证码。
*
*
* 15位身份证号码各位的含义:
* 1-2位省、自治区、直辖市代码;
* 3-4位地级市、盟、自治州代码;
* 5-6位县、县级市、区代码;
* 7-12位出生年月日,比如670401代表1967年4月1日,这是和18位号码的第一个区别;
* 13-15位为顺序号,其中15位男为单数,女为双数;
* 与18位身份证号的第二个区别:没有最后一位的验证码。
* 举例:
* 130503 670401 001的含义; 13为河北,05为邢台,03为桥西区,出生日期为1967年4月1日,顺序号为001。
Continue reading

禁用页面选择、右键复制等

今天临时让改了一个东西,有作者反馈称小说章节阅读页现在居然可以选择和复制了,然后安排了我来排查处理了下,本来想找下原来的代码看下哪边出了问题的,但是找了半天没有一点头绪,索性就重新加了下禁用选择复制和鼠标右键的js代码了。
先是改的pc站上的,很容易直接js禁用页面的选择和右键。不过还是有点小插曲,本来想直接把代码写到head里的,试了下,不行。此时页面还没有加载出body标签,代码会报错。然后就直接写到jquery的document.ready里了。上传代码,成功!
[code type=javascript]
document.body.onselectstart = document.body.oncontextmenu = function(){return false;};
[/code]
接下来修改m站的阅读页,在chrome里手机浏览器模拟里测试不起来,鼠标的操作在模拟器本身就完全做不了选择操作。
Continue reading

PHP+JS验证银行卡号

本规则只应对主流的LUHN卡号生成规则的银行卡,需要说明的LUHN规则只能验证卡号是否符合规则,并不能验证是否有这张卡。

关于LUHN卡号的信息:

LUHN算法,主要用来计算信用卡等证件号码的合法性。
1、从卡号最后一位数字开始,偶数位乘以2,如果乘以2的结果是两位数,将两个位上数字相加保存。
2、把所有数字相加,得到总和。
3、如果信用卡号码是合法的,总和可以被10整除。
英文描述:
1.Counting from the check digit, which is the rightmost, and moving left, double the value of every second digit.
2.Sum the digits of the products (e.g., 10: 1 + 0 = 1, 14: 1 + 4 = 5) together with the undoubled digits from the original number.
3.If the total modulo 10 is equal to 0 (if the total ends in zero) then the number is valid according to the Luhn formula; else it is not valid.

LUHN算法或是LUHN公式,也被称作“模10算法”。它是一种简单的校验公式,一般会被用于身份证号码,IMEI号码,美国供应商识别号码,或是加拿大的社会保险号码的验证。该算法是由IBM的科学家Hans Peter LUHN所创造,于1954年1月6日提出该专利的申请,并于1960年8月23日被授予,在美国的专利号为2950048。
该算法一直都被大家所公用,并且时至今日应用也很广泛。它被指定在ISO/IEC7812-1。它的目的不是成为一种加密安全的哈希函数;它的目的是防止意外出现的错误,而不是恶意攻击。很多信用卡和众多的政府身份识别号码都使用该算法从一系列的随机数字中提取有效的数字。
优点和缺点
LUHN算法会检测到任何单码的错误以及几乎所有的相邻数字换位的错误。但是它不会检测两个数字序列09转90的错误(反之亦然)。它会检测到十分之七的相同双位数错误(不会检测到22和55的互换,33和66的互换,44和77的互换)。其他更复杂的检查数字算法,如费尔赫夫算法,可以检测出更多的转录错误。模N的Luhn算法是Luhn算法的一个扩展,支持非数字字符串。因为该算法采取了从右向左的方式,而且零位会影响计算的结果。只有当零位造成了数位的移动或是用零来填充一串数字的开头时才不会影响计算结果的生成。因此不论在将1234用零填充为0001234之前或是之后,使用LUHN算法得到的结果都是一样的。
该算法在美国专利上是为了给手持或是机械设备计算校验码。所以它必须尽可能的简单。

此代码验证主要为16或者19位。个别特殊位数的不好做处理
PHP代码

if(!$bank_card_number || (strlen($bank_card_number) != 16 && strlen($bank_card_number) != 19)){
echo json_encode(array('success'=>false,'data'=>array('msg'=>'银行卡号长度错误')));
exit;
}
$luhn_check = false;//是否对卡号进行luhn规则校验
if($luhn_check){
//luhn运算 校验卡号
$bank_card_number = (string)$bank_card_number;
$len = strlen($bank_card_number) - 1;
$sum = 0;
for($i = 1; $i <= $len; $i++){
if($i%2==1){
if($bank_card_number[$i-1]<5){ $sum += 2 * $bank_card_number[$i-1]; }else{ $sum += 2 * $bank_card_number[$i-1] - 9; } }else{ $sum += $bank_card_number[$i-1]; } } if( (intval($sum) + intval(substr($bank_card_number,-1,1))) % 10 != 0){ echo json_encode(array('success'=>false,'data'=>array('msg'=>'银行卡号错误')));
exit;
}
}

Javascript代码

/**
* Created by CheungQ on 8/7/2015.
*
* Description: 银行卡号Luhn校验算法
*
* luhn校验规则:16位银行卡号(19位通用):
*
* 1.将未带校验位的 15(或18)位卡号从右依次编号 1 到 15(18),位于奇数位号上的数字乘以 2。
* 2.将奇位乘积的个十位全部相加,再加上所有偶数位上的数字。
* 3.将加法和加上校验位能被 10 整除。
*
* bankno位银行卡号
*/
var Luhn = {
luhnCheck:function(bankno){
if(bankno.length != 16 && bankno.length !=19){
return false;
}

var lastNum=bankno.substr(bankno.length-1,1);//取出最后一位(与luhn进行比较)

var first15Num=bankno.substr(0,bankno.length-1);//前15或18位
var newArr=new Array();
for(var i=first15Num.length-1;i>-1;i--){ //前15或18位倒序存进数组
newArr.push(first15Num.substr(i,1));
}
var arrJiShu=new Array(); //奇数位*2的积 <9 var arrJiShu2=new Array(); //奇数位*2的积 >9

var arrOuShu=new Array(); //偶数位数组
for(var j=0;j<newArr.length;j++){
if((j+1)%2==1){//奇数位
if(parseInt(newArr[j])*2<9) arrJiShu.push(parseInt(newArr[j])*2); else arrJiShu2.push(parseInt(newArr[j])*2); } else //偶数位 arrOuShu.push(newArr[j]); } var jishu_child1=new Array();//奇数位*2 >9 的分割之后的数组个位数
var jishu_child2=new Array();//奇数位*2 >9 的分割之后的数组十位数
for(var h=0;h<arrJiShu2.length;h++){
jishu_child1.push(parseInt(arrJiShu2[h])%10);
jishu_child2.push(parseInt(arrJiShu2[h])/10);
}

var sumJiShu=0; //奇数位*2 < 9 的数组之和 var sumOuShu=0; //偶数位数组之和 var sumJiShuChild1=0; //奇数位*2 >9 的分割之后的数组个位数之和
var sumJiShuChild2=0; //奇数位*2 >9 的分割之后的数组十位数之和
var sumTotal=0;
for(var m=0;m<arrJiShu.length;m++){
sumJiShu=sumJiShu+parseInt(arrJiShu[m]);
}

for(var n=0;n<arrOuShu.length;n++){
sumOuShu=sumOuShu+parseInt(arrOuShu[n]);
}

for(var p=0;p<jishu_child1.length;p++){
sumJiShuChild1=sumJiShuChild1+parseInt(jishu_child1[p]);
sumJiShuChild2=sumJiShuChild2+parseInt(jishu_child2[p]);
}
//计算总和
sumTotal=parseInt(sumJiShu)+parseInt(sumOuShu)+parseInt(sumJiShuChild1)+parseInt(sumJiShuChild2);

//计算luhn值
var k= parseInt(sumTotal)%10==0?10:parseInt(sumTotal)%10;
var luhn= 10-k;

if(lastNum==luhn){
return true;//luhn验证通过
}
else{
return false;//luhn验证不通过
}
}
};

另外并不是所有的银行卡都是遵循LUHN规则生成卡号的,比如:

发行机构 卡号开始 使用 卡号长度 验证方式
American Express 34, 37 Yes 15 Luhn algorithm
Bankcard 5610, 560221-560225 No 16 Luhn algorithm
China UnionPay 62 Yes 16-19 no validation
Diners Club Carte Blanche 300-305 Yes 14 Luhn algorithm
Diners Club enRoute 2014, 2149 No 15 no validation

所以目前主要流行的银行卡验证方法是通过汇款转账一个数额较小的指定金额(如1元5角3分),通过这笔转账交易确认银行卡及持卡人的真实性。