分类: Javascript

一个开发中用到的报文压缩实现

之前开发中用到的一个报文压缩的实现方案,简单在这里单独提出来写一下,分为Java和JS两版代码。

Java版代码是服务端使用,用来在各服务端之间发送接收报文使用。JS版是在前端页面上用来查看或者调试接口报文使用的。

Java代码,压缩和解压缩方法


/**
 * 
 * 功能描述:字符串压缩 <br>
 * 将字符串压缩
 *
 * @param str 待压缩的字符串
 * @return 压缩后的字符串
 */
@SuppressWarnings("restriction")
public static String gzip(String str) {
    // 创建字符流
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    GZIPOutputStream gzip = null;
    try {
        // 对字符串进行压缩并写入压缩流
        gzip = new GZIPOutputStream(out);
        gzip.write(str.getBytes());
    } catch (IOException e) {
        String errMsg = e.getMessage();
        logger.error(errMsg);
    } finally {
        if (gzip != null) {
            try {
                gzip.close();
            } catch (IOException e) {
                String errMsg = e.getMessage();
                logger.error(errMsg);
            }
        }
    }
    // 返回压缩后的字符串
    return new sun.misc.BASE64Encoder().encode(out.toByteArray());
}

/**
 * 
 * 功能描述: 字符串解压<br>
 * 将字符串解压
 *
 * @param str 待解压的字符串
 * @return 解压后的字符串
 * @throws Exception
 */
@SuppressWarnings("restriction")
public static String gunzip(String str) {
    // 校验压缩数据
    if (str == null) {
        return null;
    }

    // 创建读取流
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ByteArrayInputStream in = null;
    // 解压缩流
    GZIPInputStream ginzip = null;
    byte[] compressed = null;
    // 原始字符串
    String decompressed = null;

    try {
        // 进行解压缩操作
        compressed = new sun.misc.BASE64Decoder().decodeBuffer(str);
        in = new ByteArrayInputStream(compressed);
        ginzip = new GZIPInputStream(in);

        byte[] buffer = new byte[BYTE_SIZE];
        int offset = -1;
        while ((offset = ginzip.read(buffer)) != -1) {
            out.write(buffer, 0, offset);
        }
        decompressed = out.toString();
    } catch (IOException e) {
        String errMsg = e.getMessage();
        logger.error(errMsg);
        logger.error("解析压缩字符串异常", e);
    } finally {
        if (ginzip != null) {
            try {
                ginzip.close();
            } catch (IOException e) {
                String errMsg = e.getMessage();
                logger.error(errMsg);
            }
        }
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                String errMsg = e.getMessage();
                logger.error(errMsg);
            }
        }
        try {
            out.close();
        } catch (IOException e) {
            String errMsg = e.getMessage();
            logger.error(errMsg);
        }
    }
    // 返回原始字符串
    return decompressed;
}

JavaScript代码,js的压缩解压缩需要调用到pako包的内容,可以在https://www.bootcdn.cn/pako/ 上找到需要的版本引用,或者简单点直接把需要min版本代码复制到你需要的页面里, https://cdn.bootcdn.net/ajax/libs/pako/2.0.4/pako.min.js,另外也用到浏览器自带的base64加密解密的方法btoa和atob

Continue reading

油猴脚本,抓取LeetCode题解[4]

接上文油猴脚本,抓取LeetCode题解[3],把json文件抓取下来之后就是在我的前端项目中怎么把内容再展示出来的事情了。今天偷了点空,做了上去

具体效果见http://next.cheungq.com/leetCode/solution/
只是简单的把HTML和Markdown文本展示出来了,没有做太多修饰。
在原来的nextjs项目中新增了,后续的再重新写下样式和一些交互上的功能

"react-markdown": "^6.0.3",
"github-markdown-css": "^5.1.0",

用来渲染实现Markdown文本的展示,“react-markdown”高版本的有问题,因为require和import引用方式的区别导致无法使用,不做过多赘述
另外尝试了其他的Markdown的依赖,另外有一些问题,比如nextjs不允许引用依赖中自己引用css文件 具体可以参看官方说明 “https://nextjs.org/docs/messages/css-npm

无刷新页面监听URL变化实现

上一篇文章中,我在LeetCode抓取题解的油猴脚本中新增了一个功能,在题解页面,单独显示一个按钮,点击按钮之后可以抓取当前页面的题解内容。

到这里就产生了一个问题,如何判断当前页面是一个题解的页面呢,举一个简单的题解页面的URL的例子

https://leetcode.cn/problems/find-duplicate-subtrees/solution/by-cheungq-6-fkom/

我们可以看到,除去前面的“https://”协议、“leetcode.cn”域名,后面的“/problems/find-duplicate-subtrees/solution/by-cheungq-6-fkom/”部分就是我们需要解析的部分,这部分内容存在于“window.location.pathname”之中,我们可以随时很方便的获取到,只需判断下是不是

/problems/{Question_Slug}/solution/{Solution_Slug}/

这样的格式即可,当然如果你非常熟悉正则的方式,也可以用正则匹配判断。这样我们就可以知道这个页面是不是一个题解的页面了,如果当前页面是一个题解页面,则我们进行控制显示一个抓取当前题解的按钮

在解决了题解页面判断之后,重新回到LeetCode网站看下,这时会发现我们遇到了新的问题。LeetCode的题目和题解页面使用的是无刷新的方法来渲染页面的,点击题目或者题解内容,页面进行渲染,同时Url进行变更,页面不进行刷新操作。这样的方式大大提升了用户的浏览体验。而很明显我们可以看到使用的不是hash模式的URL,而是history模式的实现。

hash模式的URL是在URL结尾拼接上“#”符及相关参数来实现的,变更URL中“#”后面的内容不会引发页面刷新操作,“#”原本是作为页面锚点的功能存在的

熟悉前端的小伙伴应该对hash模式history模式这两个概念非常了解了,基本属于面试必问基础题系列,如果不知道的可以再自行百度一下

那么在有了以上的基础认知之后,我们就开始着手对history模式URL变更进行监听操作

不同于hash模式的URL,当hash模式的URL发生变更的时候,会触发window.onhashchange事件(参见Window:hashchange event),这样的话我们只要监听window.onhashchange事件就可以了知道URL变更了。而history模式下,是通过history对象来操作实现的,根据MDN文档上的说明信息,在history操作的时候会触发popstate事件,不过下面有一条额外的备注

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event

调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件

在力扣的网页上试了下window.addEventListener('popstate',function)相关的操作,确实在点击了之后没有反应。

那么到这里我们又有了另一个思路,参照之前的【油猴脚本】关于用油猴脚本爬取考试题库这件事文章中使用的手法,我们自己建个replaceState和pushState方法替换掉原来history中的replaceState和pushState方法,在我们自己建的方法中调用原来浏览器的对应方法,并向外抛出一个相应的事件,于是就有了如下代码

let _historyWrap = function(type) {
    const orig = history[type];
    return function() {
        const rv = orig.apply(this, arguments);
        publishCustomEvent(type,arguments)
        return rv;
    };
};
history.pushState = _historyWrap('pushState');
history.replaceState = _historyWrap('replaceState');

其中的publishCustomEvent方法是调用的之前的window.CustomEvent浏览器自定义事件文章中写的自定义浏览器事件相关的代码。

这样,我们只要再监听下这里抛出的pushState和replaceState事件就可以了,代码如下

    window.addEventListener('pushState',(e)=>{
        //do something
        //判断当前url是否是题解页面
    },true)

油猴脚本,抓取LeetCode题解[3]

再更新一下,接之前的版本油猴脚本,抓取LeetCode题解[2],在原来的基础上增加了单独抓取单篇题解的功能,再做这个的时候其实遇到个问题,主要难点在于如何监听浏览器地址栏的变化,另开一个文章来说《无刷新页面监听URL变化实现

至于新增这个功能的原因如下,在使用初版的功能全量的抓取了自己的题解文章之后,对于每天新增的题解不能单独抓取补充到indexDB之中,只能再次全量抓取一下,显然不太方便,所以又改进了下增加了这样的功能

另外我试了下,这个抓取用户题解文章列表的接口是不做权限验证的,也就是说A用户可以抓取B用户的所有题解列表,我找了下使用场景,当A用户点开某个B用户的LeetCode个人主页的时候,下面有一块展示区域是防止当前B用户的题解列表的。这是一个正常使用的场景,也就是说,你可以抓取任何你喜欢的用户的所有题解列表

另外修改了一点样式。在右边单独建了小的Panel把按钮都放进去,到处分散的不大好看,

下一步内容,解析题目、题解中的图片标签,抓取图片内容并base64之后作为文本保存下来,使题解内容脱离对力扣图片资源的依赖

Continue reading

油猴脚本,抓取LeetCode题解[2]

接上篇,油猴脚本,抓取LeetCode题解,补充改版了下,顺便为啥要写这个呢?原因其实很简单,因为之前写了很多题解,都是直接在LeetCode上写的,加起来有快400篇了吧,然后自己这边博客每篇都要搬运一下,比较麻烦费事,所以才想着写了这么个工具。

这次改进做了如下

  1. 引入了IndexDBWrapper(https://www.npmjs.com/package/indexdbwrapper/v/1.0.4),封装了IndexDB的操作过程
  2. 直接任何力扣页面都可以操作了,账户需要登录
  3. 直接抓取我的题解了列表,之后根据列表再抓取题目和自己写的题解
  4. 使用了CustomEvent,抓取了列表之后,发起一个事件,页面同时监听事件,得到列表后进行抓取详细内容
  5. 定义了MY_ACCOUNT,需要手动替换为自己的账号ID
  6. 完成后点击下载按钮生成一个json文件下载
  7. 代码中对返回结果内容的字段进行了一些删除,删掉不需要的字段信息,缩小下载文件的大小

之后要做的事情,既然之前自己的写的题解已经拿到了,那么就是想办法再展示出来了,下一步可能想把下载得到的文件通过一个前端项目展示出来,不过这里还是有点问题,部分题解中含有图片内容,这个内容还是存在LeetCode服务器上的,这个还需要另外再想办法获取下来

Continue reading

油猴脚本,抓取LeetCode题解

用来抓取自己之前写的LeetCode刷题写的题解

// ==UserScript==
// @name         LeetCode Test
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://leetcode.cn/problems/*/solution/*
// @grant        none
// ==/UserScript==





(function() {
    'use strict';
    let createButton = (func, btnText, top) => {
        let btn = document.createElement("button")
        btn.style.position = "fixed"
        btn.style.right = 0
        // btn.style.top='30%'
        btn.style.top = top
        btn.style.padding = "10px"
        btn.style.zIndex = 99999
        btn.innerText = btnText
        btn.addEventListener("click", func)
        document.body.append(btn)
    }

    let dbName = 'solutionArticle', version = 1, storeName = 'mySolution'

    let indexedDB = window.indexedDB
    let db
    const request = indexedDB.open(dbName, version)
    request.onsuccess = function(event) {
        db = event.target.result // 数据库对象
        console.log('数据库打开成功')
    }

    request.onerror = function(event) {
        console.log('数据库打开报错')
    }

    request.onupgradeneeded = function(event) {
        // 数据库创建或升级的时候会触发
        console.log('onupgradeneeded')
        db = event.target.result // 数据库对象
        let objectStore
        if (!db.objectStoreNames.contains(storeName)) {
            objectStore = db.createObjectStore(storeName, { keyPath: 'questionFrontendId' }) // 创建表
            // objectStore.createIndex('name', 'name', { unique: true }) // 创建索引 可以让你搜索任意字段
        }
    }
    let saveToIndeDB = (data)=>{
        let request = db.transaction([storeName], 'readwrite') // 事务对象 指定表格名称和操作模式("只读"或"读写")
        .objectStore(storeName) // 仓库对象
        .add(data)

        request.onsuccess = function(event) {
            console.log('数据写入成功')
        }

        request.onerror = function(event) {
            console.log('数据写入失败')
            throw new Error(event.target.error)
        }
    }
    let cursorGetData = () =>{
        let list = []
        let store = db.transaction(storeName, 'readwrite') // 事务
        .objectStore(storeName) // 仓库对象
        let request = store.openCursor() // 指针对象
        return new Promise((resolve, reject) => {
            request.onsuccess = function(e) {
                let cursor = e.target.result
                if (cursor) {
                    // 必须要检查
                    list.push(cursor.value)
                    cursor.continue() // 遍历了存储对象中的所有内容
                } else {
                    resolve(list)
                }
            }
            request.onerror = function(e) {
                reject(e)
            }
        })
    }

    createButton( ()=>{
        cursorGetData().then(list=>{
            console.log(list)
        })
    } , "列表", "160px")




    let key = ''
    let addLocal = (k,v)=>{
        let oldJson = localStorage.getItem(key)
        if(null==oldJson){
            oldJson = {}
        }else{
            oldJson = JSON.parse(oldJson);
        }
        oldJson[k] = v;
        localStorage.setItem(key,JSON.stringify(oldJson))
    }
    let getQuestion = (slug)=>{
        let p = {
            "operationName":"questionData",
            "variables":{"titleSlug":slug},
            "query":"query questionData($titleSlug: String!) {\n  question(titleSlug: $titleSlug) {\n    questionId\n    questionFrontendId\n    categoryTitle\n    boundTopicId\n    title\n    titleSlug\n    content\n    translatedTitle\n    translatedContent\n    isPaidOnly\n    difficulty\n    likes\n    dislikes\n    isLiked\n    similarQuestions\n    contributors {\n      username\n      profileUrl\n      avatarUrl\n      __typename\n    }\n    langToValidPlayground\n    topicTags {\n      name\n      slug\n      translatedName\n      __typename\n    }\n    companyTagStats\n    codeSnippets {\n      lang\n      langSlug\n      code\n      __typename\n    }\n    stats\n    hints\n    solution {\n      id\n      canSeeDetail\n      __typename\n    }\n    status\n    sampleTestCase\n    metaData\n    judgerAvailable\n    judgeType\n    mysqlSchemas\n    enableRunCode\n    envInfo\n    book {\n      id\n      bookName\n      pressName\n      source\n      shortDescription\n      fullDescription\n      bookImgUrl\n      pressImgUrl\n      productUrl\n      __typename\n    }\n    isSubscribed\n    isDailyQuestion\n    dailyRecordStatus\n    editorType\n    ugcQuestionId\n    style\n    exampleTestcases\n    jsonExampleTestcases\n    __typename\n  }\n}\n"
        }
        let options = {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(p)
        }
        fetch("https://leetcode.cn/graphql/",options).then((res)=>{
            if(res.ok){
                //如果取数据成功
                res.json().then((data)=>{
                    //转化为json数据进行处理
                    console.log(data.data)
                    let question = data.data.question
                    addLocal('questionTitle',question.translatedTitle)
                    addLocal('questionFrontendId',question.questionFrontendId)
                    addLocal('questionFullTittle',"LeetCode刷题【"+question.questionFrontendId+"】"+question.translatedTitle)
                    let questionContentAppend = "<div>Related Topics</div><div><ul>"
                    let questionTags = [];
                    for(let tag of question.topicTags){
                        questionTags.push(tag.translatedName);
                        questionContentAppend += "<li>"+tag.translatedName+"</li>"
                    }
                    questionContentAppend += "</ul></div></div><br><div><li>👍 "+question.likes+"</li><li>👎 "+question.dislikes+"</li></div>"
                    addLocal('questionContent',question.translatedContent+questionContentAppend)
                    addLocal('questionTags',questionTags)
                    addLocal('questionTagsStr',"算法,LeetCode,"+questionTags.join(",")+",")
                    saveToIndeDB(JSON.parse(localStorage.getItem(key)))

                })
            }else{
                console.log(res.status);
                //查看获取状态
            }
        }).catch((res)=>{
            //输出一些错误信息
            console.log(res.status);
        })
    }
    let pathname = window.location.pathname
    let param = {
        "operationName":"solutionDetailArticle",
        "variables":{
            "slug":pathname.split("/")[4],
            "orderBy":"DEFAULT"
        },
        "query":"query solutionDetailArticle($slug: String!, $orderBy: SolutionArticleOrderBy!) {\n  solutionArticle(slug: $slug, orderBy: $orderBy) {\n    ...solutionArticle\n    content\n    question {\n      questionTitleSlug\n      __typename\n    }\n    position\n    next {\n      slug\n      title\n      __typename\n    }\n    prev {\n      slug\n      title\n      __typename\n    }\n    __typename\n  }\n}\n\nfragment solutionArticle on SolutionArticleNode {\n  rewardEnabled\n  canEditReward\n  uuid\n  title\n  slug\n  sunk\n  chargeType\n  status\n  identifier\n  canEdit\n  canSee\n  reactionType\n  reactionsV2 {\n    count\n    reactionType\n    __typename\n  }\n  tags {\n    name\n    nameTranslated\n    slug\n    tagType\n    __typename\n  }\n  createdAt\n  thumbnail\n  author {\n    username\n    profile {\n      userAvatar\n      userSlug\n      realName\n      __typename\n    }\n    __typename\n  }\n  summary\n  topic {\n    id\n    commentCount\n    viewCount\n    __typename\n  }\n  byLeetcode\n  isMyFavorite\n  isMostPopular\n  isEditorsPick\n  hitCount\n  videosInfo {\n    videoId\n    coverUrl\n    duration\n    __typename\n  }\n  __typename\n}\n"
    }
    let options = {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(param)
    }
    fetch("https://leetcode.cn/graphql/",options).then((res)=>{
        if(res.ok){
            //如果取数据成功
            res.json().then((data)=>{
                //转化为json数据进行处理
                //console.log(data);
                let {data:solutionArticle} = data
                let {solutionArticle:{author,content,question,title}} = solutionArticle
                if(author.username != "cheungq-6"){
                    return
                }
                console.log(solutionArticle)
                getQuestion(question.questionTitleSlug)
                key = '_____'+question.questionTitleSlug
                console.error("key:"+key)
                addLocal('title',title)
                addLocal('content',title+"\n"+content)
                addLocal('slug',question.questionTitleSlug)
            })
        }else{
            console.log(res.status);
            //查看获取状态
        }
    }).catch((res)=>{
        //输出一些错误信息
        console.log(res.status);
    })
    // Your code here...
})();

抓到的数据最终存到indexdDB中,中间存了下localStorage,后来改的存indexdDB,但是没删掉中间存localStorage的过程

MutationObserver网页元素变更监视

MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分。

构造函数:MutationObserver()

方法:

  • disconnect():解除监视
  • observe():监视某个dom元素的变更
  • takeRecords():从MutationObserver的通知队列中删除所有待处理的通知

代码:

let MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
let observer = new MutationObserver(function(mutations) {
    // console.log(mutations)
    mutations.forEach(function(record) {
        if(record.attributeName === "value" ){
            console.log(record.target.getAttribute("id"))
            console.log(record.oldValue)
            console.log(record.target.value)
        }
    });
});
let observerOptions = { attributes: true, childList: true, characterData: true, attributeOldValue :true, attributeFilter:["value"] }
observer.observe(document.getElementById("xxxxx"), observerOptions);

observerOptions中可以配置监听的变更内容。