Page 6 of 61

Elasticsearch 设置字段默认值

本期新开发需求,其中给ES索引新增了一个字段,对应的,历史数据中这个字段的值就是null了,那么就需要给历史数据写上默认值。写了个执行语句如下

POST /your_es_index/your_es_index/_update_by_query
{
  "script": {
    "lang": "painless",
    "inline": "if (ctx._source.store_id == null) {ctx._source.store_id = 'default_store_id'}"
  }
}

用更新把null修改为目标默认值,查看下更新操作的变化

GET /your_es_index/your_es_index/_search
{
  "query": {
    "exists": {
      "field": "store_id"
    }
  }
}

执行前结果83条,执行后478条,ES索引中总记录条数478条,说明确实都刷成默认值了,测试环境数据比较少

不过这里还踩了坑,一开始不是这么写的,如下

POST /your_es_index/your_es_index/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": "if (ctx._source.store_id == null) {ctx._source.store_id = 'default_store_id'}"
  }
}

一开始写的source,执行之后得到报错异常信息

{
 "error":{
  "root_cause":[{
   "type":"parse_exception",
   "reason":"expected one of [inline], [file] or [stored] fields, but found none"
  }],
  "type":"parse_exception",
  "reason":"expected one of [inline], [file] or [stored] fields, but found none"
 },
 "status":400
}

根据给出的信息“expected one of [inline], [file] or [stored] fields, but found none”,ES系统期望能接收到的字段为inline、file或者stored,查阅了相关资料后,判定为ES版本区别的原因,改为inline字段后就可以正常执行了。

相关信息

https://stackoverflow.com/questions/67488446/elasticsearch-bulk-update-geo-location-of-all-documents-with-curl

it looks like you're running an older version of ES. Try the command below which simply replaces source by inline as it was the norm in older versions

https://elasticsearch.cn/question/6458

其他的一些操作方法 https://blog.csdn.net/laoyang360/article/details/119012322


10月26,更新下,在生产环境执行的结果

执行后报了个异常

2022-10-25 17:10:48  java.io.IOException: listener timeout after waiting for [300000] ms
	at org.elasticsearch.client.RestClient$SyncResponseListener.get(RestClient.java:912)
	at org.elasticsearch.client.RestClient.performRequest(RestClient.java:233)
	at org.elasticsearch.client.RestClient.performRequest(RestClient.java:327)
......

超时了

由于变更执行语句需要重新走审批流程,所以先尝试重新执行了下,报了另外一个版本冲突的异常,其中一些信息我手动做了脱敏处理

2022-10-25 17:53:55  org.elasticsearch.client.ResponseException: method [POST], host [http://10.***.***.31:9***], URI [/the_es_*******_index/the_es_*******_index/_update_by_query], status line [HTTP/1.1 409 Conflict]
{"took":29772,"timed_out":false,"total":20462314,"updated":33984,"deleted":0,"batches":34,"version_conflicts":16,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1.0,"throttled_until_millis":0,"failures":[{"index":"the_es_*******_index","type":"the_es_*******_index","id":"103141993495926871","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][103141993495926871]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"167146670228584497","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][167146670228584497]: version conflict, current version [4] is different than the one provided [3]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"100618429327498328","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][100618429327498328]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"168276494396132973","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][168276494396132973]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"164741654402007284","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][164741654402007284]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"166332349807514318","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][166332349807514318]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"101164299438739744","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][101164299438739744]: version conflict, current version [4] is different than the one provided [3]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"101444916306189041","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][101444916306189041]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"165709803817590009","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][165709803817590009]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"172236348446552109","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][172236348446552109]: version conflict, current version [4] is different than the one provided [3]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"171252183551964717","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][171252183551964717]: version conflict, current version [4] is different than the one provided [3]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"94363645778869762","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][94363645778869762]: version conflict, current version [4] is different than the one provided [3]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"169670413328115540","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][169670413328115540]: version conflict, current version [6] is different than the one provided [5]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"94091161628589223","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][94091161628589223]: version conflict, current version [7] is different than the one provided [6]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"169393862032975651","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][169393862032975651]: version conflict, current version [5] is different than the one provided [4]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409},{"index":"the_es_*******_index","type":"the_es_*******_index","id":"168775992414826034","cause":{"type":"version_conflict_engine_exception","reason":"[the_es_*******_index][168775992414826034]: version conflict, current version [4] is different than the one provided [3]","index_uuid":"GRt4moPETEeYEdxhhkPOxw","shard":"1","index":"the_es_*******_index"},"status":409}]}
	at org.elasticsearch.client.RestClient$SyncResponseListener.get(RestClient.java:936)

咨询过平台服务的同事,超时只是客户端超时了,实际语句在系统中仍在继续运行。

通过执行查询语句,使用Elasticsearch的must_not来查询所修改字段仍无默认值的数据量

get /your_es_index/your_es_index/_search
{
  "query": {
    "bool": {
      "must_not": {
        "exists": {
          "field": "store_id"
        }
      }
    }
  }
}

确认到查询结果的数量是一直在变小的,那么确实是仍在运行的。

最终索引中总共2000多万条数据,执行消耗时间从下午5点到晚上8点半多执行完成,耗时3个半小时

最近写HIVE SQL的一点笔记【1】

几个点,一个个来

字段内容为JSON字符串,ETL过程中需要解析JSON字符串,并取出解析后的对象的某个值

处理方法,使用get_json_object()函数、get_json_object(string json_string, string path)

函数可以接收两个参数,第一个参数为需要解析的字段或者字符串,第二个字段为解析后取值的表达式,表达式固定以`$`符号开头。

举个栗子

直接查对象:

select get_json_object('{"abc":123}','$.abc') as result;

返回结果:

查询数组:

select get_json_object('["abc","bcd"]','$[1]') as result

返回结果:

也可以组合嵌套使用:

select get_json_object('["abc",{"bcd":123}]','$[1].bcd') as result
select get_json_object('{"def":["abc",{"bcd":123}]}','$.def[1].bcd') as result

如果查询一个不存在的key,则返回 null,比如

select get_json_object('{"aaa":["abc",{"bcd":123}]}','$.def[1].bcd') as result

或者解析的字符串格式不对,无法被解析成JSON对象,比如

select get_json_object('{"aaa}]}','$.def[1].bcd') as result

一个SafePoint和STW的问题

起因是前几天看的一篇文章,里面贴了一段代码,大意如下

public class SafePointTest {

    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            for (int i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            System.out.println(Thread.currentThread().getName()+"执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("num = " + num);
    }
}

执行之后输出如下,环境为Java8,没有添加其他jvm参数

从上面的结果可以看到,主线程在`Thread.sleep(1000)`的睡眠1秒之后并没有能直接执行下一步的sout代码,来输出num的的值,而是一直“阻塞”等到两个线程的for循环结束之后,才执行了sout输出代码。所以,我们可以很明显的猜测到,是这两个for循环的执行,对主线程的Thread.sleep产生了影响,让主线程未能从sleep的TIMED_WAITING状态返回RUNNABLE状态,我们可以改造下代码,观察下主线程的状态

略有点粗糙的改造,不过可以大致看到Thread.sleep之后主线程的状态变化,代码如下

public class SafePointTest {

    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Runnable r = () -> {
                    for (int i = 0; i < 1000000000; i++) {
                        num.getAndAdd(1);
                    }
                    System.out.println(Thread.currentThread().getName() + "执行结束!");
                };
                Thread t1 = new Thread(r);
                Thread t2 = new Thread(r);
                t1.start();
                t2.start();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("num = " + num);
            }
        };
        long time = System.currentTimeMillis();
        Thread t = new Thread(runnable);
        System.out.println((System.currentTimeMillis()-time) +" \t"+ t.getState());
        t.start();
        System.out.println((System.currentTimeMillis()-time) +" \t"+ t.getState());
        Thread.State state = t.getState();
        while (t.isAlive()){
            Thread.State currentState = t.getState();
            if (state.compareTo(currentState) != 0){
                System.out.println((System.currentTimeMillis()-time) +" \t"+ currentState);
                state = currentState;
            }
        }
        System.out.println((System.currentTimeMillis()-time) +" \t"+ t.getState());
    }
}

运行得到结果如下

也有可能获取到主线程(t)最后进入到BLOCKED状态的情况,如下

看下Thread.State的源码,BLOCKED状态的说明,我们可以看到如下注释内容,竞争锁资源的失败,加入等待锁的同步阻塞队列,进入阻塞状态。

Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling

看到锁,回头来看下刚才的代码,那么我们很自然的想到了sout方法中其实是有个锁的

那么这里出现主线程(t)BLOCKED状态很明显就是在和子线程(t1、t2)以及当前的main函数的线程,3个线程同时竞争打印锁的时候没有竞争成功而进入了阻塞状态。测试下,把t线程和t1、t2线程中的sout方法同时注释掉。这样就只有main函数的线程需要占用打印锁,多次运行之后均没有发现t线程出现BLOCKED状态的情况,在没有注释之前,主线程(t)进入BLOCKED的情况还是比较频繁容易出现的。

好了,BLOCKED的问题搞清楚之后,我们重新回到原来的问题之上,根据输出的内容,我们可以看到主线程(t)在sleep之后进入TIMED_WAITING状态,直到两个子线程(t1、t2)都执行结束的时候才恢复到RUNNABLE状态。

不卖关子,我们直接说结论

SafePoint

这就是产生这个现象的原因,JVM有个参数-XX:GuaranteedSafepointInterval,作用是控制所有线程进入SafePoint的时间间隔,默认是

-XX:GuaranteedSafepointInterval=1000

即每隔1000毫秒,所有线程进入一次SafePoint。如果有线程无法进入SafePoint,那么其他线程一直等待,直到所有线程都进入SafePoint了,再立刻从SafePoint中恢复。那么,什么是SafePoint呢?

Safepoint 可以理解成是在代码执行过程中的一些特殊位置当线程执行到这些位置的时候,线程可以暂停。线程在执行的时候,如果JVM要STW,暂停线程的执行,线程并不是可以在任意地方立刻暂停下来的,这可能和我们直觉有点不一样,在日常开发中,我们可以在代码的任意地方添加断点,从而让程序在断点的地方停下来。这个断点其实就是我们在代码中强制添加的SafePoint,稍后我们会再提到。

在我们的Java代码编写完之后,开始执行代码,理论上,JVM可以在每条字节码的边界都可以放一个SafePoint,但是更多的SafePoint就意味着需要更多的占用内存空间,因为在SafePoint中需要保存当前线程的各种上下文信息,因为随时可能在这个SafePoint处暂停。同时也会生成polling代码询问JVM是否要“进入SafePoint”,而polling操作也是有开销的。不同的 JVM 选用不同的位置放置 SafePoint。

通过JIT的优化,只在部分地方放置SafePoint。其中一个情况需要事先了解下,比如我们上面的例子中的for循环的counted loop的循环(可数循环),特指这种for ( int )的循环,相应的long类型的并不是,算作是uncounted loop(不可数循环),在我们使用的Java8版本中,JIT会认为counted loop较小,不会再循环中添加SafePoint。所以我们知道上面的代码中for (int i = 0; i < 1000000000; i++)的循环体内部是没有SafePoint的。

当然也有其他需要添加SafePoint的情况,根据之前RednaxelaFX的说法(see https://www.zhihu.com/question/29268019/answer/43762165

而在JIT编译的代码里,HotSpot会在所有方法的临返回之前,以及所有非counted loop的循环的回跳之前放置safepoint。

另外我们需要知道一点(see https://zhuanlan.zhihu.com/p/161710652

当运行 native 代码时,VM 线程略过这个线程,但是给这个线程设置 poll armed,让它在执行完 native 代码之后,它会检查是否 poll armed,如果还需要停在 SafePoint,则直接 block

回到我们当前的代码中,我们看原来的第一版代码,主线程在t1、t2线程启动之后,执行了一个Thread.sleep(1000);,而Thread.sleep方法就是一个native方法,进入native方法后主线程不受JVM控制,而等待1000毫秒

1000毫秒之后。尝试回到JVM环境的控制中,需要检查SafePoint的状态,但是根据JVM的默认设定-XX:GuaranteedSafepointInterval=1000,每1000毫秒所有线程应当进入SafePoint一次,那么当前主线程应当进入SafePoint,此时两个子线程t1、t2还在进行1000000000的循环当中,这里的循环当中没有SafePoint,无法暂停,必须等到循环结束。

所以此时主线程就一直卡在当前这个状态,直到t1、t2两个线程执行完for循环进入了SafePoint。

为了验证上面的想法,我们再次修改下初版的代码,如下

public class SafePointTest {

    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            System.out.println(Thread.currentThread().getName() + "执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        tryMakeSafePoint();
        System.out.println("num = " + num);
    }

    private static void tryMakeSafePoint() {
        int i = 0;
        for (; i < 10000000; i++) {
        }
        System.out.println(1);
    }
}

我们新增了一个tryMakeSafePoint方法,尝试强制添加一个SafePoint,怎么添加呢?我们在System.out.println(1);这里打上一个断点。同时,为了不触发这个断点,我们给这个断点设置一个条件

当i==0的时候才触发断点,那么根据当前的条件,这个断点永远都不会触发。在这里我们需要知道,我们打的断点,本质上就是强制在对应代码行的位置添加了一个SafePoint,并让当前程序运行的时候停止在这个SafePoint。所以在这个SafePoint的位置,我们可以通过debug查看到当前线程的上下文,各种变量等。另外为了排除定时进入SafePoint参数的影响,我们给JVM添加参数-XX:GuaranteedSafepointInterval=100000,把定时进入SafePoint的时间配置为100秒,排除这项的影响,因为子线程t1、t2最终执行完也只需要不到30秒

所以主线程在现在的断点位置进入SafePoint,并停在SafePoint,等待其他线程进入SafePoint。

那么接下来跑一下debug看下结果。

可以看到,结果确实如我们所预测的那样

下一步,我们重新回到初版代码,再进一步验证,我们给JVM在添加两个新的参数,现在的参数如下(默认定时每1000毫秒进入一次SafePoint)

-XX:+SafepointTimeout
-XX:SafepointTimeoutDelay=1000

这两个参数用于控制启用进入SafePoint的超时提示机制,以及超时的时间。运行得到如下结果

从打印出来的结果,可以看到JVM检测到两个线程(Thread-0和Thread-1)在尝试定时进入SafePoint的时候超时了,并不包含主线程,因为主线程从native代码回来后直接进入SafePoint了。

再一次修改JVM参数,如下

-XX:+SafepointTimeout
-XX:SafepointTimeoutDelay=1000
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1

我们新增了PrintSafepointStatistics参数,-XX:+PrintSafepointStatistics –XX:PrintSafepointStatisticsCount=1这两个参数可以强制JVM打印safepoint的一些统计信息,运行后可以看到如下结果

打印出来的spin值指的是SafepointSynchronize在同步每个线程时做的自旋。(see https://www.cnblogs.com/liboware/p/15431232.html)即,主线程等待两个子线程t1、t2进入SafePoint的时候进行自旋花费了25.8秒,又主线程之前sleep了1000毫秒,总时间也就是这边显示的大概26.9秒

不过从上面的结果中我们还看到了在SafePoint中进行的一些其他操作EnableBiasedLocking、RevokeBias,这两个就是JVM在SafePoint中进行的启用和取消偏向锁相关的操作

摒除这部分信息,我们在JVM参数中再加一条

-XX:-UseBiasedLocking

再跑一遍加了断点的代码看下,可以看到

多了ForceSafepoint,不过ForceSafepoint的相关信息我搜了下却非常少,不过这也是我断定打的断点就是强制在对应的代码处添加了SafePoint的原因,有兴趣的同学可以继续深入探索下。

饶了几圈兜兜转转,我们再次回到这个问题本身,要解决这个问题也很简单,

  1. 把int的for循环修改为long的就可以,因为long的属于uncounted loop(不可数循环),其中可以插入SafePoint
  2. 使用-XX:+UseCountedLoopSafepoints 参数去强制在可数循环中也放置安全点,不过这个参数在 JDK 8 下有 Bug,有导致虚拟机崩溃的风险
  3. 居官方说法,JDK10之后修复了这个问题,我电脑上只有18,索性我就直接拿18跑了下看下

输出字符集有点问题,不过不影响我们看到结果主线程在sleep了1000毫秒后正确输出了结果

另外还看到了一个有趣的操作,又想加SafePoint,又不想让代码每次都检查SafePoint,在遍历循环中,手动插入能进入SafePoint的代码,并只在一定的情况下才进入这个SafePoint,这也是我写本文的起因

这个代码片段出自 RocketMQ 的源码

相关文章https://mp.weixin.qq.com/s/JQgzDAuCcVL0AAHEvtI_-g

而进行相关搜索的时候我也发现有文章提到STW时间过长,发现清理时间其实很短,但是等待某些线程进入SafePoint的时间很长,而导致整个STW时间过长的问题

油猴脚本,抓取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