开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

异步选择模型 异步选择模型

发表于 2021-11-27

这是我参与11月更文挑战的第二十五天,活动详情查看:2021最后一次更文挑战

异步选择模型

前言

掌握创建基本窗口的代码,以及回调函数的概念;掌握异步选择模型的通讯过程;掌握异步选择模型的代码实现。

内容和步骤

服务器端:

实现基本窗口功能:

1、创建窗口结构体:WNDCLASSEX(这一步不能少设置属性,否则会在第三步失败)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ini复制代码WNDCLASSEX wndc;

    wndc.cbClsExtra = 0;//窗口类额外数据,不用就写0

    wndc.cbSize = sizeof(WNDCLASSEX);//窗口类大小

    wndc.cbWndExtra = 0;//窗口额外数据,不用就写0

    wndc.hbrBackground = NULL;//用默认白色

    wndc.hCursor = NULL;//默认光标

    wndc.hIcon = NULL;//默认窗口图标

    wndc.hIconSm = NULL;//默认任务栏图标

    wndc.hInstance = hInstance;//少这个就不能创建成功

    wndc.lpfnWndProc = callBackProc;//回调函数名称,要和定义的回调函数名字一样,由系统调用

    wndc.lpszClassName = "emptywnd";//窗口类名

    wndc.lpszMenuName = NULL;//窗口菜单名称

    wndc.style = CS_HREDRAW | CS_VREDRAW;//https://docs.microsoft.com/zh-cn/windows/win32/winmsg/window-class-styles

2、注册窗口结构体:RegisterClassEx

1
2
3
4
5
6
ini复制代码int regid = RegisterClassEx(&wndc);
    if (regid == 0)//如果注册失败
    {
         int RegisterClassExerr = GetLastError();

    }

3、创建窗口:CreateWindowEx

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码HWND hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, "emptywnd", "窗口标题", WS_OVERLAPPEDWINDOW, 100, 100, 640, 480, NULL, NULL, hInstance, NULL);
    if (hWnd == NULL)//如果创建失败

    {

         int CreateWindowExerr = GetLastError();

         //MessageBox(0,"注册窗口失败", "提示", MB_OK);

         return 0;

    }

4、显示窗口:ShowWindow

1
scss复制代码howWindow(hWnd, SW_NORMAL);

5、网络通信功能, SOCKET初始化操作

1
2
3
4
5
ini复制代码WORD wdVersion = MAKEWORD(2, 2);

    int a = *((char*)&wdVersion);

    int b = *((char*)&wdVersion + 1);
5.1.打开网络库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
arduino复制代码if (0 != nRes)

    {

         switch (nRes)

         {

         case WSASYSNOTREADY:

             printf("解决方案:重启。。。\n");

             break;

         case WSAVERNOTSUPPORTED:

             break;

         case WSAEINPROGRESS:

             break;

         case WSAEPROCLIM:

             break;

         case WSAEFAULT:

             break;

         }

         return 0;

 

    }
5.2.校验版本
1
2
3
4
5
6
7
8
9
10
11
scss复制代码if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))

    {

         printf("版本有问题!\n");

         WSACleanup();

         return 0;

    }
5.3.创建SOCKET
1
ini复制代码SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
5.4.绑定地址与端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
scss复制代码if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si)))

    {
         int err = WSAGetLastError();//取错误码

         printf("服务器bind失败错误码为:%d\n", err);

         closesocket(socketServer);//释放

         WSACleanup();//清理网络库
         return 0;

    }

    printf("服务器端bind成功!\n");5.5.开始监听

if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))

    {

         int err = WSAGetLastError();//取错误码

         printf("服务器监听失败错误码为:%d\n", err);

         closesocket(socketServer);//释放

         WSACleanup();//清理网络库

         return 0;

    }
    printf("服务器端监听成功!\n");
5.6.绑定消息和服务器SOCKET,并投递给操作系统
1
2
3
4
5
6
7
8
9
10
11
scss复制代码if (WSAAsyncSelect(socketServer, hWnd, UM_ASYNCSELECTMSG, FD_ACCEPT) == SOCKET_ERROR)//失败处理
    {

         int WSAAsyncSelecterr = WSAGetLastError();

         closesocket(socketServer);

         WSACleanup();

         return 0;
    }
6、消息循环:GetMessage、TranslateMessage、DispatchMessage
1
2
3
4
5
6
7
scss复制代码MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
         TranslateMessage(&msg);//将消息转换为可识别的代号
         DispatchMessage(&msg);//分发消息,让回调函数来处理

    }
7、创建回调函数

LRESULT CALLBACK callBackProc(HWND hWnd, UINT msgID, WPARAM wparam, LPARAM lparam)

7.1从wparam中获取socket句柄

SOCKET sock = (SOCKET)wparam;

7.2获取操作码,使用分支语句进行判断分别进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
scss复制代码switch (LOWORD(lparam))

         {

         case FD_ACCEPT:

         {

             //参数1:当前窗口的上下文句柄

             //参数2,3:要显示的位置坐标,左上角是0,0

             //参数4:要显示的字符串

             //参数5:参数4的长度,这里不用加后面的/0,因此可以sizeof要减一,或者直接用strlen

             TextOut(hdc, 10, y, "accept执行", sizeof("accept执行") - 1);

             y += 15;

 

             SOCKET socketClient = accept(sock, NULL, NULL);//获取客户端SOCKET句柄

             if (socketClient == INVALID_SOCKET)//出错拿错误码

             {

                  int accepterr = WSAGetLastError();

                  break;

             }

             //没错则将事件和客户端SOCKET句柄装消息队列并投递到操作系统

             if (WSAAsyncSelect(socketClient, hWnd, UM_ASYNCSELECTMSG, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)

             {

                  int WSAAsyncSelecterr = WSAGetLastError();

                  closesocket(socketClient);

             }

 

             //成功则装进SOCKET数组,便于最后释放

             garr_sockAll[gi_sockCout] = socketClient;

             gi_sockCout++;

 

             break;

         }

         case FD_READ:

         {

             TextOut(hdc, 10, y, "read执行", sizeof("read执行") - 1);

             y += 15;

 

             char str[1000] = { 0 };

 

             //接收消息

             if (recv(sock, str, 999, 0) == SOCKET_ERROR)

             {

                  int recverr = WSAGetLastError();

                  break;

             }

             //显示消息

             TextOut(hdc, 10, y, str, strlen(str));

             y += 15;

 

             break;

         }

         case FD_WRITE:

         {

             TextOut(hdc, 10, y, "wirte执行", sizeof("wirte执行") - 1);

             y += 15;

             //窗口还没地方写消息,写个死的先

             if (send(sock, "异步选择模型连接成功~", sizeof("异步选择模型连接成功~"), 0) == SOCKET_ERROR)

             {

                  int FD_WRITEsenderr = WSAGetLastError();

             }

             break;

         }

        case FD_CLOSE:

         {

             TextOut(hdc, 10, y, "close执行", sizeof("close执行") - 1);

             y += 15;

             //通过将后面两个参数设置为0,即可关闭该socket上的消息

             WSAAsyncSelect(sock, hWnd, 0, 0);

             //关闭socket

             closesocket(sock);

             //记录数组中删除该socket

             for (int i = 0; i < gi_sockCout; i++)

             {

                  if (garr_sockAll[i] == sock)

                  {

                      garr_sockAll[i] = garr_sockAll[gi_sockCout - 1];//没有顺序要求,直接用最后一个元素补位

                      gi_sockCout--;

                      break;

                  }

             }

             WSACleanup();//清理网络库

             //最后一个分支不用break

         }

    }
8、关闭SOCKET句柄
1
2
3
4
5
scss复制代码ReleaseDC(hWnd, hdc);//释放hdc

    //对未处理的消息进行默认处理

    return DefWindowProc(hWnd, msgID, wparam, lparam);

运行结果

image.png

如何理解异步、同步

同步:提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步: 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

云原生时代下的容器镜像安全(上)

发表于 2021-11-27

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。

大家好,我是张晋涛。

Kubernetes 作为云原生的基石,为我们带来了极大的便利性,越来越多的公司也都将 Kubernetes 应用到了生产环境中。然而,在享受其带来的便利性的同时,我们也需要关注其中的一些安全隐患。

本篇,我将为你重点介绍容器镜像安全相关的内容。

通常情况下,我们提到容器镜像安全,主要是指以下两个方面:

  • 镜像自身内容的安全;
  • 镜像分发过程的安全;

镜像自身内容的安全

要聊镜像自身内容的安全,那我们就需要知道镜像到底是什么,以及它其中的内容是什么。

镜像是什么

我们以 debian镜像为例,pull 最新的镜像,并将其保存为 tar 文件,之后进行解压:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码➜  ~ mkdir -p debian-image
➜ ~ docker pull debian
Using default tag: latest
latest: Pulling from library/debian
647acf3d48c2: Pull complete
Digest: sha256:e8c184b56a94db0947a9d51ec68f42ef5584442f20547fa3bd8cbd00203b2e7a
Status: Downloaded newer image for debian:latest
docker.io/library/debian:latest
➜ ~ docker image save -o debian-image/debian.tar debian
➜ ~ ls debian-image
debian.tar
➜ ~ tar -C debian-image -xf debian-image/debian.tar
➜ ~ tree -I debian.tar debian-image
debian-image
├── 827e5611389abf13dad1057e92f163b771febc0bcdb19fa2d634a7eb0641e0cc.json
├── b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── manifest.json
└── repositories

1 directory, 6 files

解压完成后,我们看到它是一堆 json 文件和 layer.tar文件的组合,我们再次对其中的 layer.tar进行解压:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码➜  ~ tar -C debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda -xf debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda/layer.tar
➜ ~ tree -I 'layer.tar|json|VERSION' -L 1 debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda
debian-image/b331057b5d32f835ac4b051f6a08af6e9beedb99ec9aba5c029105abe360bbda
├── bin
├── boot
├── dev
├── etc
├── home
├── lib
├── lib64
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var

19 directories, 0 files

解压后的目录结构想必你已经很熟悉了,是的,这是 rootfs的目录结构。

如果我们使用的是自己构建的一些应用镜像的话,经过几次解压,你也会在其中找到应用程序相对应的文件。

镜像自身内容安全如何保证

前面我们已经看到了容器镜像就是 rootfs和应用程序,以及一些配置文件的组合。所以要保证它自身内容的安全性,主要从以下几个方面来考虑:

rootfs安全

对应到我们的实际情况,rootfs通常是由我们使用的基础(系统)镜像提供的,或者也可以认为是我们构建镜像时,Dockerfile的 FROM字段所配置的镜像提供的。

在这个方面想要做到安全性就需要我们:

  • 使用可信来源的镜像,比如 Docker 官方维护的镜像;
  • 对基础镜像持续的进行漏洞扫描和升级;
  • 也可以考虑使用 Distroless镜像,这样也可以一定程度上免受攻击;

应用程序

应用程序其实是我们自己提供的,在这方面想要做到安全性,那么就需要我们:

  • 持续的进行软件的漏洞扫描;
  • 对依赖及时的进行更新;
  • 可以考虑从 SDL(Security Development Lifecycle)过渡到 DevSecOps ;

配置文件

镜像中所包含的那些配置文件是由镜像构建工具所提供的,一般情况下,只要我们保证使用的镜像构建工具未被篡改或者留下什么漏洞,那么这里基本上不会有什么大的问题。

综合来看,我们可以直接使用类似 Trivy 或者 Anchore Engine 等镜像漏洞扫描工具来帮助我们保障镜像内容的安全。此外,一些镜像仓库,比如 Harbor 等都已经内置了镜像安全的扫描工具,或者可以使用 docker scan命令进行镜像的安全扫描。

镜像分发安全

镜像如何分发

我们首先来看看,容器镜像是怎么样从构建到部署到我们的 Kubernetes 环境中的。

img

图 1 ,容器镜像自创建到发布部署的简要过程示意图

开发者在编写完代码后,推送代码到代码仓库。由此来触发 CI 进行构建,在此过程中会进行镜像的构建,以及将镜像推送至镜像仓库中。

在 CD 的环节中,则会使用镜像仓库中的镜像,部署至目标 Kubernetes 集群中。

那么在此过程中,攻击者如何进行攻击呢?

镜像分发中的安全问题

img

图 2 ,镜像分发部署安全示例

如图,在镜像分发部署的环节中其上游是镜像仓库,下游是 Kubernetes 集群。对于镜像仓库而言,即使是内网的自建环境,由于我们的观念已从基于边界的安全转变为零信任安全,所以,我们统一以公共仓库为例来讲解。

攻击者可以通过一些手段进行劫持、替换成恶意的镜像,包括直接攻击镜像仓库等。

要保证部署到 Kubernetes 集群中镜像的安全性来源以及完整性,其实是需要在两个主要的环节上进行:

  • 构建镜像时进行镜像的签名;
  • 镜像分发部署时进行签名的校验;(下一篇内容继续)

我们来分别看一下。

镜像的标签和摘要

我们通常在使用容器镜像时有两种选择:

  • 标签,比如 alpine:3.14.3
  • 摘要,比如 alpine@sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2

大多数场景下,我们会直接使用标签,因为它的可读性更好。但是镜像内容可能会随着时间的推移而变化,因为我们可能会为不同内容的镜像使用相同的标签,最常见的就是 :latest标签,每次新版本发布的时候,新版本的镜像都会继续沿用 :latest标签,但其中的应用程序版本已经升级到了最新。

使用摘要的主要弊端是它的可读性不好,但是,每个镜像的摘要都是唯一的,摘要是镜像内容的 SHA256 的哈希值。所以我们可以通过摘要来保证镜像的唯一性。

通过以下示例可以直接看到标签和摘要信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plain复制代码➜  ~ docker pull alpine:3.14.3                                                                          
3.14.3: Pulling from library/alpine
Digest: sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2
Status: Image is up to date for alpine:3.14.3
docker.io/library/alpine:3.14.3
➜ ~ docker image inspect alpine:3.14.3 | jq -r '.[] | {RepoTags: .RepoTags, RepoDigests: .RepoDigests}'
{
"RepoTags": [
"alpine:3.14.3"
],
"RepoDigests": [
"alpine@sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2"
]
}

那么如何来保证镜像的正确性/安全性呢?这就是镜像签名解决的主要问题了。

镜像签名解决方案

数字签名是一种众所周知的方法,用于维护在网络上传输的任何数据的完整性。对于容器镜像签名,我们有几种比较通用的方案。

Docker Content Trust (DCT)

在传输一般文件时,可能有过类似的经历,比如因为网络原因导致下载的文件不完整;或是遭遇中间人的攻击导致文件被篡改、替换等。

镜像在分发过程中其实也可能会遇到类似的问题,这就是此处我们要讨论的重点,也就是 Docker Content Trust(DCT)主要解决的问题。

Docker Content Trust 使用数字签名,并且允许客户端或运行时验证特定镜像标签的完整性和发布者。对于使用而言也就是 docker trust 命令所提供的相关功能。注意:这需要 Docker CE 17.12 及以上版本。

前面我们提到了,镜像记录可以有一些标签,格式如下:

1
plain复制代码[REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]

以标签为例,DCT 会与标签的一部分相关联。每个镜像仓库都有一组密钥,镜像发布者使用这些密钥对镜像标签进行签名。(镜像发布者可以自行决定要签署哪些标签)镜像仓库可以同时包含多个带有已签名标签和未签名标签的镜像。

这里需要说明下,如果镜像发布者先推送签名的 latest 镜像,再推送未签名的 latest 镜像,那么后一个镜像不会影响前一个镜像的内容(区别于上文中标签覆盖的地方)。

img

图 4 ,DCT 镜像签名示例(图中简略了登录镜像仓库的认证过程)

在生产中,我们可以启用 DCT 确保使用的镜像都已签名。如果启用了 DCT,那么只能对受信任的镜像(已签名并可验证的镜像)进行拉取、运行或构建。

启用 DCT 有点像对镜像仓库应用“过滤器”,即,只能看到已签名的镜像标签,看不到未签名的镜像标签。如果客户端没有启用 DCT ,那么它可以看到所有的镜像。

这里我们来快速的看一下 DCT 的工作过程

它对镜像标签的信任是通过使用签名密钥来管理的。在我们首次开启 DCT 并使用的时候会创建密钥集。一个密钥集由以下几类密钥组成:

  • 一个离线密钥 offline key ,它是镜像标签 DCT 的根 (丢失根密钥很难恢复)
  • 对标签进行签名的存储库或标记密钥 tag key
  • 服务器管理的密钥,例如时间戳密钥

img

图 5 , 镜像签名密钥示例

刚从我们提到客户端使用 DCT 也就是我们的 docker trust命令,它是建立在 Notary v1 上的。默认情况下,Docker 客户端中禁用 DCT 。要启用需要设置 DOCKER_CONTENT_TRUST=1 环境变量 。

效果如下:

1
2
3
4
5
6
7
8
plain复制代码➜  ~ DOCKER_CONTENT_TRUST=1 docker pull alpine:3.12
Pull (1 of 1): alpine:3.12@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a
docker.io/library/alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a: Pulling from library/alpine
188c0c94c7c5: Already exists
Digest: sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a
Status: Downloaded newer image for alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a
Tagging alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a as alpine:3.12
docker.io/library/alpine:3.12

Notary v1

前面我们提到 DCT 是基于 Notary v1 实现的,不过这不是我们本篇的重点,所以这里仅对 Notary v1 做个简单的介绍。Notary 项目地址: github.com/notaryproje…

img

图 6 ,Notary 客户端、服务器和签名相关的交互流程

过程1 - 身份认证,任何没有令牌的连接将被重定向到授权服务器(Docker Registry v2 身份认证);

过程2 - 客户端将通过 HTTPS 上的身份验证登录到授权服务器,获取令牌;

过程3 - 当客户端上传新的元数据文件时,服务器会根据以前的版本检查它们是否存在冲突,并验证上传的元数据的签名、校验和和有效性;

过程4 - 一旦所有上传的元数据都经过验证,服务器会生成时间戳(可能还有快照),然后将它们发送给 sign 进行签名;

过程5 - sign 从其数据库中检索加密私钥,解密密钥,并使用它们进行签名,并发送回服务器;

过程6 - 服务器将客户端上传和服务器生成的元数据存储在 TUF 库中。生成的时间戳和快照元数据证明客户端上传的元数据是该可信集合的最新版本。之后,服务器会通知客户端–上传成功;

过程7 - 客户端现在可以立即从服务器下载最新的元数据了。在时间戳过期的情况下,服务器将遍历整个序列,生成新的时间戳,请求 sign 签名,将新签名的时间戳存储在数据库中。然后,它将这个新的时间戳连同其他存储的元数据一起发送给请求客户端;

这个项目由于是一个安全项目,虽然用途很大,但整体并不活跃。现在正在进行 v2 版本的开发,有兴趣的小伙伴欢迎加入。

sigstore 和 Cosign

这里介绍另一个来自 Linux 基金会的项目,叫做 sigstore 它主要是为了提供一些标准的库/工具,便于更好的进行签名和校验。当然,目前 sigstore 已经汇聚了包括 Cosign,Fulcio 和 Rekor 等开源项目,涉及到镜像镜像签名校验和供应链等方面。

img

图 7 ,sigstore 简介

Cosign 是 sigstore 的工具之一,用于 OCI registry 中创建、存储和验证容器镜像签名。Cosign v1.0 已于今年下半年发布,是否能稳定用于生产环境,还有待考验。 截止目前,Cosign 已经发布了 v1.3.1 版本,详细变更请参考其 ReleaseNote: github.com/sigstore/co…

我们这里看下它如何进行镜像的签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
plain复制代码➜  cosign cosign generate-key-pair                                                                                              
Enter password for private key:
Enter password for private key again:
Private key written to cosign.key
Public key written to cosign.pub
➜ cosign cosign sign --key cosign.key ghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo:fa5714f419b3d11dee6ac795e38356e9c3c439cb
Enter password for private key: %
➜ cosign cosign verify --key cosign.pub ghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo:fa5714f419b3d11dee6ac795e38356e9c3c439cb

Verification for ghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo:fa5714f419b3d11dee6ac795e38356e9c3c439cb --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
- Any certificates were verified against the Fulcio roots.

[{"critical":{"identity":{"docker-reference":"ghcr.io/tao12345666333/argo-cd-demo/argo-cd-demo"},"image":{"docker-manifest-digest":"sha256:768845efa2a32bc5c5d83a6f7ec668b98f5db46585dd1918afc9695a9e653d2d"},"type":"cosign container image signature"},"optional":null}]

看起来还是比较简单的。

总结

以上就是关于镜像自身内容安全,以及镜像分发安全中的镜像签名校验部分的内容。

下一篇我将为大家介绍如何在镜像分发及部署时进行签名的校验,以及如何保护 Kubernetes 集群免受未签名或不可信来源镜像的攻击,敬请期待!


欢迎订阅我的文章公众号【MoeLove】

TheMoeLove

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java集合Comparable和comparator接口的

发表于 2021-11-27

最近在学习集合排序问题,对于Comparable接口的compareTo()方法,什么样的表达式表示的是升序、降序呢?

我们用一道题来进行说明。

1
dart复制代码分别用Comparable和Comparator两个接口对下列四位同学的成绩做降序排序,如果成绩一样,那在成绩排序的基础上按照年龄由小到大排序。分别用Comparable和Comparator两个接口对下列四位同学的成绩做降序排序,如果成绩一样,那在成绩排序的基础上按照年龄由小到大排序。

那么Comparable和Comparator两个接口有什么区别呢?

Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进

行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

一、如果我们用Comparable接口实现排序,而排序的内容要自定义时则需要重写compareTo()方法,举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码​
public class compare {
  public static void main(String[] args) {
      List<Student> list = new ArrayList<Student>();
      list.add(new Student("贾宝玉",14,88.5));
      list.add(new Student("林黛玉",13,90.5));
      list.add(new Student("史湘云",13,85));
      list.add(new Student("薛宝钗",15,91));
      System.out.println("以前的顺序: ");
      for (Student s:list) {
          System.out.println(s);
      }
      System.out.println("现在的顺序: ");
      Collections.sort(list);
      for (Student s:list) {
          System.out.println(s);
      }
  }
}

image.png

在这里我这样重写了compareTo()方法:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码@Override
public int compareTo(Student o) {
  if (o.getScore() == this.getScore()){
      //按年龄升序
      return (this.getAge() < o.getAge()) ? -1 : (this.getAge() == o.getAge()) ? 0 : 1;
  }else {
      //按成绩降序
      return (this.getScore() < o.getScore()) ? 1 : (this.getScore() == o.getScore()) ? 0 : -1;
  }
}

这样写是模仿了Integer里的compareTo方法的源码

1
2
3
arduino复制代码public static int compare(int x, int y) {
  return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

首先明白,若compareTo方法 return 0;则集合顺序不变;

若 return 1;集合顺序正序输出;

若 return -1;集合顺序倒序输出;

1
2
3
4
5
6
7
ini复制代码(x<y)?-1 : (x==y)? 0: 1;
x是自己,y是另一个值。
若x<y,则rturn -1;
​
(x<y)? 1 : (x==y)? 0: -1;
x是自己,y是另一个值。
若x<y,则rturn 1;

这样写的话,年龄从小到大升序排列,成绩就按降序排列。

二、用比较器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码Collections.sort(list, new Comparator<Student>() {
  //comparator比较器,可以根据需要定制特定的比较规则
  @Override
  public int compare(Student o1, Student o2) {
      if (o1.getScore() == o2.getScore()){
          //按年龄升序
          return (o2.getAge() < o1.getAge()) ? 1 : (o2.getAge() == o1.getAge()) ? 0 : -1;
      }else {
          //按成绩降序
          return (o2.getScore() < o1.getScore()) ? -1 : (o2.getScore() == o1.getScore()) ? 0 : 1;
      }
  }
});
for (Student s:list) {
  System.out.println(s);
}

核心原理相同。

希望对你有帮助~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Excel神办公—【二】使用EasyExce读取Excel数

发表于 2021-11-27

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

hello,你好呀,我是灰小猿,一个超会写bug的程序猿!

在上一篇文章中我和大家简单的介绍了使用easyexcel技术在有对象和无对象情况下实现Excel文件的写入操作,

那么今天这一篇文章,我就继续来和大家讲一下,使用easyexcel技术如何读取excel中的数据呢?

easyexcel的优势

在Java领域解析、生成Excel比较有名的框架有Apache poi,jxl等,但是在使用的时候,其实他们都存在一个严重的问题,就是非常的耗内存,如果你的系统并发量不大的话,可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的垃圾回收.

而EasyExcel是阿里巴巴开源的一个excel处理框架,他具有使用简单,节省内存的特点,EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析的,这一特点待会在读取excel数据的时候也会体现出来。

添加easyexcel依赖

使用easyexcel时需要在pom中导入相应的依赖文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码        <!--poi依赖03版本-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.17</version>
        </dependency>
        <!--poi依赖07版本-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.17</version>
        </dependency>
        <!--easyexcel依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.6</version>
        </dependency>

由于easyexcel是在poi的基础上发展起来的,所以在导入依赖的时候,也需要导入对03和07版本的excel的依赖。

使用easyexcel读取数据

在使用easyexcel读取文件数据的时候,需要设置一个监听器,通过实现该监听器,就可以实现数据的单行读取操作, 我们以下面的这个数据对象为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码

/**
 * 基本数据demodata
 */
@Data
public class DemoData {
    @ExcelProperty(value = "字符串标题")
    private String stringTitle;
    @ExcelProperty(value = "时间标题")
    private Date dateTitle;
    @ColumnWidth(50)
    @ExcelProperty(value = "数字标题")
    private int doubleTitle;
}

读取的数据内容如下:

监听器的实现

在读取excel数据的时候,需要实现AnalysisEventListener监听器,其中需要传入对应的数据类型,在该监听接口中,主要使用的方法是:\

  • invoke:一行一行读取,每读取一行数据时就会调用该方法。
  • invokeHeadMap:读取表头数据,
  • doAfterAllAnalysed:所有数据读取完毕之后调用,一般用于对最后读取到的数据进行处理。
  • onException:在转换异常,获取其他异常的情况下会调用此接口,抛出异常就停止读取,如果不抛出异常就继续读取

接口的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
java复制代码
/**
 * 读取excel,设置监听器
 */
@Slf4j
public class DemoDataListener extends AnalysisEventListener<DemoData> {

    /**
     * 定义一个存储的界限,每读取5条数据就存储一次数据库,防止数据过多时发生溢出
     * 存储完成之后就清空list重新读取新的数据,方便内存回收
     */
    private static final int BATCH_COUNT = 5;

    /**
     * 定义一个数据存储缓存,用于临时存储读取到的数据
     */
    private List<DemoData> cacheDataList = new ArrayList<>();


    //    对接持久层的dao
    private DemoDao demoDao;

    /**
     * 无参构造
     */
    public DemoDataListener() {
//        由于是示例,所以这里直接new一个dao
        this.demoDao = new DemoDao();
    }

    /**
     * 有参构造,如果无法直接Autowired,那么需要在有参构造中传入
     *
     * @param demoDao
     */
    public DemoDataListener(DemoDao demoDao) {
        this.demoDao = demoDao;
    }

    /**
     * 一行一行读取
     *
     * @param demoData
     * @param analysisContext
     */
    @Override
    public void invoke(DemoData demoData, AnalysisContext analysisContext) {
        log.info("读取到数据:" + demoData);
        cacheDataList.add(demoData);
        /**
         * 如果当前缓存列表中的数据等于指定值,就存储
         */
        if (cacheDataList.size() == BATCH_COUNT) {
            //保存数据到数据库
            saveData();
            //清空缓存列表重新读取
            cacheDataList.clear();
        }

    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        log.info("表头:" + headMap);
    }

    /**
     * 读取结束后
     *
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("读取结束~~~");
//        将最后的数据存储到数据库
        saveData();
        cacheDataList.clear();
    }

    /**
     * 在转换异常,获取其他异常的情况下会调用此接口,
     * 抛出异常就停止读取,如果不抛出异常就继续读取。
     * @param exception
     * @param context
     * @throws Exception
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        log.info("解析失败,但是继续解析下一行:{}",exception.getMessage());

        if (exception instanceof ExcelDataConvertException){
            ExcelDataConvertException e = (ExcelDataConvertException) exception;
            log.info("数据解析异常,所在行:{},所在列:{},数据是:{}",e.getRowIndex(),e.getColumnIndex(),e.getCellData());
        }
    }

    /**
     * 将数据存储到持久层
     */
    private void saveData() {
        log.info("将要保存【" + cacheDataList.size() + "】条数据入库");
        demoDao.saveDataforList(cacheDataList);
        log.info("【" + cacheDataList.size() + "】条数据入库成功!");
    }


}

接口实现完毕之后,进行数据读取的方法其实是非常简单,只需要一句代码就可以了,

读取如下:

1
2
3
4
5
6
7
8
9
csharp复制代码    /**
     * 从excel中读取全部工作表的数据
     */
    public void readAllSheetDataForExcel() {
        /**
         * 主要是doReadAll()方法
         */
        EasyExcel.read(FILEPATH + "testExcel_1.xlsx", DemoData.class, new DemoDataListener()).sheet().doRead();
    }

读取结果如下:

以上就是使用easyexcel技术读取数据的操作, 之后会继续和大家分享读取和写入复杂数据。

觉得不错,点赞关注小猿呀!

我是灰小猿,我们下期见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入浅出SynchronousQueue队列(二) 前言 内

发表于 2021-11-27

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

前言

本文继续讲解SynchronousQueue队列的公平策略下的内部实现,不废话,直接看源码。

内部实现类

公平策略下的实现类TransferQueue。

TransferQueue

基于Transferer实现公平策略下的实现类TransferQueue,公平策略需要先进先出,这里queue也表明其结构特点,内部通过QNode类实现链表的队列形态,通过CAS操作更新链表元素。

有两种状态需要注意:

取消操作(被外部中断或者超时):item == this;

出队操作(已成功匹配,找到互补操作):next == this;

构造方法

头尾节点初始化操作

1
2
3
4
5
6
ini复制代码        TransferQueue() {
// 初始化一个值为null的QNode,初始化头尾节点
QNode h = new QNode(null, false);
head = h;
tail = h;
}

QNode

QNode即为队列的链表实现,其中的变量属性isData保存的是每次的操作动作而不仅仅是入队的值,入队操作以QNode保存,出队操作也是如此,变量则通过CAS操作更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
typescript复制代码        static final class QNode {
// next指向链表下一个节点
volatile QNode next;
// 队列元素的值
volatile Object item;
// 保存等待的线程
volatile Thread waiter;
// 是否有数据,队列元素的类型标识,入队时有数据值为true,出队时无数据值为false
final boolean isData;

QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
// cas操作更新next
boolean casNext(QNode cmp, QNode val) {
return next == cmp &&
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// cas操作更新item
boolean casItem(Object cmp, Object val) {
return item == cmp &&
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}

// cas操作取消,把此时的QNode的item赋值为当前的QNode
void tryCancel(Object cmp) {
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}

// 判断是否取消成功,然后tryCancel操作后进行判断
boolean isCancelled() {
return item == this;
}

// 判断当前节点是否已处于离队状态,这里看到是将节点next指向自己
boolean isOffList() {
return next == this;
}

// 获取item和next的偏移量,操作CAS使用
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;

static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = QNode.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}

变量部分

队头队尾元素引用设置,需要注意的是cleanMe节点的含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码        // 队头
transient volatile QNode head;
// 队尾
transient volatile QNode tail;

// 标记节点,清理链表尾部节点时,不直接删除尾部节点,而是将尾节点的前驱节点next指向设置为cleanMe
// 防止此时向尾部插入节点的线程失败导致出现数据问题
transient volatile QNode cleanMe;

// 偏移量获取
private static final sun.misc.Unsafe UNSAFE;
private static final long headOffset;
private static final long tailOffset;
private static final long cleanMeOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = TransferQueue.class;
headOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("head"));
tailOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("tail"));
cleanMeOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("cleanMe"));
} catch (Exception e) {
throw new Error(e);
}
}

CAS操作

CAS更新变量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码        // 尝试将nh更新为新的队头
void advanceHead(QNode h, QNode nh) {
if (h == head &&
UNSAFE.compareAndSwapObject(this, headOffset, h, nh))
// 原头节点next指向更新为自己,使得h为离队状态,isOffList方法为true
h.next = h; // forget old next
}

// 尝试更新队尾节点
void advanceTail(QNode t, QNode nt) {
if (tail == t)
UNSAFE.compareAndSwapObject(this, tailOffset, t, nt);
}

// 尝试更新cleanMe节点
boolean casCleanMe(QNode cmp, QNode val) {
return cleanMe == cmp &&
UNSAFE.compareAndSwapObject(this, cleanMeOffset, cmp, val);
}

transfer

入队和出队操作,都是用一个方法,即实现接口中的transfer方法来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
scss复制代码    @SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {

QNode s = null; // constructed/reused as needed
// e为null时相当于出队操作isData为false,入队操作为true
boolean isData = (e != null);

for (;;) {
// 获取最新的尾节点和头节点
QNode t = tail;
QNode h = head;
// 头,尾节点为空,未初始化,则循环spin
if (t == null || h == null)
continue;
// 首尾节点相同则为空队列或尾节点类型和新操作的类型相同,都是入队操作或出队操作
// 一入队和一出队直接进入else匹配上不会再保存在链表中
if (h == t || t.isData == isData) {
QNode tn = t.next;
// 尾节点已经被其他线程更新修改,则重新循环判断
if (t != tail)
continue;
// 如果tn不为空,说明其他线程已经添加了节点,尝试更新尾节点,重新循环判断
if (tn != null) {
advanceTail(t, tn);
continue;
}
// 设置超时时间并且超时时间小于等于0则直接返回null
if (timed && nanos <= 0)
return null;
// s为null则初始化节点s
if (s == null)
s = new QNode(e, isData);
// 尝试将s添加到尾节点的next上,失败则重新循环
if (!t.casNext(null, s))
continue;
// 尝试更新尾节点,尾节点此时为s
advanceTail(t, s);
// 通过awaitFulfill方法自旋阻塞找到匹配操作的节点item
Object x = awaitFulfill(s, e, timed, nanos);
// 表示当前线程已经中断或者超时,在awaitFulfill超时或者中断时更新s.item指向自己
if (x == s) {
// 清理节点,取消本次操作
clean(t, s);
return null;
}
// 判断s是否已从队列移除
if (!s.isOffList()) {
// 未被从队列清除则尝试更新队头
advanceHead(t, s);
// 当前线程为出队操作时,s节点取消操作
if (x != null)
s.item = s;
// 清除等待线程
s.waiter = null;
}
return (x != null) ? (E)x : e;

// 与上次队列操作非同一类型操作
} else {
QNode m = h.next;
// 头节点或尾节点被其他线程更新或者为空队列则循环操作
if (t != tail || m == null || h != head)
continue;
// 头节点的下一个节点对应的item
Object x = m.item;
// 同类型,被取消操作或更新item失败则更新头节点指向重新操作
if (isData == (x != null) || // m already fulfilled 相同类型操作说明m已经被其他线程操作匹配
x == m || // m cancelled 取消操作标识
// CAS更新item为匹配上的操作值,比如当前是出队操作,m为入队操作x为入队的值,那么此时要替换为出队值null
// CAS操作失败
!m.casItem(x, e)) {
// 删除匹配上的头节点,更新头节点
advanceHead(h, m);
continue;
}
// 更新头节点
advanceHead(h, m);
// 释放m的等待线程锁使得m操作结束
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}

阻塞等待唤醒(awaitFulfill)

在transfer相同类型操作时被调用,正常情况下(不算超时和中断)阻塞线程直到与之匹配的操作到来再继续执行。例如此时是入队操作,上次也是入队操作,在未设置超时时,这里可能需要自旋或阻塞等待一个出队操作来唤醒本次入队操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
scss复制代码        // 自旋或阻塞直到超时或被唤醒匹配上节点
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
// 获取超时时间点
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 当前线程
Thread w = Thread.currentThread();
// 仅在head.next==s时才使用spins(自旋次数),同时判断是否设置了超时
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
// 判断当前线程是否中断,外部中断操作
if (w.isInterrupted())
// 尝试将s节点的item设置为s自己
s.tryCancel(e);
Object x = s.item;
// s的item已经改变,直接返回x
// 没改变的情况下即没有匹配的操作,有匹配上的item即x将被改变,取消时如上也会改变
if (x != e)
return x;
// 线程超时将s节点的item设置为s自己
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
// 需要自旋时循环
if (spins > 0)
--spins;
// 设置s的等待线程
else if (s.waiter == null)
s.waiter = w;
// 未设置超时,直接阻塞
else if (!timed)
LockSupport.park(this);
// 设置超时时间阻塞
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}

clean方法

整体处理过程如下:

如果删除的不是尾节点,则用pred.casNext(s, s.next) 方式来进行清理;

如果删除的是队尾节点,若cleanMe为空,则将其前继节点pred更新为cleanMe, 为下次删除做准备;

如果cleanMe不为空,则根据cleanMe删除上次需要删除的节点, 然后将cleanMe置空, 如果此次pred非之前cleanMe,则cleanMe置为pred,为下一次删除操作做准备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
ini复制代码    // 中断取消操作将pred节点代替s节点,修改前后节点之间的关联
void clean(QNode pred, QNode s) {
// 清理前先将等待线程置空
s.waiter = null;
// pred与s的前后关系
while (pred.next == s) {
QNode h = head;
QNode hn = h.next;
// hn非空且被取消操作,更新头节点为hn
if (hn != null && hn.isCancelled()) {
advanceHead(h, hn);
continue;
}
// 尾节点
QNode t = tail;
// 空队列返回
if (t == h)
return;
// 尾节点下一个
QNode tn = t.next;
// 尾节点已被其他线程更新
if (t != tail)
continue;
// 非空 更新尾节点
if (tn != null) {
advanceTail(t, tn);
continue;
}
// s不是尾节点
if (s != t) {
// s的下一个节点
QNode sn = s.next;
// 更新pred节点后一个节点为s的下一个节点,相当于删除s在链表中的关系
if (sn == s || pred.casNext(s, sn))
return;
}
// 执行到这里说明s为尾节点则需要处理cleanMe节点
QNode dp = cleanMe;
if (dp != null) {
QNode d = dp.next;
QNode dn;
if (d == null || // 清除节点为null,相当于已经清理了
d == dp || // dp节点处于离队状态
!d.isCancelled() || // 清除节点被取消
(d != t && // 清除节点非尾节点
(dn = d.next) != null && // 清除节点下一节点非null
dn != d && // 清除节点下一节点在队列中
dp.casNext(d, dn))) // 清理d与其他节点的关系
casCleanMe(dp, null); // 清理完毕设置为null
// 相当于s为需要清理的节点,上边已经清理过了,不需要再次清理
if (dp == pred)
return;
// 更新cleanMe为pred,为下次清理准备
} else if (casCleanMe(null, pred))
return;
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

利用SOS扩展库进入高阶NET6程序的调试 1SOS 扩

发表于 2021-11-27

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。

  • 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
  • 📢本文作者:由webmote 原创,首发于 【掘金】
  • 📢作者格言: 生活在于折腾,当你不折腾生活时,生活就开始折腾你,让我们一起加油!💪💪💪

有时候我们可能想深入到程序的运行核心,去观察下内存分配情况以及堆栈内保存的东东,那么作为编程新贵的底层框架.NET6,又为我们提供了什么可用的观测工具呢?

1.SOS 扩展是什么?

SOS扩展库是Windows 附带的调试扩展库,它允许开发人员在 WinDbg、CDB 或 NTSD 中调试托管代码,请记住,当您执行托管二进制文件时,运行时会生成特定于平台的本机代码,而SOS 扩展允许您以“托管方式”调试本机代码。

最最主要的是,SOS 调试扩展允许您查看有关在** .NET Core 运行时**运行的代码的信息,包括实时进程和转储文件。

  1. SOS支持跨平台吗?

是的,sos已经有mac、liunx和windows上的各个版本,它们支持不同的内核核心,可以用到不同的平台上进行扩展调试。

3.如何开始使用SOS?

sos扩展必须附加到别的调试工具上,因此其并不能独立运行。

为了调试.net 6程序,我们分别在不同的平台使用不同的工具进行演示。

3.1 Linux平台上使用LLDB工具

第一步是安装调试器LLDB。LLDB 与 WinDbg 非常相似,也是 SOS 团队一直在使用的调试器,因此我采用它作为 Linux 的默认调试器。

您可以从以下链接安装 LLDB:

lldb.llvm.org/download.ht…

或者您也可以通过运行以下命令来安装它:

yum install lldb

在 Linux 机器上安装 lldb 后,打开终端 并通过在提示符下键入以下命令来启动调试器:

lldb

如果 lldb 已正确启动,您将获得如下所示的 lldb 提示。

3.2 Linux 平台安装sos

没有sos扩展的加持,你是无法调试.NET程序的,因此还需要安装sos扩展。目前dotnet提供了简易安装方式,我们只需要录入下列命令即可。

1
2
shell复制代码dotnet tool install --global dotnet-sos
dotnet-sos install

默认安装的sos是和你cpu架构一致的版本,如果你需要其他版本,可以指定参数进行安装。
参数有下列值可用。

  • Arm
  • Arm64
  • X86
  • X64

例如:

1
shell复制代码dotnet-sos install --architecture Arm

在Linux系统中安装完sos后,再次启动LLDB,会默认加载sos扩展的。

3.3 利用LLDB调试程序

先启动.net 程序,然后利用ps查找进程号。

1
perl复制代码ps -ef | grep dotnet

然后启动lldb

1
shell复制代码lldb

在lldb命令界面内键入附加进程命令:

1
shell复制代码process attach -p 31339

附加dotnet程序进程到分析空间。

Process 31339 stopped

Executable module set to “/tmp/dotnet/bin/Debug/net5.0/dotnet”.

Architecture set to: x86_64–linux-gnu.

一旦附加到 lldb后,就可以显示线程列表。

您可以运行使用bt命令来检索当前线程的调用堆栈,但是很难调试它,因为您无法以“托管方式”查看堆栈.

为了测试,让我们运行clrstack命令,现在我们可以更好地了解正在发生的事情。

cid:image010.jpg@01D2AC97.6B53D120

我们还可以运行其他 SOS 命令(如clrThreads) 来找出所有管理线程,为此我们输入:

sos clrThreads

cid:image011.jpg@01D2AC97.6B53D120

如果您想自己尝试其他 SOS 命令,它们会列在 . NET 框架文档

希望这对你有用!

3.4 Windows上调试的支持

还可以通过将 SOS 调试扩展加载到 WinDbg/dbg 调试程序中并在 Windows 调试程序中执行命令来使用此扩展。 可对实时进程或转储使用 SOS 命令。

欢迎尝试Windbg 预览版【微软商店】,千年不变的界面终于焕然一新。

image.png
安装sos依然是这些命令:

1
2
shell复制代码dotnet tool install --global dotnet-sos 
dotnet-sos install

安装后,可以在windbg内加载它

1
shell复制代码. Load %userprofile%\.Dotnet\SOS\sos.dll

然后在任务管理器中找到这个进程,保存dump文件到临时目录,利用windbg打开,并加载sos.dll.

1
shell复制代码!clrstack -a

image.png

当然你可以根据需要键入不同的调试命令进行跟踪分析。

1
2
3
4
diff复制代码!runaway
!threadpool
!continue
!syncblk
  1. 小结

高阶调试是不是把你学fei了?恩,学这个跟下篇文章有些瓜葛,因此不得不先介绍下调试器的使用。

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构—线索二叉树的原理以及Java实现案例 1 线索二叉

发表于 2021-11-27

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」。

本文介绍了线索二叉树的概念,以及线索二叉树的Java的实现。如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。

1 线索二叉树的概述

如果对二叉树的概念不是很了解,可以先看这篇文章:二叉树的入门以及Java实现案例详解。二叉树的概念在此不做赘述。

对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个引用域,所以一共是2n个引用域。而n个结点的二叉树一共有n-1条分支线数,也就是说,其实是存在2n-(n-1)=n+1个空引用域。这些空间不存储任何事物,白白的浪费着内存的资源。

如下图:

在这里插入图片描述

二叉树的遍历本质上是将一个复杂的非线性结构转换为线性结构,使每个结点都有了唯一前驱和后继(第一个结点无前驱,最后一个结点无后继)。对于二叉树的一个结点,查找其左右子女是方便的,其前驱后继只有在遍历中得到。为了容易找到前驱和后继,有两种方法。一是在结点结构中增加向前和向后的引用,这种方法增加了存储开销,不可取;二是利用二叉树的空链引用。

我们可以考虑利用那些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址,空left存放前驱,空right存放后继。我们把这种指向前驱和后继的引用称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。

对二叉树以某种遍历方式(如先序、中序、后序或层序)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。 根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

采用中序遍历线索化之后的数据结构如下:

在这里插入图片描述

可以看到,它充分利用了空引用域的空间(这等于节省了空间),又保证了创建时的一次遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。所以在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。

2 节点设计

线索二叉树中的线索能记录每个结点前驱和后继信息。为了区别线索引用和孩子引用,在每个结点中设置两个标志ltag和rtag。

当tag和rtag为0时,left和right分别是指向左孩子和右孩子的指针;否则,left是指向结点前驱的线索(pre),right是指向结点的后继线索(suc)。由于标志只占用一个二进位,每个结点所需要的存储空间节省很多。

现将二叉树的结点结构重新定义如下:

left ltag data rtag right

其中:ltag=0 时left指向左儿子;ltag=1 时left指向前驱;rtag=0 时right指向右儿子;rtag=1 时right指向后继。

3 中序线索二叉树的构建

现提供中序线索二叉树的构建的完整Java代码,先序、后续线索二叉树的构建和中序的构建基本一致,都是在先、中、后序遍历的方法的基础上改进而来的,如果对关于二叉树的4种遍历方式不了解的,可以看这篇文章:二叉树的4种遍历方式详解以及Java代码的完整演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
java复制代码public class ThreadedBinaryTree<E> {

/**
* 外部保存根节点的引用
*/
private BinaryTreeNode<E> root;

/**
* 线索化的时候保存刚刚访问过的前驱节点
*/
private BinaryTreeNode<E> pre;

/**
* 树节点的数量
*/
private int size;


/**
* 内部节点对象
*
* @param <E> 数据类型
*/
public static class BinaryTreeNode<E> {

//数据域
E data;
//左子节点/前驱
BinaryTreeNode<E> left;
//右子节点/后继
BinaryTreeNode<E> right;
boolean ltag; //false:指向左子节点、true:前驱线索
boolean rtag; //false:指向右子节点、true:后继线索


public BinaryTreeNode(E data) {
this.data = data;
}

@Override
public String toString() {
return data.toString();
}
}


/**
* 空构造器
*/
public ThreadedBinaryTree() {
}

/**
* 构造器,初始化root节点
*
* @param root 根节点数据
*/
public ThreadedBinaryTree(E root) {
checkNullData(root);
this.root = new BinaryTreeNode<>(root);
size++;
}

/**
* 添加子节点
*
* @param parent 父节点的引用
* @param data 节点数据
* @param left 是否是左子节点,true 是;false 否
*/
public BinaryTreeNode<E> addChild(BinaryTreeNode<E> parent, E data, boolean left) {
checkNullParent(parent);
checkNullData(data);
BinaryTreeNode<E> node = new BinaryTreeNode<>(data);
if (left) {
if (parent.left != null) {
throw new IllegalStateException("该父节点已经存在左子节点,添加失败");
}
parent.left = node;
} else {
if (parent.right != null) {
throw new IllegalStateException("该父节点已经存在右子节点,添加失败");
}
parent.right = node;
}
size++;
return node;
}

/**
* 是否是空树
*
* @return true 是 ;false 否
*/
public boolean isEmpty() {
return size == 0;
}


/**
* 返回节点数
*
* @return 节点数
*/
public int size() {
return size;
}

/**
* 获取根节点
*
* @return 根节点 ;或者null--表示空树
*/
public BinaryTreeNode<E> getRoot() {
return root;
}

/**
* 获取左子节点
*
* @param parent 父节点引用
* @return 左子节点或者null--表示没有左子节点
*/
public BinaryTreeNode<E> getLeft(BinaryTreeNode<E> parent) {
return parent == null ? null : parent.left;
}

/**
* 获取右子节点
*
* @param parent 父节点引用
* @return 右子节点或者null--表示没有右子节点
*/
public BinaryTreeNode<E> getRight(BinaryTreeNode<E> parent) {
return parent == null ? null : parent.right;
}


/**
* 数据判null
*
* @param data 添加的数据
*/
private void checkNullData(E data) {
if (data == null) {
throw new NullPointerException("数据不允许为null");
}
}


/**
* 检查父节点是否为null
*
* @param parent 父节点引用
*/
private void checkNullParent(BinaryTreeNode<E> parent) {
if (parent == null) {
throw new NoSuchElementException("父节点不能为null");
}
}


/**
* 将以root为根节点的二叉树线索化 中序法
*
* @return true 线索化成功 ;false 线索化失败
*/
public boolean inThread() {
if (isEmpty()) {
return false;
}
inThread(getRoot());
return true;
}

/**
* 将以root为根节点的二叉树线索化 中序法
*
* @param root 节点,从根节点开始
*/
private void inThread(BinaryTreeNode<E> root) {
BinaryTreeNode<E> left = getLeft(root);
if (left != null) {
//如果左子节点不为null,则继续递归遍历该左子节点
inThread(left);
}

/*相比于中序遍历,中间多了如下步骤*/
else {
//如果左子节点为null,因为其前驱结点刚刚访问过,将左子节点设置为线索
//完成前驱结点的线索化
root.ltag = true;
root.left = pre;
}
//如果前驱没有右子节点,那就把当前节点当作 前驱结点的后继节点
if (pre != null && null == pre.right) {
pre.rtag = true;
pre.right = root;
}
//每次将当前节点设置为pre,下一个节点就把该节点当成前驱结点
pre = root;


BinaryTreeNode<E> right = getRight(root);
if (right != null) {
//如果右子节点不为null,则继续递归遍历该右子节点
inThread(right);
}
}


/**
* 中序遍历线索二叉树
*/
public void inThreadList(BinaryTreeNode<E> root) {
if (root == null) {
return;
}
//查找中序遍历的起始节点
while (root != null && !root.ltag) {
root = root.left;
}

while (root != null) {
System.out.print(root.data + ",");
// 如果右子节点是线索
if (root.rtag) {
root = root.right;
} else {
//有右子节点,遍历右子节点
root = root.right;
//如果右子节点不为null,并且右子节点的左子结点存在
while (root != null && !root.ltag) {
root = root.left;
}
}
}

}
}

3.1 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码public class ThreadTreeTest {
/**
* 构建二叉树,添加根节点r
*/
ThreadedBinaryTree<String> integerThreadedBinaryTree = new ThreadedBinaryTree<>("r");

/**
* 构建二叉树
*/
@Before
public void buildTree() {
/*构建二叉树*/
ThreadedBinaryTree.BinaryTreeNode<String> r = integerThreadedBinaryTree.getRoot();
//添加r根节点的左子结点a
ThreadedBinaryTree.BinaryTreeNode<String> a = integerThreadedBinaryTree.addChild(r, "a", true);
//添加r根节点的右子结点b
ThreadedBinaryTree.BinaryTreeNode<String> b = integerThreadedBinaryTree.addChild(r, "b", false);
//添加a节点的左子结点c
ThreadedBinaryTree.BinaryTreeNode<String> c = integerThreadedBinaryTree.addChild(a, "c", true);
//添加a节点的右子结点d
ThreadedBinaryTree.BinaryTreeNode<String> d = integerThreadedBinaryTree.addChild(a, "d", false);
//添加b节点的左子结点e
ThreadedBinaryTree.BinaryTreeNode<String> e = integerThreadedBinaryTree.addChild(b, "e", true);
//添加b节点的右子结点f
ThreadedBinaryTree.BinaryTreeNode<String> f = integerThreadedBinaryTree.addChild(b, "f", false);
//添加c节点的左子结点g
ThreadedBinaryTree.BinaryTreeNode<String> g = integerThreadedBinaryTree.addChild(c, "g", true);
//添加c节点的右子结点h
ThreadedBinaryTree.BinaryTreeNode<String> h = integerThreadedBinaryTree.addChild(c, "h", false);
//添加d节点的左子结点i
ThreadedBinaryTree.BinaryTreeNode<String> i = integerThreadedBinaryTree.addChild(d, "i", true);
//添加f节点的左子结点j
ThreadedBinaryTree.BinaryTreeNode<String> j = integerThreadedBinaryTree.addChild(f, "j", true);
}


/**
* 中序线索二叉树
*/
@Test
public void test2() {
//线索化
System.out.println(integerThreadedBinaryTree.inThread());
//线索化之后进行遍历,效率更高
integerThreadedBinaryTree.inThreadList(integerThreadedBinaryTree.getRoot());
//g c h a i d r e b j f
}
}

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python爬虫120例之第20例,1637、一路商机网全站

发表于 2021-11-27

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」

接下来的一些案例,将围绕销售用基础数据采集,行业将选择美妆行业,请知晓。

本案例将采用 lxml 与 cssselect 相结合的方式进行采集,重点在 cssselect 选择器。

目标站点分析

本次要抓取的目标为 http://www.1637.com/,该网站具备多分类,采集时提前将分类存储到一列表中,便于后续扩展。后来发现可一级行业可以选择 不限,此时可获取全部分类,基于此,我们先将全部数据抓取到本地,然后在筛选出美容/美妆行业相关加盟数据即可。

本次要抓取的数据量与页数如下图所示。

Python爬虫120例之第20例,1637、一路商机网全站加盟数据采集
抓取数据采用旧办法,先把 HTML 页面保存到本地,然后在进行二次处理。

使用到的技术点

请求数据使用 requests,数据提取使用 lxml + cssselect 实现,使用 cssselect 之前,通过 pip install cssselect 安装对应库即可。

安装完毕,在代码中有两种使用方式,其一采用 CSSSelector class,具体如下:

1
2
3
4
5
6
python复制代码from lxml.cssselect import CSSSelector
# 与正则表达式的使用方式有点相似,先构造一个CSS选择器对象
sel = CSSSelector('#div_total>em', translator="html")
# 然后将 Element 对象传入
element = sel(etree.HTML(res.text))
print(element[0].text)

上述用法适合提前构建好选择器,更便于扩展,如果不使用该方式,可以直接使用 cssselect method 进行实现,即下述代码:

1
2
python复制代码# 通过 cssselect 选择器,选择 em 标签
div_total = element.cssselect('#div_total>em')

不管使用上述两种方式中的哪一种,括号中的内容 #div_total>em 才是我们学习的重点,该写法是 CSS 选择器 的一种写法,如果你比较了解前端知识,很容易就可以掌握,如果不了解也没有问题,先记住如下内容。

CSS 选择器
假设存在如下一段 HTML 代码:

1
html复制代码<div class="totalnum" id="div_total">共<em>57041</em>个项目</div>

其中 class,id 都为 HTML 标签的属性值,一般 class 在网页中可以存在多个,而 id 只能存在一个。

如果希望获取 div 标签,使用 css 选择器,使用 #div_total 或者 .totalnum 都可以实现,重点注意如果依据 id 获取,那前面的符号为 #,如果依赖 class 获取,那前面的符号为 . 有的时候还会存在其它属性,在 css选择器 中,可以这样编写,修改 HTML 代码如下所示。

1
xml复制代码<div class="totalnum" id="div_total" custom="abc"> 共<em>57041</em>个项目 </div>

编写如下测试代码,注意 CSSSelector 部分的 css选择器 写法,即 div[custom="abc"] em。

1
2
3
python复制代码sel = CSSSelector('div[custom="abc"] em', translator="html")
element = sel(etree.HTML('<div class="totalnum" id="div_total" custom="abc"> 共<em>57041</em>个项目 </div>'))
print(element[0].text)

上述 css选择器 还应用到了一个知识点,叫做后代选择器,例如 #div_total>em,其中 #div_total 与 em 之间,存在一个 > 符号,该符号表示选择 id=div_total 的直接子元素 em,如果去除中间的 >,修改为 #div_total>em,表示选择 id=div_total 所有后代元素(子孙辈元素)中的 em 元素。

以上内容进行了简单掌握之后,你就可以简单编写自己的 cssselect 代码了。

编码时间

本案例采用的抓取方式为,先抓取 HTML 页面到本地,在针对本地文件进行解析,故采集代码比较简单,只需要动态获取一下总页码数即可。下述代码重点注意 get_pagesize 函数内部逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
python复制代码import requests
from lxml.html import etree
import random
import time


class SSS:
def __init__(self):
self.start_url = 'http://xiangmu.1637.com/p1.html'
self.url_format = 'http://xiangmu.1637.com/p{}.html'
self.session = requests.Session()
self.headers = self.get_headers()

def get_headers(self):
# 可从先前博客获取该函数
uas = [
"Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"
]
ua = random.choice(uas)
headers = {
"user-agent": ua,
"referer": "https://www.baidu.com"
}
return headers

def get_pagesize(self):

with self.session.get(url=self.start_url, headers=self.headers, timeout=5) as res:
if res.text:
element = etree.HTML(res.text)
# 通过 cssselect 选择器,选择 em 标签
div_total = element.cssselect('#div_total>em')
# 获取 em 标签内部文本 div_total[0].text,并将其转换为整数
total = int(div_total[0].text)
# 获取页码
pagesize = int(total / 10) + 1
# print(pagesize)
# 总数恰好被10整数,不用额外增加一页数据
if total % 10 == 0:
pagesize = int(total / 10)

return pagesize
else:
return None

def get_detail(self, page):
with self.session.get(url=self.url_format.format(page), headers=self.headers, timeout=5) as res:
if res.text:
with open(f"./加盟1/{page}.html", "w+", encoding="utf-8") as f:
f.write(res.text)
else:
# 如果无数据,重新请求
print(f"页码{page}请求异常,重新请求")
self.get_detail(page)

def run(self):
pagesize = self.get_pagesize()
# 测试数据,可临时修改 pagesize = 20
for page in range(1, pagesize):
self.get_detail(page)
time.sleep(2)
print(f"页码{page}抓取完毕!")


if __name__ == '__main__':
s = SSS()
s.run()

经过测试,如果不增加时间限制,很容易被限制 IP,即无法获取到数据,通过添加代理可以解决,如果只对数据感兴趣,可以直接在 下载地址 下载 HTML 包数据,解压密码为 cajie。

Python爬虫120例之第20例,1637、一路商机网全站加盟数据采集

二次提取数据

当静态 HTML 全部爬取到本地之后,提取页面数据,就变得简单了,毕竟不需要再解决反爬问题。

此时用到的核心技术点就是读取文件,在通过 cssselect 提取固定数据值。

通过开发者工具,查询数据所在标签节点如下,针对 class='xminfo' 的内容进行提取即可。

Python爬虫120例之第20例,1637、一路商机网全站加盟数据采集

下述代码核心展示数据提取方法,其中 format 函数为重点学习内容,由于数据存储为 csv 文件,所以需要 remove_character 函数去处理 \n 和英文 , 号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
python复制代码# 数据提取类
class Analysis:
def __init__(self):
pass

# 去除特殊字符
def remove_character(self, origin_str):
if origin_str is None:
return
origin_str = origin_str.replace('\n', '')
origin_str = origin_str.replace(',', ',')
return origin_str

def format(self, text):
html = etree.HTML(text)
# 获取所有项目区域 div
div_xminfos = html.cssselect('div.xminfo')
for xm in div_xminfos:
adtexts = self.remove_character(xm.cssselect('a.adtxt')[0].text) # 获取广告词列表
url = xm.cssselect('a.adtxt')[0].attrib.get('href') # 获取详情页地址

brands = xm.cssselect(':nth-child(2)>:nth-child(2)')[1].text # 获取品牌列表
categorys = xm.cssselect(':nth-child(2)>:nth-child(3)>a')[0].text # 获取分类,例如 ["餐饮","小吃"]
types = ''
try:
# 此处可能不存在二级分类
types = xm.cssselect(':nth-child(2)>:nth-child(3)>a')[1].text # 获取分类,例如 ["餐饮","小吃"]
except Exception as e:
pass
creation = xm.cssselect(':nth-child(2)>:nth-child(6)')[0].text # 品牌建立时间列表
franchise = xm.cssselect(':nth-child(2)>:nth-child(9)')[0].text # 加盟店数量列表
company = xm.cssselect(':nth-child(3)>span>a')[0].text # 公司名称列表

introduce = self.remove_character(xm.cssselect(':nth-child(4)>span')[0].text) # 品牌介绍
pros = self.remove_character(xm.cssselect(':nth-child(5)>:nth-child(2)')[0].text) # 经营产品介绍
investment = xm.cssselect(':nth-child(5)>:nth-child(4)>em')[0].text # 投资金额
# 拼接字符串
long_str = f"{adtexts},{categorys},{types},{brands},{creation},{franchise},{company},{introduce},{pros},{investment},{url}"
with open("./加盟数据.csv", "a+", encoding="utf-8") as f:
f.write(long_str + "\n")

def run(self):
for i in range(1, 5704):
with open(f"./加盟/{i}.html", "r", encoding="utf-8") as f:
text = f.read()
self.format(text)


if __name__ == '__main__':
# 采集数据,运行哪部分,去除注释即可
# s = SSS()
# s.run()
# 提取数据
a = Analysis()
a.run()

上述代码在提取 HTML 标签时,反复用到了 :nth-child(2),该选择器是:匹配属于其父元素的第 N 个子元素,不论元素的类型,所以你只需要准确的找到元素位置即可。

收藏时间

代码下载地址:codechina.csdn.net/hihell/pyth…,可否给个 Star。

来都来了,不发个评论,点个赞,收个藏吗?

今天是持续写作的第 200 / 200 天。
可以关注我,点赞我、评论我、收藏我啦。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

了解 WaitGroup 和 errorgroup 思考 W

发表于 2021-11-27

思考

我们可以通过 无buffer 的 channel来进行通知,那有没有更简便的方法?

WaitGroup

WaitGroup是什么?

WaitGroup等待一组Goroutine的完成,main goroutine 调用 Add 来设置要等待的 goroutine 的数量。然后每个 goroutine 运行并在完成时调用 Done。同时,Wait 可以用来阻塞,直到所有的 goroutine 都完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码type WaitGroup struct {
noCopy noCopy

// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
// 64-bit atomic operations require 64-bit alignment, but 32-bit
// compilers do not ensure it. So we allocate 12 bytes and then use
// the aligned 8 bytes in them as state, and the other 4 as storage
// for the sema.
state1 [3]uint32
}

// Add adds delta, which may be negative, to the WaitGroup counter.
func (wg *WaitGroup) Add(delta int) {}

// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {}

// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait() {}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
go复制代码func main() {
urls := []string{
"https://pic.netbian.com/uploads/allimg/210925/233922-163258436234e8.jpg",
"https://pic.netbian.com/uploads/allimg/210920/180354-16321322345f20.jpg",
"https://pic.netbian.com/uploads/allimg/210916/232432-16318058722f4d.jpg",
}
var wg sync.WaitGroup

for _,url := range urls{
wg.Add(1)
go func() {
defer wg.Done()
downloadFile(url)
}()
}
wg.Wait()
}

func downloadFile(URL string) error {
//Get the response bytes from the url
response, err := http.Get(URL)
if err != nil {
return err
}
defer response.Body.Close()

if response.StatusCode != 200 {
return errors.New("Received non 200 response code")
}
//Create a empty file
file, err := os.Create(path.Base(URL))
if err != nil {
return err
}
defer file.Close()

//Write the bytes to the fiel
_, err = io.Copy(file, response.Body)
if err != nil {
return err
}

return nil
}

问题

  1. 并发的时候url的值可能会发生混淆,因为在循环的时候使用的是相同的实例url,当执行downloadFile(url)的时候,url的值可能已经被更改

如何检测这种情况呢?

go vet

如何解决?

* 启动的时候将当前值绑定给闭包
* 创建一个新的变量
  1. 如何知道启动的goroutine组里边他们的运行情况?是否发生错误了?如何返回错误?假如某一个goroutine发生错误了,如何取消其他goroutine,避免资源的浪费
  2. 如何控制超时或者取消

errgroup

errgroup是什么?

提供同步,错误传播,一组gorouines的context的取消,致力于解决通用任务的子任务们

函数签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
go复制代码// A Group is a collection of goroutines working on subtasks that are part of
// the same overall task.
//
// A zero Group is valid and does not cancel on error.
type Group struct {
cancel func()

wg sync.WaitGroup

errOnce sync.Once
err error
}

// WithContext returns a new Group and an associated Context derived from ctx.
func WithContext(ctx context.Context) (*Group, context.Context) {}

// Wait blocks until all function calls from the Go method have returned, then
// returns the first non-nil error (if any) from them.
func (g *Group) Wait() error {}

// Go calls the given function in a new goroutine.
//
// The first call to return a non-nil error cancels the group; its error will be
// returned by Wait.
func (g *Group) Go(f func() error) {}

func WithContext

1
go复制代码func WithContext(ctx context.Context ) (*Group , context.Context )

WithContext 返回一个新的 Group 和一个从 ctx 派生的关联上下文。会创建一个带取消的Group

派生的 Context 在第一次传递给 Go 的函数返回非 nil 错误时或第一次 Wait 返回时被取消,以先发生者为准。

func (*Group) Go

1
go复制代码func (g * Group ) Go(f func() error)

Go 在一个新的 goroutine 中调用输入的函数。

第一次调用返回非nil 错误并且会执行取消逻辑;它的错误将由 Wait 返回。

func (*Group) Wait

1
go复制代码func (g * Group ) Wait() error

Wait 阻塞,直到所有来自 Go 方法的函数调用都返回,然后从它们返回第一个非 nil 错误(如果有)。

示例

传播错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码func main() {
urls := []string{
"https://pic.netbian.com/uploads/allimg/210925/233922-163258436234e8.jpg",
"https://pic.netbian.com/uploads/allimg/210920/180354-16321322345f20.jpg",
"https://pic.netbian.com/uploads/allimg/210916/232432-16318058722f4d.jpg",
"https://pic.netbian.com/uploads/allimg/210916/232432-16318058722f4d11.jpg",
}
eg := &errgroup.Group{}

for _,url := range urls{
url := url
eg.Go(func() error {
return downloadFile(url)
})

}
fmt.Println(eg.Wait())
}

取消其他子任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
go复制代码func main() {

eg, ctx := errgroup.WithContext(context.Background())

for i := 0; i < 10; i++ {
i := i
eg.Go(func() error {

if i >= 8{
time.Sleep(1 * time.Second)
}else{
time.Sleep(2 * time.Second)
}

select {
case <-ctx.Done():
fmt.Println("canceled ",i)
return ctx.Err()
default:
if i >= 8 {
fmt.Println("Error:", i)
return fmt.Errorf("Error occurred: %d", i)
}
fmt.Println(i)
return nil
}
})
}

fmt.Println("wait ", eg.Wait())
}

总结

  • 如果多个Goroutine出现错误,只会获取到第一个出错的Goroutine的错误信息,其他出错的Goroutine的错误信息将不会被感知到。
  • errgroup.Group在出现错误或者等待结束后都会调用 Context对象 的 cancel 方法同步取消信号

参考链接

  • waitgroup
  • errgroup

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

递归-虽然我不懂,但大受震撼?

发表于 2021-11-27

递归-虽然我不懂,但大受震撼?

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

笔者最近在学习《数据结构与算法之美》,正好借着这个机会边练习边记录一下自己学习的知识点。今天带来的是递归。

一、什么是递归

我记得刚开始接触递归的时候,是使用递归解决汉诺塔问题,学完之后,我的感觉大概是虽然我没太懂,但我大受震撼。递归的方式去解决汉诺塔问题确实很优雅,但同时也让我觉得递归好难懂。

递归简单来说就是将一个复杂的问题拆分成类似的多个小问题,解决小问题的同时最终解决复杂问题。

举个栗子:军训报数,教官想知道这一排有多少人,就让最后一个人说让你前面的人报数,前面的人有对他前面人说让你前面的人报数,直到第一个人,他前面没人他就会知道他是第一个,报数一,后一个就会知道报数二,直到最后一个报数,教官就会知道这一排有多少人了(实际上肯定不会这么干)。从后往前传递让前面的人报数的过程就是递,从前往后依次报数的过程就是归,相信你现在对递归应该有了一个简单的认识。

二、递归满足的是三个条件

了解了什么是递归,那什么样的问题适合用递归求解呢?这里总结了三个条件:

  • 一个复杂问题可以拆分为多个子问题求解。就像报数时,我想知道我是几号我首先应该知道我前面的人是几号,而前面的人也是如此他也需要知道他前面的人是几号。
  • 这个复杂问题和子问题除了规模不相同之外,解答思路完全一致。50 个人报数和 100 个人报数除了人数不同之外,解答问题的思路是完全相同的。
  • 存在递归的终止条件。报数时,当你的前面没有人时,就是终止条件,因为这时你就是第一个,报数一。

用个简单的口诀:大拆小,无不同,有终止。当问题满足这三个条件时,使用递归的方式求解问题可能会让你事半功倍哦。

三、如何实现递归

上面我们说了这么多,但是核心问题没解决,那就是递归怎么实现或者说递归的编码怎么写?

实现递归需要将大问题分解成一个个子问题,写出递推公式和终止条件,最后将其翻译成代码。

不啰嗦,我们直接实践。

3.1 实现斐波那契数列求值

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……,从第三项开始,每一项等于前两项之和。在数学上,斐波那契数列以如下被以递推的方法定义:

f(0)=0,f(1)=1,f(n)=f(n−1)+f(n−2)f(0)=0,f(1)=1,f(n)=f(n-1)+f(n-2)f(0)=0,f(1)=1,f(n)=f(n−1)+f(n−2)
例如,求解n=5。由公式可知,f(5)=f(4)+f(3)的和,而f(4) = f(3)+f(2),f(3)=f(2)+f(1)。f(2)=f(1)+f(0)。我们知道f(0)=0和f(1)=1,到此也无需继续分解,所以f(0)=0或f(1)=1就是递归终止的条件了。

f(5)f(4)f(3)f(3)f(2)f(2)f(1)f(1)f(0)f(2)f(1)f(1)f(0)
知道了递推公式和终止条件,将其翻译成代码,下面是具体实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class FibonacciTest {
public static int fibonacci(int n){
if (n== 0 || n == 1){
return n;
}
return fibonacci(n-1)+fibonacci(n-2);
}

public static void main(String[] args) {
System.out.println(fibonacci(12));
}
}

3.2 实现求阶乘n!

阶乘是数学中的一种计算方式,常用于求解排列组合问题。一个正整数的阶乘是所有小于及等于该数的正整数的积,并且0的阶乘为1。

亦即n!=1×2×3×…×(n-1)×n。阶乘亦可以递归方式定义:

0!=1,n!=(n−1)!×n0!=1,n!=(n-1)!×n0!=1,n!=(n−1)!×n
例如,求解5!。我们将5!的求解一层层分解,最后分解到1!=1× 0!时,而0!=1无法继续分解了,递归就此终止。

5!=5×4!4!=4×3!3!=3×2!2!=2×1!1!=1×0!5!=5×4!\
4!=4×3!\
3!=3×2!\
2!=2×1!\
1!=1×0!\5!=5×4!4!=4×3!3!=3×2!2!=2×1!1!=1×0!
下面是具体求解代码:

1
2
3
4
5
6
java复制代码public static int factorial(int n){
if (n == 0){
return 1;
}
return n * factorial(n-1);
}

3.3 实现一组数据集合的全排列

排列,一般地,从n个不同元素中取出m(m≤n)个元素,按照一定的顺序排成一列,叫做从n个元素中取出m个元素的一个排列。特别地,当m=n时,这个排列被称作全排列。

例如:

数组{1},全排列的结果就是{1}。

数组{1,2},全排列的结果就是{1,2},{2,1}。

数组{1,2,3},全排列的结果就是{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,2,1},{3,1,2}。

这个问题和上面的问题有些不同,它没有直接给出递推公式,但没有关系,还是一样的先进行问题分解,找出递推公式和终止条件,以数组{1,2,3}为例,看一下排列过程的实现。

先取元素1先取元素2先取元素3取元素2取元素3取元素1取元素3取元素1取元素2{ 1 , 2 , 3 }1 { 2 , 3 }2 { 1 , 3 }3 { 1 ,2 }1 , 2 { 3 }1 , 3 { 2 }1 , 2 , 31 , 3 , 22 , 1 { 3 }2 , 1 , 32 , 3 { 1 }2 , 3 , 13 , 1 { 2 }3 , 1 , 23 , 2 { 1 }3 , 2 , 1
可以看到从数组{1,2,3}中,

步骤1:先取元素1,就变成了元素1和{2,3}的全排列

步骤2:再取{2,3}中的元素2,此时变成了元素1,2和{3}的全排列,

步骤3:因为{3}只有一种排列方式,此时得到1,2,3的全排列。

在重复步骤2,再取{2,3}中的元素3,此时变成了元素1,3和{2}的全排列,因为{2}只有一种排列方式,此时得到1,3,2的全排列。

同样的,重复以上步骤,,对元素2和元素3都获取全排列。

对于n个元素也是一样,同样可以用类似上面的方式重复求出n个元素的全排列

步骤1:从n个元素中任意取一个元素x,就形成了x和n-1个元素的全排列

步骤2:为了求n-1个元素的全排列,从n-1个元素中任取一个元素y,形成x,y和n-2元素的全排列

步骤3:直到元素只剩一下一个,即只有一种全排列方式,则将这个元素放在全排列的最后一个位置,全排列结束。

这个问题的递推公式不像之前的两题可以写成具体的数学公式,而是以步骤的方式表示,且递归终止的条件就是元素只有一个时,全排列只有一种方式,则输出全排列。

下面是具体的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码public class Test {
/**
* 求数组元素的全排列
*/
public static void func(int[] arr){
func(arr,0,arr.length-1);
}

/**
* 求解数组位置 k 到位置 m 元素的全排列
*/
public static void func(int[] arr,int k,int m){
if (k == m){
//当元素只剩最后一个
System.out.println(Arrays.toString(arr));
}else {
for (int i = k; i <=m ; i++) {
swap(arr,i,k);//取位置i元素放到前面
func(arr,k+1,m);//求 k+1 到 m 元素的排列
swap(arr,i,k);//将之前放到前面的元素交换回来
}
}
}

/**
*交互数组位置 i 和位置 k 的元素
*/
private static void swap(int[] arr, int i , int k){
int temp = arr[i];
arr[i]= arr[k];
arr[k]= temp;
}

public static void main(String[] args) {
int arr[] = new int[]{1,2,3,4};
func(arr);
}
}

四、递归的陷阱

递归可以让我们解决问题的代码十分简洁优雅,但同时也要警惕递归存在的一些陷阱。

4.1 栈溢出

递归在编码实现时,是一个不断回调方法本身的过程,将方法一遍遍压入栈中,递归次数太多,栈就满了也即发生栈溢出。

那么怎么解决栈溢出的问题呢?

简单的解决方法一是限制递归的次数,但却不实用;二是将递归改写为循环。

4.2 重复计算

在斐波那契数列数列中求解过程中,存在着重复计算的问题。

例如求解f(5)=f(4)+f(3)的过程,在计算f(4)时,我们其实已经计算过了f(3),但计算完f(4)后,又重复f(3)求解过程。

这里可以使用哈希表避免重复计算,将求解的中间结果存储在哈希表中,每次都先从哈希表中获取结果,没有再进行计算。

五、总结

  • 递归简单来说就是将一个复杂的问题拆分成类似的多个小问题,解决小问题的同时最终解决复杂问题。
  • 满足大拆小,无不同,有终止,这三个条件的问题就可以用递归来实现。
  • 实现递归先是分解问题,总结递推公式和终止条件,翻译成代码。找出递推公式是第一道难关,翻译成代码是第二道难关,要想破关也只能多练多用才能孰能生巧。
  • 最后要警惕递归的陷阱,栈溢出和重复计算问题。

如果你有所收获,欢迎你点赞评论,发表你对递归的想法。

你有你的想法,我有我的想法,彼此交换,我们就对递归就有了两种理解,甚至更多。

dianzanjiaxiao.jpg

往期:
队列栈链表数组

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…151152153…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%