分类 其它 下的文章

今天的博客来自 JuiceFS 云服务用户 Jerry,他们通过使用 JuiceFS snapshot 功能,创新性地实现了数据的版本控制。Jerry,是一家位于北美的科技公司,利用人工智能和机器学习技术,简化用户购买汽车和家庭保险的比较及购买流程。

在软件开发领域,严格的测试和受控发布已经成为几十年来的标准做法。但如果我们能将这些原则应用到数据库和数据仓库中会怎样?想象一下,能够为数据基础设施定义一套带有测试用例的标准,自动应用于每个新的"发布",以确保客户始终看到准确和一致的数据。这将会极大改善数据质量。

01 挑战:为什么端到端测试在数据管理中并不常见

这个想法看似直观,但端到端测试在数据管理中并不常见,因为它需要数据库或数据仓库具备克隆或快照的功能,而大多数数据系统都不提供这一功能。

现代数据仓库本质上是随时间变化的有组织的可变存储,我们通过数据管道对其进行操作。数据通常在生成后立即对最终客户可见,没有"发布"的概念。当没有这个发布概念,对数据仓库进行端到端测试就没有多大意义。因为无法确保测试所看到的内容就是客户将看到的内容,这些数据在不断因为数据管线的修改而变化。

所以问题的核心,就是要在实现一种数据发布的机制,这种机制能够把某一个时刻数据仓库的状态提取成一个“快照“,并且控制这个”快照“对最终用户的可见性。这样,这个快照就成为一个”发布工件“,我们控制它什么条件、什么时间最终可以让用户看见。

02 现有方法及其局限性

一些团队在数据仓库之上开发了版本控制系统。他们不直接修改最终用户查询的表,而是为变更创建新版本的表,并使用原子交换操作来"发布"表。虽然这种方法在某种程度上有效,但它带来了重大挑战:

  • 高效实施"创建和交换"模式并不容易;
  • 确保涉及多个表的一致性(例如,验证订单表中的每一行在价格表中都有对应行)需要将多个表的变更"打包"成一个"事务",这也具有挑战性,不仅仅实现这种模式是困难的,这种模式也要求数据管线被相对严格的编排。

03 解决方案:由 JuiceFS 支持的 ClickHouse 数据库克隆

我们开发了一个系统,利用 JuiceFS snapshot 功能将 ClickHouse 数据库"克隆"为副本。这种方法在我们早前的文章 "低成本读写分离:Jerry构建主从ClickHouse架构" 中有详细介绍。

它的工作原理如下:

  • 我们在 JuiceFS 上运行 ClickHouse 数据库,JuiceFS 是一个由对象存储服务(OSS)支持的 POSIX 兼容共享文件系统。
  • JuiceFS 提供了一个实现 git 分支语义的"快照"功能。
  • 使用简单的命令如
    juicefs snapshot src_dir des_dir
    ,我们可以创建 src_dir 在那一刻的克隆。

这种方法使我们能够轻松地从运行中的实例复制/克隆 ClickHouse 实例,创建一个可以被视为"发布工件"的冻结快照。

04 使用数据库克隆实施端到端测试

有了这种机制,我们可以对 ClickHouse 副本运行端到端测试,并根据测试结果控制其可见性。

现在可以使用常见的单元测试框架(我们使用 pytest )开发、组织和迭代数据端到端测试。这种方法使我们能够将数据可用性和可靠性的基础设施和业务标准编码成数据测试。

一个典型的测试是表大小测试,它有助于防止由意外或临时表损坏导致的数据问题。还可以定义业务标准,以保护数据报告和分析免受数据管道中可能导致数据错误的意外更改的影响。例如,用户可以在一列或一组列上强制唯一性以避免重复——这在计算营销成本时是一个关键因素。

在 Jerry,这种架构在近几个季度中发挥了至关重要的作用,有效防止了几乎所有可能暴露给最终客户的 P0 级数据问题。

这种方法不仅限于 ClickHouse。如果在 JuiceFS 之上运行任何类型的数据湖或湖仓,采纳本文描述的发布机制可能会更容易。

05 结论

通过将现代软件开发实践引入数据管理世界,我们可以显著提高数据质量、可靠性和一致性。数据库克隆和端到端测试的结合为确保客户始终看到正确的数据提供了强大的工具集,就像他们期望在经过充分测试的软件发布中看到正确的功能一样。

下图展示了我们的数据库发布和端到端测试过程的工作流程。

这个架构的诞生标志着我们在缩小软件开发与数据管理之间的差距方面迈出了重要一步,为数据领域的创新和质量保障开辟了全新的可能性。

希望这篇内容能够对你有一些帮助,如果有其他疑问欢迎加入
JuiceFS 社区
与大家共同交流。

相关:

python编写的扫雷游戏

如何使用计算机程序求解扫雷游戏


image-20241115143032377



本文中实现的《扫雷》游戏的AI解法的项目地址:

https://openi.pcl.ac.cn/devilmaycry812839668/AI_mine_game


该项目的解法效果:

image-20241115125404601



之前介绍了网上的一些解决《扫雷》游戏的一些解法,包括DQN和启发式等AI算法,看着这些的实现个人有些手痒,于是就花了些时间自己用python代码实现了一个启发式方法求解《扫雷》游戏的算法。


求解《扫雷》游戏,很多人给出了很多启发式规则,但是实际上我个人认为就两条或者说三条,其他的那些规则属于在这两条或者说是三条规则基础上衍生的,实际上用这两条或三条规则就足够。

第一条:

如果一个格子揭开后其显示的周围有雷的数量和周边8个格子中未揭开的格子数量相同,那么说明这几个未被揭开的格子都是有雷的;

第二条:

如果一个格子揭开后其显示的周围有雷的数量和周边8个格子中标记的有雷的格子数量相同,那么说明其他几个未被揭开的格子都是无雷的。

第三条:

之所以第三条是独立出来的,因为这一条并不等同于前两条那么基础和必要,或者说没有前两条规则那么肯定不能行,但是没有第三条规则其实很多情况下也是可行的。但是有第三条的话会一定程度上提高我们的胜利比率。(
注意:
扫雷游戏在很多情况下是没有确定的胜利的情况的,也就是说在某种情况下能否胜利是要看概率的,而我们写的算法代码可以看作只是为了去尽可能接近这个概率而已)

第三条规则就是一个格子显示雷的数值在某些情况下是可以通过附近24个格子的数值进行优化的,比如一个格子的坐标为(x, y)那么在其-2,+2的范围下的其他标有数值的格子是可能和其进行化简的。

比如:一个格子坐标为(x, y)数值为3,其周边8个格子标有雷的数量为0,未被揭开的格子有四个,我们假设这4个未揭开的格子的真实有雷和无雷的情况分别用0或1表示,那么我们可以得到下面这个等式:

a+b+c+d=3

而坐标为(x+2, y+2)的一个格子可以得到下面的等式:

a+b+c=2

那么我们可以得到结论,那就是有无雷情况用d表示的格子肯定有雷,因为:

set(a,b,c,d)-set(a,b,c)=d

3-2=1

同理,如果假设(x-2, y-2)的一个格子的数值表示为a+b+c+d+e=3,那么我们可以判断出e这个表示的格子的情况一定为无雷的。

其实,第三条虽然是一种集合运算,但是其模拟的却是一种人类所用的数学推理的方法,在考虑使用这个方法之前曾经考虑过用python的线性代数运算library来实现这种数学推理的计算,但是感觉这么搞有些离谱,总觉得这个问题应该不至于到这个程度,然后盯着游戏画面好久,突然灵感一现,发现这个推理过程虽然看似是一个矩阵运算那种线性代数运算,但是如果只是小范围来看这个其实就是很简单的线性代数,因为每个变量的取值只能为0或1,并且可以完全使用for循环的方式加上set集合运算的方式实现消元化简,从而使用较为简单的编码方式就可以实现这种推理过程,而不需要搞进来一个线性数学的library,不过后来发现GitHub上的其他人实现的也都是大致用这种set结合加for循环的方式实现消元操作,看来这估计是正解。


在这三条的规则基础上我还加入了概率判断,这一点体现在随机进行格子选择时,我们可以根据一个未知格子周边(附近8个)已知格子的显示数值和这8个格子与其周边的另个格子中未知雷的数量计算出这个已知格子对这个目标的未知格子的又雷概率,我们可以取这个未知格子周边已知格子推断出的有雷概率取max,然后再计算出当前一共有多少未知格子和多少雷,然后计算出完全随机选格子的有雷概率,然后再在其中选择概率最小的,这样就能找到最小概率有雷的格子,以此实现最小概率触发雷。


再往下就是使用前面的前两个规则判断刚选的格子(假设此时为触发雷)是否可以判断出周边格子的情况。但是,这里我又加入一个规则,那就是一个格子被揭开显示数值后会影响其周边8个格子中未知格子的推理,导致这8个格子中的未知格子有可能被推理出来,因此我们将每个先揭开的格子或标记的格子其周边的格子保存起来,然后再对这些保存的各种进行判定,看其周边的格子是否可以被推理出来。

需要注意的是,一个格子被揭开显示数值或者被标记有雷后,其周边受影响可能被推断出的格子为附近8个,这时可以根据之前给出的前两条规则进行判断和推理,但是一个格子被揭开显示数值或者被标记有雷后其周边24个格子包括其自身,也就是共25个格子的set集合都有可能被推理化简,也就是之前所谓的for+set实现的消元操作。通过将这些规则结合在一起也就有了本文给出的代码实现。



最终的代码实现:

import numpy as np
import random
from typing import List


def belong_to(h, w, H, W):
    near = []
    for i in range(h-2, h+3):
        for j in range(w-2, w+3):
            if i>=0 and j>=0 and i<H and j<W and (i,j)!=(h,w):
                near.append((i, j))
    return near

def near_by(h, w, H, W):
    near = []
    for i in range(h-1, h+2):
        for j in range(w-1, w+2):
            if i>=0 and j>=0 and i<H and j<W and (i,j)!=(h,w):
                near.append((i,j))
    return near

def mine_count(h, w, real_state:np.array, H, W):
    count = 0
    for i, j in near_by(h, w, H=H, W=W):
        if real_state[i][j]==1:
            count += 1
    return count


class Env():
    def __init__(self, H, W, N):
        self.H = H
        self.W = W
        self.N = N

        # real state中0表示无雷,1表示有雷
        self.real_state = np.zeros((H, W), dtype=np.int32)
        self.mine = set()
        while len(self.mine)!=N:
            self.mine.add(random.randint(0, H*W-1))
        for x in self.mine:
            # print(x, self.H, self.W)
            # print(self.real_state.shape)
            self.real_state[x//self.W][x%self.W] = 1    
            
        # state_type中0表示无雷,1-8表示有雷, 用此来表示对附近雷的计数
        self.state_type = np.zeros((H, W), dtype=np.int32)  
        for i in range(H):
            for j in range(W):
                self.state_type[i][j] = mine_count(h=i, w=j, H=H, W=W, real_state=self.real_state)

        # obs为-100表示未翻开(未知),0-8表示翻开但无雷,数值大小表示翻开位置周边雷的数量
        # agent的状态记录所用,也可以用来作为打印之用
        self.obs = np.zeros((H, W), dtype=np.int32) -100
    
    def act(self, i, j):
        done = False
        if self.obs[i][j]!=-100:
            print("该位置已经被揭开过,重复翻开,error!!!")
            return ValueError
        if self.real_state[i][j] == 1:
            # game over 触雷
            done = True
            return None, done

        self.obs[i][j] = self.state_type[i][j]
        return self.obs[i][j], done
        
    def pp(self):
        for i in range(self.H):
            for j in range(self.W):
                if self.obs[i][j]>=0:
                    print(self.obs[i][j], end=' ')
                else:
                    print('*', end=' ')
            print()
        
    def input(self):
        while True:
            i, j = input('请输入坐标:').split()
            _, done = self.act(int(i), int(j))
            if done:
                print('game over!!!')
                print(self.real_state)
                print(self.state_type)
                break
            self.pp()
        
# 测试用
# env=Env(5, 5, 5)
# env.input()

def play():
    N = 99  # 雷的数量
    H = 16
    W = 30
    env = Env(H=H, W=W, N=N)  # H=36, W=64, N=100

    known_count_dict = {} # (2,2):3, (3,3):2

    known_set = set()   #  (2, 2)
    unknown_set = set()
    boom_set = set()
    for i in range(H):
        for j in range(W):
            unknown_set.add((i,j))

    new_nodes = []
    new_relation_nodes_set = set()

    while(len(unknown_set)>0):
        probs_list = [] # ((1,1), 0.5, 3), ((2,2), 0.5, 2) # (node, prob, count) # count为node附近的unknown个数
        for node in unknown_set:
            p_list = []
            n_c = 0  # node附近的unknown_node的个数
            for _node in near_by(*node, H, W):
                if _node in unknown_set:
                    n_c += 1
                if _node in known_set:
                    count = known_count_dict[_node]
                    n = 0
                    for _node_node in near_by(*_node, H, W):
                        if _node_node in unknown_set:
                            n += 1
                        if _node_node in boom_set:
                            count -= 1
                    p_list.append(count/n) # 有雷的概率
            p_list.append(N/len(unknown_set))
            probs_list.append((node, max(p_list), n_c))
        m_p = min(probs_list, key=lambda x:x[1])[1]
        probs_list = [x for x in probs_list if x[1]==m_p]
        node = min(probs_list, key=lambda x:x[2])[0]
        
        count, done = env.act(*node)
        if done == True:
            print('游戏失败,触雷,game over!!!')
            print(node)
            raise Exception
        print("成功完成一步!!! \n\n")
        print("remove node:", node)
        unknown_set.remove(node)
        known_set.add(node)
        known_count_dict[node] = count
            
        env.pp()  # 打印当前游戏环境的显示

        new_nodes.append(node)
        new_relation_nodes_set.add(node)
        
        
        while new_nodes or new_relation_nodes_set:
            # debug
            # print(new_nodes)
            # print(new_relation_nodes_set)
            while new_nodes:
                node = new_nodes.pop()
                k = 0
                b = 0
                count = known_count_dict[node]
                tmp_unk = set()
                for _node in near_by(*node, H, W):
                    if _node in known_set:
                        new_relation_nodes_set.add(_node)
                        # k += 1
                        continue
                    if _node in boom_set:
                        new_relation_nodes_set.add(_node)
                        b += 1
                        continue
                    tmp_unk.add(_node) # 对unknown节点进行判断
                count -= b
                if count==len(tmp_unk):
                    # 全是雷
                    for _node in tmp_unk:
                        print("remove node:", _node)
                        unknown_set.remove(_node)
                        boom_set.add(_node)
                        new_relation_nodes_set.add(_node)
                        N -= 1
                if count==0 and len(tmp_unk) > 0:
                    # 全都不是雷
                    for _node in tmp_unk:
                        c, done = env.act(*_node)
                        if done:
                            print("程序判断出错,把雷误触发了!!!")
                            raise Exception

                        print("remove node:", _node)
                        unknown_set.remove(_node)
                        known_set.add(_node)
                        known_count_dict[_node] = c
                        new_nodes.append(_node)
                        new_relation_nodes_set.add(_node)
                
            while new_relation_nodes_set:
                node = new_relation_nodes_set.pop()
                tmp_set = set()
                for i in range(-2, 3):
                    for j in range(-2, 3):
                        if node[0]+i>=0 and node[0]+i<H and node[1]+j>=0 and node[1]+j<W:
                            if (node[0]+i, node[1]+j) in known_set:
                                if known_count_dict[(node[0]+i, node[1]+j)]==0:
                                    continue
                                tmp_set.add((node[0]+i, node[1]+j))
                if len(tmp_set)==0:
                    continue

                relations = []
                for node in tmp_set:  # node 为 known set
                    tmp_tmp_set = set()
                    c = known_count_dict[node]
                    for _node in near_by(*node, H, W):
                        if _node in boom_set:
                            c -= 1
                            continue
                        if _node in unknown_set:
                            tmp_tmp_set.add(_node)
                            continue
                    if len(tmp_tmp_set)==0:
                        continue
                    relations.append([tmp_tmp_set, c, node])

                if len(relations)<2:
                    continue
                for i in range(0, len(relations)):
                    for j in range(1, len(relations)):
                        if relations[i][0].issuperset(relations[j][0]):
                            relations[i][0] -= relations[j][0] 
                            relations[i][1] -= relations[j][1]
                            
                        if relations[i][1]==len(relations[i][0]) and relations[i][1]>0:
                            # 全是雷
                            for _node in relations[i][0]:
                                if _node in boom_set:
                                    continue
                                print("remove node:", _node)
                                unknown_set.remove(_node)
                                boom_set.add(_node)
                                new_relation_nodes_set.add(relations[i][2])
                                N -= 1
                        if relations[i][1]==0 and len(relations[i][0]):
                            # 全都不是雷
                            for _node in relations[i][0]:
                                if _node in known_set:
                                    continue
                                c, done = env.act(*_node)
                                if done:
                                    print("程序判断出错,把雷误触发了!!!")
                                    raise Exception
                                print("remove node:", _node)
                                unknown_set.remove(_node)
                                known_set.add(_node)
                                known_count_dict[_node] = c
                                new_nodes.append(_node)
                                new_relation_nodes_set.add(_node)

                        if relations[j][0].issuperset(relations[i][0]):
                            relations[j][0] -= relations[i][0] 
                            relations[j][1] -= relations[i][1]
        
                        if relations[j][1]==len(relations[j][0]) and relations[j][1]>0:
                            # 全是雷
                            for _node in relations[j][0]:
                                if _node in boom_set:
                                    continue
                                print("remove node:", _node)
                                unknown_set.remove(_node)
                                boom_set.add(_node)
                                new_relation_nodes_set.add(relations[j][2])
                                N -= 1
                        if relations[j][1]==0 and len(relations[j][0]):
                            # 全都不是雷
                            for _node in relations[j][0]:
                                if _node in known_set:
                                    continue
                                c, done = env.act(*_node)
                                if done:
                                    print("程序判断出错,把雷误触发了!!!")
                                    raise Exception
                                print("remove node:", _node)
                                unknown_set.remove(_node)
                                known_set.add(_node)
                                known_count_dict[_node] = c
                                new_nodes.append(_node)
                                new_relation_nodes_set.add(_node)


    print('游戏胜利,game over!!!')
    return True

sss = []
for xyz in range(30000):
    try:
        sss.append(play())
        print('第 %d 次游戏成功'%xyz)
    except Exception:
        print('第 %d 次游戏失败!!!'%xyz)
        continue
print("成功次数: ", sum(sss))
print("成功比例: ", sum(sss)/30000)



个人github博客地址:
https://devilmaycry812839668.github.io/

前言

在开发和维护 .NET 应用程序的过程中,有时会遇到难以捉摸的性能瓶颈或内存泄漏等问题。这些问题往往发生在生产环境中,难以复现。为了更准确地诊断这些运行时问题,通常会收集应用程序在生产环境中的内存转储文件(.dump 文件)。在这种情况下,分析内存转储文件(.dump 文件)成为解决问题的重要手段。

本文将详细介绍如何使用 Visual Studio 分析 .NET 应用程序的内存转储文件(.dump 文件),以便诊断内存泄漏、性能问题或其他运行时异常。

准备工作

在开始分析之前,我们需要准备的开发环境,确保有以下条件

  • Visual Studio
    :至少需要 Visual Studio 2019 或更高版本。
  • .NET 应用程序
    :需要分析的应用程序。
  • .dump 文件
    :需要分析的内存转储文件。

Dump 文件是什么

内存转储文件(.dump 文件)是一种包含了程序在某个时刻内存快照的文件。它记录了程序的运行状态,包括内存分配、线程状态以及寄存器值等信息。当应用程序崩溃或出现异常行为时,转储文件可以帮助我们诊断问题所在。

分析 .NET Dump 的步骤

第一步:准备 .dump 文件

1、
获取 .dump 文件

在出现问题的应用程序上生成内存转储文件。这可以通过多种方式完成,例如使用
windbg
或通过 Visual Studio 的远程调试功能。

为了演示如何创建和分析 .NET 应用程序的内存转储文件,我们编写一段简单的 .NET 控制台应用内存泄漏代码,该应用存在明显的内存泄漏问题。

var listRuesult = new List<string>();while (true)
{
listRuesult.Add(
new string('a', 20000));
Console.WriteLine(
"添加成功!");
Thread.Sleep(
1000);
}

首先运行上面这段代码,我们可以在Visual Studio 中进程看到这段代码的情况,具体如下图所示

然后,打开任务资源管理,找到我们刚才的应用程序,在进程中选择右击,可以看到创建转储文件,点击就可以,生成.dump 文件,具体操作如下图所示:

2、
传输 .dump 文件
:将生成的 .dump 文件传输到我们的开发环境中。

第二步:打开 Visual Studio 加载 .dump 文件

1、
打开转储文件

在 Visual Studio 中,选择“文件” > “打开” > “转储文件”,然后选择之前准备好的 .dump 文件。

2、
加载符号

在加载转储文件后,可能需要加载符号文件来获取详细的调试信息。可以通过“调试” > “选项和设置” > “符号”来配置符号路径。

第三步:分析转储文件

1、
查看堆栈跟踪

通过“调试” > “窗口” > “调用堆栈”来查看转储文件中的堆栈跟踪。

2、
检查内存状态

使用“调试” > “窗口” > “内存”来查看内存分配情况。

3、
分析内存泄漏

利用“调试” > “窗口” > “对象浏览器”来查找可疑的内存泄漏。

4、
性能分析

使用“调试” > “窗口” > “性能探查器”来分析性能瓶颈。

第四步:解决问题

1、定位问题

根据转储文件中的信息,定位导致问题的原因。

2、
修复问题

修复找到的问题,并重新测试以验证修复是否有效。

线程调用堆栈

线程调用堆栈(Call Stack)是在线程的内存中操作的数据结构,它用于记录线程当前执行的方法或函数以及这些方法之间的调用关系。每当一个新的线程在应用程序中被启动时,操作系统会为这个线程分配一定量的内存空间,其中一部分就是用来存储该线程的调用堆栈信息。

简而言之,调用堆栈记录了程序运行过程中各个函数调用的历史和当前状态,每个线程都有自己的独立调用堆栈,这样可以确保线程间的调用历史不会相互干扰。当线程开始执行时,调用堆栈为空;随着函数的调用,新的条目被压入堆栈;当函数返回时,对应的条目从堆栈中弹出。

这种机制对于调试和理解程序的执行流程非常重要,特别是在多线程环境中,因为它可以帮助开发者追踪每个线程的执行路径和状态。

总结

通过使用 Visual Studio 分析 .NET 应用程序的内存转储文件,可以深入了解应用程序在运行时的状态,并有效地诊断和解决问题。希望本文能够帮助大家更好地理解和使用这一强大的工具。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!

JSON.parse 是我们在前端开发中经常会用到API,如果我们要自己实现一个JSON.parse,我们应该怎么实现呢?今天我们就试着手写一个JSON Parser,了解下其内部实现原理。

JSON语法

JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null 。语法规则如下:

  • 数据使用名/值对表示。
  • 使用大括号({})保存对象,每个名称后面跟着一个 ':'(冒号),名/值对使用 ,(逗号)分割。

file

  • 使用方括号([])保存数组,数组值使用 ,(逗号)分割。

file

  • JSON值可以是:数字(整数或浮点数)/字符串(在双引号中)/逻辑值(true 或 false)/数组(在方括号中)/对象(在花括号中)/null

file

实现Parser

Parser 一般会经过下面几个过程,分为
词法分析 、语法分析、转换、代码生成
过程。

词法分析

file

通过对 JSON 语法的了解,我们可以看到 JSON 中会有一下类型及其特征如下表:

类型 基本特征
Object "{" ":" "," "}"
Array "[" "," "]"
String '"'
Number "0" "1" "2" "3" "4" "5" "6" "7" "8" "9"
Boolean "true" "false"
Null "null"

所以根据这些特征,对 JSON 字符串进行遍历操作并与上述特征进行对比可以得到相应的 token。词法分析实现代码如下:

// 词法分析
const TokenTypes = {
  OPEN_OBJECT: '{',
  CLOSE_OBJECT: '}',
  OPEN_ARRAY: '[',
  CLOSE_ARRAY: ']',
  STRING: 'string',
  NUMBER: 'number',
  TRUE: 'true',
  FALSE: 'false',
  NULL: 'null',
  COLON: ':',
  COMMA: ',',
}

class Lexer {
  constructor(json) {
    this._json = json
    this._index = 0
    this._tokenList = []
  }

  createToken(type, value) {
    return { type, value: value || type }
  }

  getToken() {
    while (this._index < this._json.length) {
      const token = this.bigbang()
      this._tokenList.push(token)
    }
    return this._tokenList
  }

  bigbang() {
    const key = this._json[this._index]
    switch (key) {
      case ' ':
        this._index++
        return this.bigbang()
      case '{':
        this._index++
        return this.createToken(TokenTypes.OPEN_OBJECT)
      case '}':
        this._index++
        return this.createToken(TokenTypes.CLOSE_OBJECT)
      case '[':
        this._index++
        return this.createToken(TokenTypes.OPEN_ARRAY)
      case ']':
        this._index++
        return this.createToken(TokenTypes.CLOSE_ARRAY)
      case ':':
        this._index++
        return this.createToken(TokenTypes.COLON)
      case ',':
        this._index++
        return this.createToken(TokenTypes.COMMA)
      case '"':
        return this.parseString()
    }
    // number
    if (this.isNumber(key)) {
      return this.parseNumber()
    }
    // true false null
    const result = this.parseKeyword(key)
    if (result.isKeyword) {
      return this.createToken(TokenTypes[result.keyword])
    }
  }

  isNumber(key) {
    return key >= '0' && key <= '9'
  }

  parseString() {
    this._index++
    let key = ''
    while (this._index < this._json.length && this._json[this._index] !== '"') {
      key += this._json[this._index]
      this._index++
    }
    this._index++
    return this.createToken(TokenTypes.STRING, key)
  }

  parseNumber() {
    let key = ''
    while (this._index < this._json.length && '0' <= this._json[this._index] && this._json[this._index] <= '9') {
      key += this._json[this._index]
      this._index++
    }
    return this.createToken(TokenTypes.NUMBER, Number(key))
  }

  parseKeyword(key) {
    let isKeyword = false
    let keyword = ''
    switch (key) {
      case 't':
        isKeyword = this._json.slice(this._index, this._index + 4) === 'true'
        keyword = 'TRUE'
        break
      case 'f':
        isKeyword = this._json.slice(this._index, this._index + 5) === 'false'
        keyword = 'FALSE'
        break
      case 'n':
        isKeyword = this._json.slice(this._index, this._index + 4) === 'null'
        keyword = 'NULL'
        break
    }
    this._index += keyword.length
    return {
      isKeyword,
      keyword,
    }
  }
}

语法分析

file

语法分析是遍历每个 Token,寻找语法信息,并且构建一个叫做 AST(抽象语法树)的对象。在正式进行语法分析前,我们针对 JSON 的语法特征创建不同的类来记录 AST 上每个节点的信息。

class NumericLiteral {
  constructor(type, value) {
    this.type = type
    this.value = value
  }
}

class StringLiteral {
  constructor(type, value) {
    this.type = type
    this.value = value
  }
}

class BooleanLiteral {
  constructor(type, value) {
    this.type = type
    this.value = value
  }
}

class NullLiteral {
  constructor(type, value) {
    this.type = type
    this.value = value
  }
}

class ArrayExpression {
  constructor(type, elements) {
    this.type = type
    this.elements = elements || []
  }
}

class ObjectExpression {
  constructor(type, properties) {
    this.type = type
    this.properties = [] || properties
  }
}

class ObjectProperty {
  constructor(type, key, value) {
    this.type = type
    this.key = key
    this.value = value
  }
}

接下来正式进行语法分析,对 Token 进行遍历并对其类型进行检查,创建节点信息,构建一个 AST(抽象语法树)的对象。代码如下:

// 语法分析
class Parser {
  constructor(tokens) {
    this._tokens = tokens
    this._index = 0
    this.node = null
  }

  jump() {
    this._index++
  }

  getValue() {
    const value = this._tokens[this._index].value
    this._index++
    return value
  }

  parse() {
    const type = this._tokens[this._index].type
    const value = this.getValue()
    switch (type) {
      case TokenTypes.OPEN_ARRAY:
        const array = this.parseArray()
        this.jump()
        return array
      case TokenTypes.OPEN_OBJECT:
        const object = this.parseObject()
        this.jump()
        return object
      case TokenTypes.STRING:
        return new StringLiteral('StringLiteral', value)
      case TokenTypes.NUMBER:
        return new NumericLiteral('NumericLiteral', Number(value))
      case TokenTypes.TRUE:
        return new BooleanLiteral('BooleanLiteral', true)
      case TokenTypes.FALSE:
        return new BooleanLiteral('BooleanLiteral', false)
      case TokenTypes.NULL:
        return new NullLiteral('NullLiteral', null)
    }
  }

  parseArray() {
    const _array = new ArrayExpression('ArrayExpression')
    while(true) {
      const value = this.parse()
      _array.elements.push(value)
      if (this._tokens[this._index].type !== TokenTypes.COMMA) break
      this.jump() // 跳过 ,
    }
    return _array
  }

  parseObject() {
    const _object = new ObjectExpression('ObjectExpression')
    _object.properties = []
    while(true) {
      const key = this.parse()
      this.jump() // 跳过 : 
      const value = this.parse()
      const property = new ObjectProperty('ObjectProperty', key, value)
      _object.properties.push(property)
      if (this._tokens[this._index].type !== TokenTypes.COMMA) break
      this.jump() // 跳过 ,
    }
    return _object
  }
}

转换

经过语法分析后得到了 AST,转换阶段可以对树节点进行增删改等操作,转换为新的 AST 树。

代码生成

生成代码阶段,是对转换后的 AST 进行遍历,根据每个节点的语法信息转换成最终的代码。

// 代码生成
class Generate {
  constructor(tree) {
    this.tree = tree
  }

  getResult() {
    let result = this.getData(this.tree)
    return result
  }

  getData(data) {
    if (data.type === 'ArrayExpression') {
      let result = []
      data.elements.map(item => {
        let element = this.getData(item)
        result.push(element)
      })
      return result
    }
    if (data.type === 'ObjectExpression') {
      let result = {}
      data.properties.map(item => {
        let key = this.getData(item.key)
        let value = this.getData(item.value)
        result[key] = value
      })
      return result
    }
    if (data.type === 'ObjectProperty') {
      return this.getData(data)
    }
    if (data.type === 'NumericLiteral') {
      return data.value
    }
    if (data.type === 'StringLiteral') {
      return data.value
    }
    if (data.type === 'BooleanLiteral') {
      return data.value
    }
    if (data.type === 'NullLiteral') {
      return data.value
    }
  }
}

使用

function JsonParse(b) {
  const lexer = new Lexer(b)
  const tokens = lexer.getToken() // 获取Token
  const parser = new Parser(tokens)
  const tree = parser.parse() // 生成语法树
  const generate = new Generate(tree)
  const result = generate.getResult() // 生成代码
  return result
}

总结

至此我们就实现了一个简单的 JSON Parse 解析器,通过对 JSON Parse 实现的探究,我们可以总结出此类解析器的实现步骤,首先对目标值的语法进行了解,提取其特征,然后通过词法分析,与目标特征进行比对得到 token,然后对 token 进行语法分析生成 AST(抽象语法树),再对 AST 进行增删改等操作,生成新的 AST,最终对 AST 进行遍历就会生成我们需要的目标值。

参考

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

前言:

把一个链接生成一个二维码图片,这是我们前端非常常见的一个需求。那么我们应该如何做呢?

查看往期文章:

五分钟一百行代码,手写一个vue项目全局通用的toast提示组件

十五分钟两百行代码,手写一个vue项目全局通用的弹框

第一步:下载
Qrcode

npm install --save qrcode

第二步:准备容器

我们生成的二维码图片需要一个展示的容器,我们需要提前准备好。

<div id="qrCode"></div>

第三步:引入并使用

import QRCode from 'qrcode'

new QRCode(document.getElementById("qrCode"), {
    text: shareLink + "&p=qr_code&v=3", //生成二维码的文本
    width: document.querySelector("#qrCode").offsetWidth,
    height: document.querySelector("#qrCode").offsetWidth,
    colorDark: "#333333", //二维码颜色
    colorLight: "#ffffff", //二维码背景色
    correctLevel: QRCode.CorrectLevel.L //容错率,L/M/H
});

说明:

  1. 当你通过new调用之后就能生成要给二维码图片了,并且能够显示在你指定的容器当中;
  2. 因为我自己开发vue项目比较多,在vue项目中使用时,需要注意,最好放在
    nextTick
    中使用,保证容器渲染完成;
this.$nextTick(() => {
		new QRCode(document.getElementById("qrCode"), {
		text: shareLink + "&p=qr_code&v=3", //生成二维码的文本
		width: document.querySelector("#qrCode").offsetWidth,
		height: document.querySelector("#qrCode").offsetWidth,
		colorDark: "#333333", //二维码颜色
		colorLight: "#ffffff", //二维码背景色
		correctLevel: QRCode.CorrectLevel.L //容错率,L/M/H
	});
});
  1. correctLevel
    容错率说明:


    • 在二维码(QR Code)的上下文中,容错率(Correct Level)是一个非常重要的概念,它表示二维码能在多大程度上被破损或遮挡而仍然能够被成功扫描和解码。容错率的设置对于二维码的实用性在实际应用中非常关键,尤其是在可能会遭受物理损害或部分遮挡的环境中。
    • 二维码标准定义了四个容错级别,每个级别都能容忍一定比例的二维码图像损坏:
      1. L (Low) :约7% 的错误可以被纠正。
      2. M (Medium) :约15% 的错误可以被纠正。
      3. Q (Quartile) :约25% 的错误可以被纠正。
      4. H (High) :约30% 的错误可以被纠正。
    • 选择更高的容错级别会增加二维码的复杂度和大小,因为需要加入更多的冗余数据来实现错误校正。这意味着相同的数据内容,高容错率的二维码可能会比低容错率的二维码大或包含更密集的模块(黑点和白点)。
    • 如果二维码不太可能受到损害或遮挡,并且空间有限,可以选择较低的容错率(如L或M)。
    • 如果二维码可能会在较为恶劣的环境中使用,或者预计会有一部分被遮挡或破损,应选择较高的容错率(如Q或H),以确保二维码仍然可读。

写在后面

这是一个通用的
qrcode
库的通用使用流程,跟框架无关,你可以按照流程操作;

对你有帮助的话给作者点一个免费的关注吧,感恩!Peace and love~~