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

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


  • 首页

  • 归档

  • 搜索

Maven插件之spotify的docker-maven-p

发表于 2021-11-18

docker-maven-plugin能有效帮助我们在面对数量众多的微服务项目时,自动化构建和容器化部署,提高部署效率。

docker-maven-plugin文档

dockerfile-maven-plugin文档

禁止转载!!!禁止转载!!!禁止转载!!!

首发地址:xu.vercel.app

一、修改docker配置

修改宿主机的docker配置,让其可以远程访问

1
2
3
4
powershell复制代码
vi /lib/systemd/system/docker.service
# 在ExecStart=后添加配置
‐H tcp://0.0.0.0:2375 ‐H unix:///var/run/docker.sock

修改后配置如下:

1
powershell复制代码ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

刷新配置重启服务:

1
2
3
4
powershell复制代码# 通知docker服务做出的修改
systemctl daemon-reload
# 重启docker服务
systemctl restart docker

接下来测试一下看是否能连接到docker api

1
2
3
powershell复制代码ps -ef | grep docker 
[root@localhost ~]# curl -X GET http://192.168.122.22:2375/info
{"ID":"XHRK:S4AH:7YDS:X43Y:BFCZ:XQ5N:VBIV:K6V4:SUVK:VFPM:JJYS:KZBB","Containers":7,"ContainersRunning":4,"ContainersPaused":0,"ContainersStopped":3,"Images":7,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","xfs"],["Supports d_type","true"],["Native Overlay Diff","true"],["userxattr","false"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","ipvlan","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","local","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":true,"IPv4Forwarding":true,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":false,"NFd":50,"OomKillDisable":true,"NGoroutines":55,"SystemTime":"2021-11-11T15:21:22.728142607+08:00","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","CgroupVersion":"1","NEventsListener":0,"KernelVersion":"3.10.0-957.el7.x86_64","OperatingSystem":"CentOS Linux 7 (Core)","OSVersion":"7","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":[],"AllowNondistributableArtifactsHostnames":[],"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":["https://pee6w651.mirror.aliyuncs.com/"],"Secure":true,"Official":true}},"Mirrors":["https://pee6w651.mirror.aliyuncs.com/"]},"NCPU":2,"MemTotal":2928689152,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"localhost.localdomain","Labels":[],"ExperimentalBuild":false,"ServerVersion":"20.10.7","Runtimes":{"io.containerd.runc.v2":{"path":"runc"},"io.containerd.runtime.v1.linux":{"path":"runc"},"runc":{"path":"runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"d71fcd7d8303cbf684402823e425e9dd2e99285d","Expected":"d71fcd7d8303cbf684402823e425e9dd2e99285d"},"RuncCommit":{"ID":"b9ee9c6314599f1b4a7f497e1f1f856fe433d3b7","Expected":"b9ee9c6314599f1b4a7f497e1f1f856fe433d3b7"},"InitCommit":{"ID":"de40ad0","Expected":"de40ad0"},"SecurityOptions":["name=seccomp,profile=default"],"Warnings":["WARNING: API is accessible on http://0.0.0.0:2375 without encryption.\n Access to the remote API is equivalent to root access on the host. Refer\n to the 'Docker daemon attack surface' section in the documentation for\n more information: https://docs.docker.com/go/attack-surface/","WARNING: bridge-nf-call-iptables is disabled","WARNING: bridge-nf-call-ip6tables is disabled"]}

1.2 启动失败的问题

启动报错Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
powershell复制代码[root@localhost ~]# systemctl start docker
Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.
[root@localhost ~]# systemctl start docker.service
Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.

[root@localhost ~]# systemctl status docker.service
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
Active: failed (Result: start-limit) since 四 2021-11-11 14:54:01 CST; 1min 14s ago
Docs: https://docs.docker.com
Process: 23534 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock ‐H tcp://0.0.0.0:2375 ‐H unix:///var/run/docker.sock (code=exited, status=1/FAILURE)
Main PID: 23534 (code=exited, status=1/FAILURE)

11月 11 14:54:01 localhost.localdomain systemd[1]: start request repeated too quickly for docker.service
11月 11 14:54:01 localhost.localdomain systemd[1]: Failed to start Docker Application Container Engine.
11月 11 14:54:01 localhost.localdomain systemd[1]: Unit docker.service entered failed state.
11月 11 14:54:01 localhost.localdomain systemd[1]: docker.service failed.
11月 11 14:54:13 localhost.localdomain systemd[1]: start request repeated too quickly for docker.service
11月 11 14:54:13 localhost.localdomain systemd[1]: Failed to start Docker Application Container Engine.
11月 11 14:54:13 localhost.localdomain systemd[1]: docker.service failed.
11月 11 14:54:18 localhost.localdomain systemd[1]: start request repeated too quickly for docker.service
11月 11 14:54:18 localhost.localdomain systemd[1]: Failed to start Docker Application Container Engine.
11月 11 14:54:18 localhost.localdomain systemd[1]: docker.service failed.

1.3 解决启动失败

因为修改的是/usr/lib/systemd/system/docker.service下的服务配置文件:
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock

那么就应该加载该目录下的system服务systemctl start docker.service

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
powershell复制代码
[root@localhost ~]# systemctl daemon-reload
[root@localhost ~]# systemctl stop docker.service
Warning: Stopping docker.service, but it can still be activated by:
docker.socket
[root@localhost ~]# ps -ef | grep docker
root 47375 18279 0 15:13 pts/0 00:00:00 grep --color=auto docker
[root@localhost ~]# systemctl start docker.service
[root@localhost ~]# systemctl status docker.service
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
Active: active (running) since 四 2021-11-11 15:13:43 CST; 37s ago
Docs: https://docs.docker.com
Main PID: 47559 (dockerd)
CGroup: /system.slice/docker.service
├─47559 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
├─47751 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 6379 -container-ip 172.17.0.2 -container-port 6379
├─47756 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 6379 -container-ip 172.17.0.2 -container-port 6379
├─47783 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 3306 -container-ip 172.17.0.3 -container-port 3306
├─47788 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 3306 -container-ip 172.17.0.3 -container-port 3306
├─47822 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9000 -container-ip 172.17.0.4 -container-port 9000
├─47836 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 9000 -container-ip 172.17.0.4 -container-port 9000
├─47884 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 30443 -container-ip 172.17.0.5 -container-port 443
├─47893 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 30443 -container-ip 172.17.0.5 -container-port 443
├─47924 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 30080 -container-ip 172.17.0.5 -container-port 80
└─47939 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 30080 -container-ip 172.17.0.5 -container-port 80

11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.097948827+08:00" level=info msg="ClientConn switching balancer to "pick_...ule=grpc
11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.103540123+08:00" level=info msg="[graphdriver] using prior storage driver: overlay2"
11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.109459318+08:00" level=info msg="Loading containers: start."
11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.226585154+08:00" level=info msg="Default bridge (docker0) is assigned wit...address"
11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.944106192+08:00" level=info msg="Loading containers: done."
11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.973773238+08:00" level=info msg="Docker daemon" commit=b0f5bc3 graphdrive...=20.10.7
11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.973916900+08:00" level=info msg="Daemon has completed initialization"
11月 11 15:13:43 localhost.localdomain dockerd[47559]: time="2021-11-11T15:13:43.999505683+08:00" level=info msg="API listen on /var/run/docker.sock"
11月 11 15:13:43 localhost.localdomain systemd[1]: Started Docker Application Container Engine.
11月 11 15:14:12 localhost.localdomain systemd[1]: Current command vanished from the unit file, execution of the command list won't be resumed.
Hint: Some lines were ellipsized, use -l to show in full.
[root@localhost ~]# netstat -ano | grep 2375
tcp6 0 0 :::2375 :::* LISTEN off (0.00/0/0)

二、修改防火墙

其他centos7防火墙配置
我这里是虚拟机,防火墙服务关闭了,所以无需配置下面的信息,生产环境自行配置

1
2
3
4
5
powershell复制代码[root@localhost ~]# systemctl status firewalld
● firewalld.service - firewalld - dynamic firewall daemon
Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled)
Active: inactive (dead)
Docs: man:firewalld(1)

修改防火墙策略,允许访问2375端口:

1
2
3
4
powershell复制代码#开放2375/tcp端口
firewall-cmd --zone=public --add-port=2375/tcp --permanent
#更新防火墙的设置,使上面的修改生效
firewall-cmd --reload

查看防火墙开放状态:

1
2
powershell复制代码#查看所有打开的端口
firewall-cmd --zone=public --list-ports

三、使用插件

3.1 docker-maven-plugin插件

官方Github文档

(1) 不使用Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码            <plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.2</version>
<configuration>
<imageName>${project.artifactId}:${project.version}</imageName>
<!-- <baseImage>jdk1.8</baseImage> -->
<baseImage>xuux/small-jre8:1.0</baseImage>
<entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<dockerHost>http://192.168.122.22:2375</dockerHost>
</configuration>
</plugin>

执行 build 操作
mvn clean package docker:build

执行 build 完成后 push 镜像:
mvn clean package docker:build -DpushImage

执行 build 并 push 指定 tag 的镜像
mvn clean package docker:build -DpushImageTag

注意:这里必须指定至少一个 imageTag,它可以配置到 POM 中,也可以在命令行指定。

指定方式有两种:

  1. 命令行指定如下:mvn clean package docker:build -DpushImageTags -DdockerImageTags=imageTag_1 -DdockerImageTags=imageTag_2
  2. POM 文件中指定:
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
xml复制代码<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.2</version>
<configuration>
<imageName>${project.artifactId}:${project.version}</imageName>
<baseImage>xuux/small-jre8:1.0</baseImage>
<entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<dockerHost>http://192.168.122.22:2375</dockerHost>
<!--指定tag-->
<imageTags>
<imageTag>imageTag_1</imageTag>
<imageTag>imageTag_2</imageTag>
</imageTags>
</configuration>
</plugin>
</plugins>
</build>

(2) 使用Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.2</version>
<configuration>
<imageName>${project.artifactId}:${project.version}</imageName>
<!-- 指定 Dockerfile 路径-->
<dockerDirectory>${basedir}/docker</dockerDirectory>
<!-- 这里是复制 jar 包到 docker 容器指定目录配置,也可以写到 Docokerfile 中 -->
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<dockerHost>http://192.168.122.22:2375</dockerHost>
<!--指定tag-->
<imageTags>
<imageTag>1.0.1</imageTag>
</imageTags>
</configuration>
</plugin>

Dockerfile文件:

1
2
3
4
5
powershell复制代码FROM xuux/small-jre8:1.0
MAINTAINER xu_ux test@email.com
WORKDIR /opt
CMD ["java", "-version"]
ENTRYPOINT ["java", "-jar", "${project.build.finalName}.jar"]

执行命令:
mvn clean package docker:build -DpushImageTag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
powershell复制代码
[INFO] Building image demo:0.0.1-SNAPSHOT
Step 1/5 : FROM xuux/small-jre8:1.0
---> 4fa1f55350fb
Step 2/5 : MAINTAINER xu_ux test@email.com
---> Running in c8b2e5b012c4
Removing intermediate container c8b2e5b012c4
---> 14353903927c
Step 3/5 : WORKDIR /opt
---> Running in d8dfcf2f63a6
Removing intermediate container d8dfcf2f63a6
---> 6e980ad6882a
Step 4/5 : CMD ["java", "-version"]
---> Running in 9bae114258a6
Removing intermediate container 9bae114258a6
---> 212ff76fe2a2
Step 5/5 : ENTRYPOINT ["java", "-jar", "${project.build.finalName}.jar"]
---> Running in 2f59b4c7ac0a
Removing intermediate container 2f59b4c7ac0a
---> 07c262894893
ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}
Successfully built 07c262894893
Successfully tagged demo:0.0.1-SNAPSHOT
1
2
3
4
5
powershell复制代码
[root@localhost jre1.8.0_311]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
demo 0.0.1-SNAPSHOT 07c262894893 About a minute ago 224MB
demo 1.0.1 07c262894893 About a minute ago 224MB

(3) 绑定 Docker 命令到 Maven各个阶段

官方Github上已经很详细了,大家可以上去看看官方文档

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
xml复制代码<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.2</version>
<configuration>
......
</configuration>
<executions>
<execution>
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
<execution>
<id>tag-image</id>
<phase>package</phase>
<goals>
<goal>tag</goal>
</goals>
<configuration>
<image>${project.artifactId}:latest</image>
<newName>${project.artifactId}:${project.version}</newName>
</configuration>
</execution>
<execution>
<id>push-image</id>
<phase>deploy</phase>
<goals>
<goal>push</goal>
</goals>
<configuration>
<imageName>${project.artifactId}:${project.version}</imageName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

执行mvn package时,执行 build、tag 操作
执行mvn deploy时,执行build、tag、push 操作
如果我们想跳过 docker 某个过程时,只需要:

1
2
3
4
powershell复制代码-DskipDockerBuild 跳过 build 镜像
-DskipDockerTag 跳过 tag 镜像
-DskipDockerPush 跳过 push 镜像
-DskipDocker 跳过整个阶段

例如:我们想执行 package 时,跳过 tag 过程,那么就需要mvn package -DskipDockerTag

3.2 dockerfile-maven-plugin插件

官方推荐使用dockerfile-maven-plugin

docker-maven-plugin:We recommend you use dockerfile-maven instead.

注意:需要结合Dockerfile文件,设置环境变量

1.设置环境变量(必须)

环境变量名称:DOCKER_HOST
变量值:tcp://192.168.122.22:2375(根据实际IP地址来)

1
powershell复制代码export DOCKER_HOST=tcp://localhost:2375

2. xml配置

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码    <plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.13</version>
<configuration>
<repository>${project.artifactId}</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.13</version>
<executions>
<execution>
<id>default</id>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
<configuration>
<repository>${project.artifactId}</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>

该插件的配置比docker-maven-plugin更简单

  • repository:指定docker镜像的repo名字
  • tag:指定docker镜像的tag
  • buildArgs:可以指定一个或多个变量,传递给Dockerfile,在Dockerfile中通过ARG指令进行引用

在execution中同时指定build和push目标:
当运行mvn package时,会自动执行build目标,构建Docker镜像。当运行mvn deploy命令时,会自动执行push目标,将Docker镜像push到Docker仓库。

3. 创建Dockerfile文件

Dockerfile文件必须在根目录下,与pom.xml文件同级

1
2
3
4
5
powershell复制代码FROM xuux/small-jre8:1.0
MAINTAINER xu_ux test@email.com
WORKDIR /opt
CMD ["java", "-version"]
ENTRYPOINT ["java", "-jar", "${project.build.finalName}.jar"]

4. 其他使用方法:

github.com/spotify/doc…

四、问题

1
xml复制代码<baseImage>jdk1.8</baseImage>
1
vbnet复制代码pull access denied for jdk1.8, repository does not exist or may require 'docker login': denied: requested access to the resource is denied

原因:
报这个错是因为docker上没有jdk1.8镜像文件。

解决方法:
换成xuux/small-jre8

1
2
3
4
5
powershell复制代码# 修改xml
<baseImage>xuux/small-jre8</baseImage>

# 宿主机获取镜像
docker pull xuux/small-jre8:1.0

参考:

Maven 插件之 docker-maven-plugin 的使用

本文转载自: 掘金

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

还在为处理事务烦恼吗,要不试试Spring是如何处理业务的

发表于 2021-11-18

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

👨‍🎓作者:Java学术趴

🏦仓库:Github、Gitee

✏️博客:CSDN、掘金、InfoQ、云+社区

💌公众号:Java学术趴

🚫特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系小编授权。

🙏版权声明:文章里的部分文字或者图片来自于互联网以及百度百科,如有侵权请尽快联系小编。微信搜索公众号Java学术趴联系小编。

☠️每日毒鸡汤:微笑拥抱每一天,做像向日葵般温暖的女子。

👋大家好!我是你们的老朋友Java学术趴。最近小编又在整了Spring全家桶笔记,笔记会每天定时的进行发放,喜欢的大佬们欢迎收藏点赞关注呦。小编会每天分享的呦。Spring 框架不局限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。Spring 框架还是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力。

第一章 Spring的事务

理解事务之前,先讲一个你日常生活中最常干的事:取钱。

比如你去ATM机取1000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉1000元钱;然后ATM出1000元钱。这两个步骤必须是要么都执行要么都不执行。如果银行卡扣除了1000块但是ATM出钱失败的话,你将会损失1000元;如果银行卡扣钱失败但是ATM却出了1000块,那么银行将损失1000元。所以,如果一个步骤成功另一个步骤失败对双方都不是好事,如果不管哪一个步骤失败了以后,整个取钱过程都能回滚,也就是完全取消所有操作的话,这对双方都是极好的。

事务就是用来解决类似问题的。事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。

在企业级应用程序开发中,事务管理必不可少的技术,用来确保数据的完整性和一致性。

事务有四个特性:ACID

  • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
  • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
  • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

核心接口

image.png

1.1 针对事务的分析

spring的事务操作是在同一个数据库执行的,操作的是这个数据库中的不同表。

什么是事务:

  • 在mysql中提出了关于事务的一词。事务是指一组sql语句的集合,集合中有多条sql语句。可能是delete、update、insert等语句。我们希望这些sql语句同时成功或者失败才可以完成相应的功能。比如转账系统。 这些sql语句的执行是一致的,作为一个整体执行。

在什么时候使用事务

  • 当项目中实现某个功能需要多个表的时候,或者是多个sql语句的insert、update、delete。需要保证这些语句都是同时成功或者失败的时候才能完成某个功能。不可以是单独的某个sql语句执行成功那么功能就实现的。

在Java代码中写程序,控制事务,此时事务应该放到哪里?

  • Service类的业务方法上,因为在Service类中的某个功能(方法)可能需要多个Dao中的方法才可以完成这个业务,而dao是执行sql语句的,此时就可以把这些dao调用的方法看做是一个业务

通常使用JDBC访问数据库、mybatis访问数据库是怎么处理业务的。

  • JDBC访问数据库:处理事务 (Connection conn ; conn.commit(); conn.rollback();)
  • mybatis访问数据库:处理事务(sqlSession.commit() ; sqlSession.rollback(); )
  • hibernate访问数据库 :处理事务(Session.commit() ; Session.rollback();)

以上处理业务有什么不足

  1. 不同的数据库需要不同的事务处理对象,方法不同,需要了解不同数据库事务的技术的原理
  2. 掌握多种数据库中事务处理的业务逻辑。什么时候提交事务,什么时候回滚事务。
  3. 处理事务的多中国方法不同。

解决事务处理的不足之处

  • 使用spring框架统一解决事务处理

1.2 Spring处理事务的统一方式

  • 事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层, 即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。

在 Spring 中通常可以通过以下两种方式来实现对事务的管理:

  1. 编程式事务管理:使用 Spring 的事务注解管理事务
  2. 声明式事务管理:使用 AspectJ 的 AOP 配置管理事务

spring提供了一种统一处理事务的模型,能使用统一步骤,方式完成多种不同数据库访问技术的事务处理。

  • 使用spring的事务处理机制,可以完成mybatis访问数据库的事务处理。
  • 使用spring的事务处理机制,可以完成hibernate访问数据库的事务处理。

image-20211116201918300

image-20211116210252534

1.3 Spring事务管理API

  • Spring 的事务管理,主要用到两个事务相关的接口。

(1) 事务管理器接口(重点)

  • 事务管理器是 PlatformTransactionManager 接口对象。 其主要用于完成事务的提交、回 滚,及获取事务的状态信息。

事务管理器是 PlatformTransactionManager 接口对象。常用的两个实现类:

  • DataSourceTransactionManager: 使用 JDBC 或 MyBatis 进行数据库操作时使用。
  • HibernateTransactionManager: 使用 Hibernate 进行持久化数据时使用。

Spring的回滚方式(理解)

  • Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。 不过,对于受查异常,程序员也可以手工设置其回滚方式。

回顾错误与异常

  • Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一) 的实例时,才能通过 Java 虚拟机或者 Java 的 throw 语句抛出。
  • Error 是程序在运行过程中出现的无法处理的错误,比如 OutOfMemoryError、 ThreadDeath、NoSuchMethodError 等。当这些错误发生时,程序是无法处理(捕获或抛出) 的,JVM 一般会终止线程。
  • 程序在编译和运行时出现的另一类错误称之为异常,它是 JVM 通知程序员的一种方式。 通过这种方式,让程序员知道已经或可能出现错误,要求程序员对其进行处理。

异常分为运行时异常与受查异常。

  • 运行时异常,是 RuntimeException 类或其子类,即只有在运行时才出现的异常。如, NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException 等均属于 运行时异常。这些异常由 JVM 抛出,在编译时不要求必须处理(捕获或抛出)。但,只要代 码编写足够仔细,程序足够健壮,运行时异常是可以避免的。
  • 受查异常,也叫编译时异常,即在代码编写时要求必须捕获或抛出的异常,若不处理, 则无法通过编译。如 SQLException,ClassNotFoundException,IOException 等都属于受查异常。
  • RuntimeException 及其子类以外的异常,均属于受查异常。当然,用户自定义的 Exception 的子类,即用户自定义的异常也属受查异常。程序员在定义异常时,只要未明确声明定义的 为 RuntimeException 的子类,那么定义的就是受查异常。

(2) 事务定义接口

  • 事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别、 事务传播行为、事务默认超时时限,及对它们的操作

定义了五个事务隔离级别常量(掌握)

这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。

➢ DEFAULT:采用 DB 默认的事务隔离级别。MySql 的默认为 REPEATABLE_READ; Oracle 默认为 READ_COMMITTED。

➢ READ_UNCOMMITTED:读未提交。未解决任何并发问题。

➢ READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。

➢ REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读 。

➢ SERIALIZABLE:串行化。不存在并发问题。

定义了七个事务传播行为常量(掌握)

  • 所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情 况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的 维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX。

  • PROPAGATION_REQUIRED
  • PROPAGATION_REQUIRES_NEW
  • PROPAGATION_SUPPORTS

重点掌握前三个

  • PROPAGATION_MANDATORY
  • PROPAGATION_NESTED
  • PROPAGATION_NEVER
  • PROPAGATION_NOT_SUPPORTED

PROPAGATION_REQUIRED:

  • 指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事 务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
  • 如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事 务内运行的,则 doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用 doOther()方法时没有在事务内执行,则 doOther()方法会创建一个事务,并在其中执行。

PROPAGATION_SUPPORTS

  • 指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。

image-20211116213029151

PROPAGATION_REQUIRES_NEW

  • 总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕

image-20211116213054881

定义了默认事务超时时限

  • 常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,sql 语句的执行时长。
  • 注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该 值一般就使用默认值即可。

总结spring的事务

  1. 管理事务的是:事务管理和他的实现类
  2. spring的事务的一个统一模型

1)指定要使用的事务管理器实现类,使用

2)指定哪些类,哪些方法需要加入事务的功能。

3) 指定方法需要的隔离级别,传播行为,超时等
3. 我们需要告诉spring,项目中类信息、方法的名称、方法的事务传播行为。

1.4 使用 Spring 的方式管理事务有两种方式

  • 声明式:使用的是AspectJ框架实现。

使用aspectJ框架需要加入在pom.xml中加入aspectJ的依赖

1
2
3
4
5
xml复制代码<dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-aspects</artifactId>
     <version>5.2.5.RELEASE</version>
   </dependency>

在spring的主配置文件中(applicationContext.xml)使用以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!--使用spring的aop方式进行事务处理-->
   <!--1. 声明事务管理器-->
   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
       <!--指定连接的数据库,指定上边定义的数据源-->
       <property name="dataSource" ref="myDataSource"/>
   </bean>
​
   <!--2. 开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象-->
   <!--
       注意:这里使用的annotation-driven 一定是tx类下的。
       transaction-manager : 代表事务管理器的id值
   -->
   <tx:annotation-driven transaction-manager="transactionManager"/>
  • 注解式:使用注解Spring中自带的aop方式实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<!--1. 声明事务管理对象-->
   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
       <property name="dataSource" ref="myDataSource"/>
   </bean>
<!--2. 声明业务方法的事务属性(隔离级别、传播行为、超时时间)-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
       <!--tx:attributes:是advice的子标签,代表配置这个事务的属性-->
       <tx:attributes>
           <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                      rollback-for="java.lang.NullPointerException,com.yunbocheng.error.RangeExceeds"/>
           <!--使用通配符的方式,一次指定很多个以 add开头的方法-->
           <tx:method name="add*" propagation="REQUIRES_NEW"/>
           <!--使用通配符指定修改方法-->
           <tx:method name="modify*" />
           <!--指定除了上边的所有方法的属性-->
           <tx:method name="*" read-only="true" propagation="SUPPORTS" />
       </tx:attributes>
   </tx:advice>
<!--3. 配置aop-->
<aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/>
<aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/>
</aop:config>

1.5 使用 Spring 的事务注解管理事务(掌握)

通过@Transactional 注解方式,可将事务织入到相应 public 方法中,实现事务管理。

需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解@Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该 方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。

若@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。

若@Transaction 注解在方法上,则表示该方法只能是public修饰的才可以将在执行时织入事务。

@Transactional 的所有可选属性如下所示:

➢ propagation: 用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为 Propagation.REQUIRED。

➢ isolation: 用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。

➢ readOnly: 用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值 为 false。

➢ timeout: 用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。

➢ rollbackFor: 指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有 一个异常类时,可以不使用数组。

➢ rollbackForClassName: 指定需要回滚的异常类类名。类型为 String[],默认值为空数组。 当然,若只有一个异常类时,可以不使用数组。

➢ noRollbackFor: 指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若 只有一个异常类时,可以不使用数组。

➢ noRollbackForClassName: 指定不需要回滚的异常类类名。类型为 String[],默认值为空 数组。当然,若只有一个异常类时,可以不使用数组。

第二章 Spring与Web

  • 在 Web 项目中使用 Spring 框架,首先要解决在 web 层(这里指 Servlet)中获取到 Spring 容器的问题。只要在 web 层获取到了 Spring 容器,便可从容器中获取到 Service 对象。

2.1Web 项目使用 Spring 的问题(了解)

  • 第一步:新建一个Maven Project

类型 maven-archetype-webapp

  • *第二步: 复制代码,配置文件,jar

将 spring-mybatis 项目中以下内容复制到当前项目中:

(1)Service 层、Dao 层全部代码

(2)配置文件 applicationContext.xml 及 jdbc.properties,mybatis.xml

(3)pom.xml

(4)加入 servlet ,jsp 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<!-- servlet依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- jsp依赖 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2.1-b03</version>
<scope>provided</scope>
</dependency>
​
  • 第三步:定义 index 页面

image-20211117224804124

第四步:定义 RegisterServlet(重点代码)

第五步:定义 success 页面

image-20211117225025992

第六步:web.xml 注册 Servlet

第七步:运行结果分析

  • 当表单提交,跳转到 success.jsp 后,多刷新几次页面,查看后台输出,发现每刷新一次 页面,就 new 出一个新的 Spring 容器。即,每提交一次请求,就会创建一个新的 Spring 容 器。对于一个应用来说,只需要一个 Spring 容器即可。所以,将 Spring 容器的创建语句放 在 Servlet 的 doGet()或 doPost()方法中是有问题的。

  • 此时,可以考虑,将 Spring 容器的创建放在 Servlet 进行初始化时进行,即执行 init()方 法时执行。并且,Servlet 还是单例多线程的,即一个业务只有一个 Servlet 实例,所有执行 该业务的用户执行的都是这一个 Servlet 实例。这样,Spring 容器就具有了唯一性了。
  • 但是,Servlet 是一个业务一个 Servlet 实例,即 LoginServlet 只有一个,但还会有 StudentServlet、TeacherServlet 等。每个业务都会有一个 Servlet,都会执行自己的 init()方法, 也就都会创建一个 Spring 容器了。这样一来,Spring 容器就又不唯一了。

2.2使用 Spring 的监听器 ContextLoaderListener(掌握)

  • 举例:springweb-2 项目(在 spring-web 项目基础上修改)
  • 对于 Web 应用来说,ServletContext 对象是唯一的,一个 Web 应用,只有一个 ServletContext 对象,该对象是在 Web 应用装载时初始化的。若将 Spring 容器的创建时机, 放在 ServletContext 初始化时,就可以保证 Spring 容器的创建只会执行一次,也就保证了 Spring 容器在整个应用中的唯一性。
  • 当 Spring 容器创建好后,在整个应用的生命周期过程中,Spring 容器应该是随时可以被访问的。即,Spring 容器应具有全局性。而放入 ServletContext 对象的属性,就具有应用的 全局性。所以,将创建好的 Spring 容器,以属性的形式放入到 ServletContext 的空间中,就 保证了 Spring 容器的全局性。
  • 上述的这些工作,已经被封装在了如下的 Spring 的 Jar 包的相关 API 中:spring-web-5.2.5.RELEASE

第一步:maven 依赖 pom.xml

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>

第二步:注册监听器 ContextLoaderListener

  • 若要在 ServletContext 初 始 化 时 创 建 Spring 容 器 , 就 需 要 使 用 监 听 器 接 口 ServletContextListener 对 ServletContext 进行监听。在 web.xml 中注册该监听器。

image-20211117225421191

  • Spring 为该监听器接口定义了一个实现类 ContextLoaderListener,完成了两个很重要的 工作:创建容器对象,并将容器对象放入到了 ServletContext 的空间中。
  • 打开 ContextLoaderListener 的源码。看到一共四个方法,两个是构造方法,一个初始化 方法,一个销毁方法。

所以,在这四个方法中较重要的方法应该就是 contextInitialized(),context 初始化方法。

  • 跟踪 initWebApplicationContext()方法,可以看到,在其中创建了容器对象。

image-20211117225511622

并且,将创建好的容器对象放入到了 ServletContext 的空间中,key 为一个常量: WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。

第三步:指定 Spring 配置文件的位置< context-parm >

  • ContextLoaderListener 在对 Spring 容器进行创建时,需要加载 Spring 配置文件。其默认 的 Spring 配置文件位置与名称为:WEB-INF/applicationContext.xml。但,一般会将该配置文 件放置于项目的 classpath 下,即 src 下,所以需要在 web.xml 中对 Spring 配置文件的位置及 名称进行指定。

  • 从监听器 ContextLoaderListener 的父类 ContextLoader 的源码中可以看到其要读取的配 置文件位置参数名称 contextConfigLocation。

第四步:获取 Spring 容器对象

在 Servlet 中获取容器对象的常用方式有两种:

(1) 直接从 ServletContext 中获取

从对监听器 ContextLoaderListener 的源码分析可知,容器对象在 ServletContext 的中存 放的 key 为 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。所以,可 以直接通过 ServletContext 的 getAttribute()方法,按照指定的 key 将容器对象获取到。

image-20211117225732047

(2) 通过 WebApplicationContextUtils 获取

  • 工具类 WebApplicationContextUtils 有一个方法专门用于从 ServletContext 中获取 Spring 容器对象:getRequiredWebApplicationContext(ServletContext sc)

调用 Spring 提供的方法获取容器对象:

总结:以上两种方式,无论使用哪种获取容器对象,刷新 success 页面后,可看到代码中使用 的 Spring 容器均为同一个对象。

关于Spring的所有内容就分享完毕啦,明天给大家带来新的知识点,SpringMVC。

以上项目的源代码,点击星球进行免费获取 星球 (Github地址)如果没有Github的小伙伴儿。可以关注本人微信公众号:Java学术趴,发送Spring,免费给发给大家项目源码,代码是经过小编亲自测试的,绝对可靠。免费拿去使用。

本文转载自: 掘金

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

if (你看这篇文章) 你会获得一些if优化技巧,有例为证!

发表于 2021-11-18

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

最近在代码review的过程中发现了一些问题,这些问题虽然不影响逻辑,但是对代码的可读性,安全性留下了隐患。今天这篇文章写在纸上写了好多天,一直放在口袋里,一直工作比较忙没时间写,今天加个班写一下。

if 是每个语言都有的语法,也是最基础的语法。因为代码本来就很晦涩,所以才有了程序员这个中间件,码农。怎么样才能写出好读的,不容易出错的代码?不人为的增加代码的难度呐?这是一个大问题。今天就聊一下我的一些关于 if 思路和总结。

1、if 合并

使用逻辑运算符进行合并if。简单的if 嵌套可以使用&& 进行合并。简单的if else 并且操作相同可以使用 || 进行合并,优化代码逻辑,增加可读性。

注意:逻辑运算符的截断性,if(a >= 10 || b >= 20) 当a>= 10 成立时 是不会判断后面的,只要条件成立,则if 判断结束。

因此简单判断放在最前面,在开发的过程中,将简单的逻辑判断前置,减少判断的次数,可以提高性能,提高性能!

2、将正常的流程放在函数的主干执行

在开发中有些同学会跟着策划写逻辑,将策划的功能描述进行直接转码,造成了很多判断的嵌套。

在这时可以将代码逻辑进行整理,先进行check ,各种不符合条件的判断提前判断并结束函数,最基本的原则就是:将最后的主逻辑留在函数的主干内。

比如下面的场景:玩家需要达到等级100,并且拥有足够的体力,才能够参与活动,否则的话则报错误码。

image.png

可以看到 主逻辑 doSomeThing 在第一处的时候if的嵌套层数是2层,在第二处的时候 嵌套层数 是0 层。在阅读的代码的时候,可以直接顺序读下去,将不太需要关注的分支跳过即可,可读性有了优化,代码更优雅。

3、减少if

  1. 使用三元运算符表达式
    比如在战斗的时候,需要根据玩家的等级进行战斗力的放大,所有有下面的代码,可以看到用三元运算符代码行数较少,而且逼格显然比较高。

image.png

2.使用java8 中流过滤filter ,不使用if
在开发的过程中需要过滤某一个类型的道具,在背包内查找满足需要的道具。

下面的代码传入需要过滤的道具类型,然后在第一处的地方模拟了背包的数据,

第二处是使用if的过滤。

第三处是使用stream 进行过滤,可以看到使用java8 避免了使用if判断,函数式的接口,优雅并且易于理解。

image.png

3、使用枚举

枚举是我们项目里用的很多的一种解决办法,比如我们的资源管理,任务管理,组队管理等等功能,都是使用了枚举解决if else 问题,当你面对的一个同样的问题,只是因为类型的不同的时候,则可以使用枚举进行优化代码。

下面的枚举定义了一个抽象方法,由每个枚举单独实现。

image.png

在使用的时候可以直接使用对应的枚举进行操作,不需要使用if 进行操作,是不是很方便?同时在以后扩展的时候只要增加枚举,不需要修改原来的代码?降低了出bug的风险。

4、使用manager

同样的问题,也可以使用manager进行解决,思路就是将对应类型的处理类先进行注册,等用的时候直接从map中获取,然后使用,避免了if 的判断。

比如上期的资源系统,就是使用了manager 进行的处理。在游戏启动的时候注册了每种资源的处理类,所有的资源类都继承了同样的接口,在使用的时候可以直接调用相同的接口,这样就没必要尽心if 判断。

image.png

image.png

5、使用Consumer

比如战斗的时候,需要根据战斗的类型不同,然后有不同的处理函数,在这时候可以使用Consumer,将每个单独的类型单独处理。避免了使用if

image.png

总结:

if 很简单,但是也有一些技巧。毕竟编程语言的创造者创造简单的if,for 就表示了这世界所有的逻辑。用好if,增加代码可读性,让自己的代码更优雅,Bug 更少。写篇文章不容易,希望能获得大家的关注,点赞,转发 三连,谢谢。

本文转载自: 掘金

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

【力扣-二叉树】9、相同的树(100) 100 相同的树

发表于 2021-11-18

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

100. 相同的树

题目描述

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

1
2
css复制代码输入: p = [1,2,3], q = [1,2,3]
输出: true

示例 2:

1
2
css复制代码输入: p = [1,2], q = [1,null,2]
输出: false

示例 3:

1
2
css复制代码输入: p = [1,2,1], q = [1,1,2]
输出: false

解析

  • 递归法
    • 确定递归函数的参数与返回值
      • bool traversal(TreeNode *P,TreeNode *q)
    • 确定终止递归的条件
      • 两个节点非空且值相等才进行递归
    • 确定单层递归的逻辑
      • 判断两个节点的值是否相等
  • 迭代法
    • 思路相同

递归法

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
c++复制代码// 递归法
class Solution
{
public:
bool isSameTree(TreeNode *p, TreeNode *q)
{
return traversal(p, q);
}

bool traversal(TreeNode *p, TreeNode *q)
{
// 如果两个节点都为空,则返回true
if (p == NULL && q == NULL)
{
return true;
}

// 两个节点都非空
if (p != NULL && q != NULL)
{
// 判断值是否相等
// 相等则进行下一层递归
if (p->val == q->val)
{
bool left = same(p->left, q->left);
bool right = same(p->right, q->right);
if (left && right)
{
return true;
}
}
}

return false;
}
};

迭代法

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
c++复制代码// 迭代法
// 前序遍历
class Solution1
{
public:
bool isSameTree(TreeNode *p, TreeNode *q)
{
if (p == NULL && q == NULL)
{
return true;
}
stack<TreeNode *> *p_st;
stack<TreeNode *> *q_st;

p_st.push(p);
q_st.push(q);

// 迭代遍历
while (!p_st.empty() && !q_st.empty())
{
int pSize = p_st.size();
int qSize = q_st.size();
// 两个栈的大小不同,则两个树就不同
if (pSize != qSize)
{
return false;
}

for (int i = 0; i < pSize; i++)
{
TreeNode *pNode = p_st.top();
p_st.pop();
TreeNode *qNode = q_st.top();
q_st.pop();

// 两个树的节点都为空
if (pNode == NULL && qNode == NULL)
{
continue;
}

// 两个树的结点都非空
if (pNode != NULL && qNode != NULL)
{
if (pNode->val == qNode->val)
{
p_st.push(pNode->left);
p_st.push(pNode->right);

q_st.push(qNode->left);
q_st.push(qNode->right);

continue;
}
}
// 不满足上面的两个条件则返回false
return false;
}
}
// 遍历结束,返回true
return true;
}
};

本文转载自: 掘金

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

深入理解pandas读取excel,txt,csv文件等命令

发表于 2021-11-18

pandas读取文件官方提供的文档

在使用pandas读取文件之前,必备的内容,必然属于官方文档,官方文档查阅地址

pandas.pydata.org/pandas-docs…

文档操作属于pandas里面的Input/Output也就是IO操作,基本的API都在上述网址,接下来本文核心带你理解部分常用的命令

pandas读取txt文件

读取txt文件需要确定txt文件是否符合基本的格式,也就是是否存在\t,,,等特殊的分隔符

一般txt文件长成这个样子

txt文件举例

下面的文件为空格间隔

1
2
3
4
yaml复制代码1 2019-03-22 00:06:24.4463094 中文测试 
2 2019-03-22 00:06:32.4565680 需要编辑encoding
3 2019-03-22 00:06:32.6835965 ashshsh
4 2017-03-22 00:06:32.8041945 eggg

读取命令采用 read_csv或者 read_table都可以

1
2
3
4
5
6
7
bash复制代码import pandas as pd
df = pd.read_table("./test.txt")
print(df)

import pandas as pd
df = pd.read_csv("./test.txt")
print(df)

但是,注意,这个地方读取出来的数据内容为3行1列的DataFrame类型,并没有按照我们的要求得到3行4列

1
2
3
4
5
6
7
python复制代码import pandas as pd
df = pd.read_csv("./test.txt")
print(type(df))
print(df.shape)

<class 'pandas.core.frame.DataFrame'>
(3, 1)

read_csv函数

默认: 从文件、URL、文件新对象中加载带有分隔符的数据,默认分隔符是逗号。

上述txt文档并没有逗号分隔,所以在读取的时候需要增加sep分隔符参数

1
ini复制代码df =  pd.read_csv("./test.txt",sep=' ')

参数说明,官方Source : github.com/pandas-dev/…
中文说明以及重点功能案例

参数 中文释义
filepath_or_buffer 可以是URL,可用URL类型包括:http, ftp, s3和文件,本地文件读取实例:file://localhost/path/to/table.csv
sep str类型,默认’,’ 指定分隔符。如果不指定参数,则会尝试使用默认值逗号分隔。分隔符长于一个字符并且不是‘\s+’,将使用python的语法分析器。并且忽略数据中的逗号。正则表达式例子:’\r\t’
delimiter 定界符,备选分隔符(如果指定该参数,则sep参数失效) 一般不用
delimiter_whitespace True or False 默认False, 用空格作为分隔符等价于spe=’\s+’如果该参数被调用,则delimite不会起作用
header 指定第几行作为列名(忽略注解行),如果没有指定列名,默认header=0; 如果指定了列名header=None
names 指定列名,如果文件中不包含header的行,应该显性表示header=None ,header可以是一个整数的列表,如0,1,3。未指定的中间行将被删除(例如,跳过此示例中的2行)
index_col(案例1) 默认为None 用列名作为DataFrame的行标签,如果给出序列,则使用MultiIndex。如果读取某文件,该文件每行末尾都有带分隔符,考虑使用index_col=False使panadas不用第一列作为行的名称。
usecols 默认None 可以使用列序列也可以使用列名,如 0, 1, 2 or ‘foo’, ‘bar’, ‘baz’ ,使用这个参数可以加快加载速度并降低内存消耗。
squeeze 默认为False, True的情况下返回的类型为Series,如果数据经解析后仅含一行,则返回Series
prefix 自动生成的列名编号的前缀,如: ‘X’ for X0, X1, … 当header =None 或者没有设置header的时候有效
mangle_dupe_cols 默认为True,重复的列将被指定为’X.0’…’X.N’,而不是’X’…’X’。如果传入False,当列中存在重复名称,则会导致数据被覆盖。
dtype 例子: {‘a’: np.float64, ‘b’: np.int32} 指定每一列的数据类型,a,b表示列名
engine 使用的分析引擎。可以选择C或者是python,C引擎快但是Python引擎功能更多一些
converters(案例2) 设置指定列的处理函数,可以用”序号”也可以使用“列名”进行列的指定
true_values / false_values 没有找到实际的应用场景,备注一下,后期完善
skipinitialspace 忽略分隔符后的空格,默认false
skiprows 默认值 None 需要忽略的行数(从文件开始处算起),或需要跳过的行号列表(从0开始)
skipfooter 从文件尾部开始忽略。 (c引擎不支持)
nrows 从文件中只读取多少数据行,需要读取的行数(从文件头开始算起)
na_values 空值定义,默认情况下, ‘#N/A’, ‘#N/A N/A’, ‘#NA’, ‘-1.#IND’, ‘-1.#QNAN’, ‘-NaN’, ‘-nan’, ‘1.#IND’, ‘1.#QNAN’, ‘N/A’, ‘NA’, ‘NULL’, ‘NaN’, ‘n/a’, ‘nan’, ‘null’. 都表现为NAN
keep_default_na 如果指定na_values参数,并且keep_default_na=False,那么默认的NaN将被覆盖,否则添加
na_filter 是否检查丢失值(空字符串或者是空值)。对于大文件来说数据集中没有N/A空值,使用na_filter=False可以提升读取速度。
verbose 是否打印各种解析器的输出信息,例如:“非数值列中缺失值的数量”等。
skip_blank_lines 如果为True,则跳过空行;否则记为NaN。
parse_dates 有如下的操作1. boolean. True -> 解析索引2. list of ints or names. e.g. If 1, 2, 3 -> 解析1,2,3列的值作为独立的日期列;3. list of lists. e.g. If [1, 3] -> 合并1,3列作为一个日期列使用 4. dict, e.g. {‘foo’ : 1, 3} -> 将1,3列合并,并给合并后的列起名为”foo”
infer_datetime_format 如果设定为True并且parse_dates 可用,那么pandas将尝试转换为日期类型,如果可以转换,转换方法并解析。在某些情况下会快5~10倍
keep_date_col 如果连接多列解析日期,则保持参与连接的列。默认为False
date_parser 用于解析日期的函数,默认使用dateutil.parser.parser来做转换。Pandas尝试使用三种不同的方式解析,如果遇到问题则使用下一种方式。1.使用一个或者多个arrays(由parse_dates指定)作为参数;2.连接指定多列字符串作为一个列作为参数;3.每行调用一次date_parser函数来解析一个或者多个字符串(由parse_dates指定)作为参数。
dayfirst DD/MM格式的日期类型
iterator 返回一个TextFileReader 对象,以便逐块处理文件。
chunksize 文件块的大小
compression 直接使用磁盘上的压缩文件。如果使用infer参数,则使用 gzip, bz2, zip或者解压文件名中以‘.gz’, ‘.bz2’, ‘.zip’, or ‘xz’这些为后缀的文件,否则不解压。如果使用zip,那么ZIP包中国必须只包含一个文件。设置为None则不解压。
新版本0.18.1版本支持zip和xz解压
thousands 千分位符号,默认‘,’
decimal 小数点符号,默认‘.’
lineterminator 行分割符,只在C解析器下使用
quotechar 引号,用作标识开始和解释的字符,引号内的分割符将被忽略
quoting 控制csv中的引号常量。可选 QUOTE_MINIMAL (0), QUOTE_ALL (1), QUOTE_NONNUMERIC (2) or QUOTE_NONE (3)
doublequote 双引号,当单引号已经被定义,并且quoting 参数不是QUOTE_NONE的时候,使用双引号表示引号内的元素作为一个元素使用。
escapechar 当quoting 为QUOTE_NONE时,指定一个字符使的不受分隔符限值。
comment 标识着多余的行不被解析。如果该字符出现在行首,这一行将被全部忽略。这个参数只能是一个字符,空行(就像skip_blank_lines=True)注释行被header和skiprows忽略一样。例如如果指定comment=’#’ 解析‘#empty\na,b,c\n1,2,3’ 以header=0 那么返回结果将是以’a,b,c’作为header
encoding 编码方式,指定字符集类型,通常指定为’utf-8’
dialect 如果没有指定特定的语言,如果sep大于一个字符则忽略。具体查看csv.Dialect 文档
error_bad_lines 如果一行包含太多的列,那么默认不会返回DataFrame ,如果设置成false,那么会将改行剔除(只能在C解析器下使用)
warn_bad_lines 如果error_bad_lines =False,并且warn_bad_lines =True 那么所有的“bad lines”将会被输出(只能在C解析器下使用)
low_memory 分块加载到内存,再低内存消耗中解析。但是可能出现类型混淆。确保类型不被混淆需要设置为False。或者使用dtype 参数指定类型。注意使用chunksize 或者iterator 参数分块读入会将整个文件读入到一个Dataframe,而忽略类型(只能在C解析器中有效)
delim_whitespace New in version 0.18.1: Python解析器中有效
memory_map 如果为filepath_or_buffer提供了文件路径,则将文件对象直接映射到内存上,并直接从那里访问数据。使用此选项可以提高性能,因为不再有任何I / O开销,使用这种方式可以避免文件再次进行IO操作
float_precision 指定C引擎应用于浮点值的转换器

该表格部分参考 博客 www.cnblogs.com/datablog/p/… 感谢博主的翻译,O(∩_∩)O哈哈~

案例1

index_col 使用

首先准备一个txt文件,这个文件最大的问题是在每行的末尾多了一个’,’ ,按照提示解释为,如果每行末尾都有分隔符,会出现问题,但是在实际测试的时候发现需要配合names参数,才可以出现效果

1
2
3
bash复制代码goof,1,2,3,ddd,
u,1,3,4,asd,
as,df,12,33,

该表格部分参考 博客 www.cnblogs.com/datablog/p/… 感谢博主的翻译,O(∩_∩)O哈哈~

案例1

index_col 使用

首先准备一个txt文件,这个文件最大的问题是在每行的末尾多了一个’,’ ,按照提示解释为,如果每行末尾都有分隔符,会出现问题,但是在实际测试的时候发现需要配合names参数,才可以出现效果

1
2
3
bash复制代码goof,1,2,3,ddd,
u,1,3,4,asd,
as,df,12,33,

编写如下代码

1
2
3
4
5
bash复制代码df =  pd.read_csv("./demo.txt",header=None,names=['a','b','c','d','e'])
print(df)

df = pd.read_csv("./demo.txt",header=None,index_col=False,names=['a','b','c','d','e'])
print(df)

其实发现意义还真不是很大,可能文档并没有表述清楚他的具体作用。接下来说一下index_col的常见用途

在读取文件的时候,如果不设置index_col列索引,默认会使用从0开始的整数索引。当对表格的某一行或列进行操作之后,在保存成文件的时候你会发现总是会多一列从0开始的列,如果设置index_col参数来设置列索引,就不会出现这种问题了。

案例2

converters 设置指定列的处理函数,可以用”序号”也可以使用“列名”进行列的指定

1
2
3
4
5
6
7
8
9
kotlin复制代码import pandas as pd

def fun(x):
return str(x)+"-haha"

df = pd.read_csv("./test.txt",sep=' ',header=None,index_col=0,converters={3:fun})
print(type(df))
print(df.shape)
print(df)

read_csv函数过程中常见的问题

  1. 有的IDE中利用Pandas的read_csv函数导入数据文件时,若文件路径或文件名包含中文,会报错。
    解决办法
1
2
3
4
ini复制代码import pandas as pd
#df=pd.read_csv('F:/测试文件夹/测试数据.txt')
f=open('F:/测试文件夹/测试数据.txt')
df=pd.read_csv(f)
  1. 排除某些行 使用 参数 skiprows.它的功能为排除某一行。 要注意的是:排除前3行是skiprows=3 排除第3行是skiprows=3
  2. 对于不规则分隔符,使用正则表达式读取文件 文件中的分隔符采用的是空格,那么我们只需要设置sep=” “来读取文件就可以了。当分隔符并不是单个的空格,也许有的是一个空格有的是多个空格时,如果这个时候还是采用sep=” “来读取文件,也许你就会得到一个很奇怪的数据,因为它会将空格也做为数据。 data = pd.read_csv(“data.txt”,sep=”\s+”)
  3. 读取的文件中如果出现中文编码错误 需要设定 encoding 参数
  4. 为行和列添加索引 用参数names添加列索引,用index_col添加行索引

read_csv该命令有相当数量的参数。大多数都是不必要的,因为你下载的大部分文件都有标准格式。

read_table函数

基本用法是一致的,区别在于separator分隔符。

csv是逗号分隔值,仅能正确读入以 “,” 分割的数据,read_table默认是’\t’(也就是tab)切割数据集的

read_fwf 函数

读取具有固定宽度列的文件,例如文件

1
2
3
4
5
复制代码id8141    360.242940   149.910199   11950.7
id1594 444.953632 166.985655 11788.4
id1849 364.136849 183.628767 11806.2
id1230 413.836124 184.375703 11916.8
id1948 502.953953 173.237159 12468.3

read_fwf 命令有2个额外的参数可以设置

colspecs :

需要给一个元组列表,元组列表为半开区间,[from,to) ,默认情况下它会从前100行数据进行推断。

例子:

1
2
3
ini复制代码import pandas as pd
colspecs = [(0, 6), (8, 20), (21, 33), (34, 43)]
df = pd.read_fwf('demo.txt', colspecs=colspecs, header=None, index_col=0)

widths:

直接用一个宽度列表,可以代替colspecs参数

1
2
ini复制代码widths = [6, 14, 13, 10]
df = pd.read_fwf('demo.txt', widths=widths, header=None)

read_fwf 使用并不是很频繁,可以参照 pandas.pydata.org/pandas-docs… 学习

read_msgpack 函数

pandas支持的一种新的可序列化的数据格式,这是一种轻量级的可移植二进制格式,类似于二进制JSON,这种数据空间利用率高,在写入(序列化)和读取(反序列化)方面都提供了良好的性能。

read_clipboard 函数

读取剪贴板中的数据,可以看作read_table的剪贴板版本。在将网页转换为表格时很有用

这个地方出现如下的BUG

module ‘pandas’ has no attribute ‘compat’

我更新了一下pandas 既可以正常使用了

还有一个比较坑的地方,就是在读取剪切板的时候,如果复制了中文,很容易读取不到数据

解决办法

  1. 打开site-packages\pandas\io\clipboard.py 这个文件需要自行检索
  2. 在 text = clipboard_get() 后面一行 加入这句: text = text.decode(‘UTF-8’)
  3. 保存,然后就可以使用了

read_excel 函数

依旧是官方文档一码当先:pandas.pydata.org/pandas-docs…

参数 中文释义
io 文件类对象 ,pandas Excel 文件或 xlrd 工作簿。该字符串可能是一个URL。URL包括http,ftp,s3和文件。例如,本地文件可写成file://localhost/path/to/workbook.xlsx
sheet_name 默认是sheetname为0,返回多表使用sheetname=0,1,若sheetname=None是返回全表 。注意:int/string返回的是dataframe,而none和list返回的是dict of dataframe,表名用字符串表示,索引表位置用整数表示;
header 指定作为列名的行,默认0,即取第一行,数据为列名行以下的数据;若数据不含列名,则设定 header = None;
names 指定列的名字,传入一个list数据
index_col 指定列为索引列,也可以使用u”strings” ,如果传递一个列表,这些列将被组合成一个MultiIndex。
squeeze 如果解析的数据只包含一列,则返回一个Series
dtype 数据或列的数据类型,参考read_csv即可
engine 如果io不是缓冲区或路径,则必须将其设置为标识io。 可接受的值是None或xlrd
converters 参照read_csv即可
其余参数 基本和read_csv一致

pandas 读取excel文件如果报错,一般处理为

错误为:ImportError: No module named ‘xlrd’

pandas读取excel文件,需要单独的xlrd模块支持 pip install xlrd 即可

read_json 函数

参数 中文释义
path_or_buf 一个有效的JSON文件,默认值为None,字符串可以为URL,例如file://localhost/path/to/table.json
orient (案例1) 预期的json字符串格式,orient的设置有以下几个值:1. ‘split’ : dict like {index -> index, columns -> columns, data -> values}2. ‘records’ : list like {column -> value}, … , {column -> value}3. ‘index’ : dict like {index -> {column -> value}}4. ‘columns’ : dict like {column -> {index -> value}}5. ‘values’ : just the values array
typ 返回的格式(series or frame), 默认是 ‘frame’
dtype 数据或列的数据类型,参考read_csv即可
convert_axes boolean,尝试将轴转换为正确的dtypes,默认值为True
convert_dates 解析日期的列列表;如果为True,则尝试解析类似日期的列,默认值为True参考列标签it ends with ‘_at’,it ends with ‘_time’,it begins with ‘timestamp’,it is ‘modified’,it is ‘date’
keep_default_dates boolean,default True。如果解析日期,则解析默认的日期样列
numpy 直接解码为numpy数组。默认为False;仅支持数字数据,但标签可能是非数字的。还要注意,如果numpy=True,JSON排序MUST
precise_float boolean,默认False。设置为在将字符串解码为双精度值时启用更高精度(strtod)函数的使用。默认值(False)是使用快速但不太精确的内置功能
date_unit string,用于检测转换日期的时间戳单位。默认值无。默认情况下,将检测时间戳精度,如果不需要,则通过’s’,’ms’,’us’或’ns’之一分别强制时间戳精度为秒,毫秒,微秒或纳秒。
encoding json编码
lines 每行将文件读取为一个json对象。

如果JSON不可解析,解析器将产生ValueError/TypeError/AssertionError之一。

案例1

  1. orient=’split’
1
2
3
less复制代码    import pandas as pd
s = '{"index":[1,2,3],"columns":["a","b"],"data":[[1,3],[2,5],[6,9]]}'
df = pd.read_json(s,orient='split')
  1. orient=’records’ 成员为字典列表
1
2
3
ini复制代码import pandas as pd
s = '[{"a":1,"b":2},{"a":3,"b":4}]'
df = pd.read_json(s,orient='records')
  1. orient=’index’ 以索引为key,以列字段构成的字典为键值。如: s = ‘{“0”:{“a”:1,”b”:2},”1”:{“a”:2,”b”:4}}’
  2. orient=’columns’ 或者 values 自己推断即可

部分中文翻译,可以参考github> github.com/apachecn/pa…

read_json()常见BUG

读取json文件出现 ValueError: Trailing data ,JSON格式问题

原格式为

1
css复制代码{"a":1,"b":1},{"a":2,"b":2}

调整为

1
css复制代码[{"a":1,"b":1},{"a":2,"b":2}]

或者使用lines参数,并且JSON调整为每行一条数据

1
2
css复制代码{"a":1,"b":1}
{"a":2,"b":2}

若JSON文件中有中文,建议加上encoding参数,赋值’utf-8’,否则会报错

read_html 函数

参数 中文释义
io 接收网址、文件、字符串。网址不接受https,尝试去掉s后爬去
match 正则表达式,返回与正则表达式匹配的表格
flavor 解析器默认为‘lxml’
header 指定列标题所在的行,list为多重索引
index_col 指定行标题对应的列,list为多重索引
skiprows 跳过第n行(序列标示)或跳过n行(整数标示)
attrs 属性,比如 attrs = {‘id’: ‘table’}
parse_dates 解析日期

使用方法,在网页中右键如果发现表格 也就是 table 即可使用

例如: data.stcn.com/2019/0304/1…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<table class="..." id="...">
<thead>
<tr>
<th>...</th>
</tr>
</thead>
<tbody>
<tr>
<td>...</td>
</tr>
<tr>...</tr>
</tbody>
</table>


<table> : 定义表格
<thead> : 定义表格的页眉
<tbody> : 定义表格的主体
<tr> : 定义表格的行
<th> : 定义表格的表头
<td> : 定义表格单元

常见BUG

出现如下报错 ImportError: html5lib not found, please install it

安装html5lib即可,或者使用参数

1
2
ini复制代码import pandas as pd
df = pd.read_html("http://data.stcn.com/2019/0304/14899644.shtml",flavor ='lxml')

更多参考源码,可以参考 > pandas.pydata.org/pandas-docs…

pandas 的读写函数简表

读取函数 写入函数 解释
read_clipboard to_clipboard 从剪贴板中读取文本并传递给read_table
read_csv to_csv 将CSV(逗号分隔)文件读入DataFrame
read_excel to_excel Excel表格
read_sql to_sql
read_pickle to_pickle
read_json to_json
read_msgpack to_msgpack
read_stata to_stata
read_gbq to_gbq 从Google BigQuery加载数据
read_hdf to_hdf
read_html to_html
read_parquet to_parquet
read_feather to_feather
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
python复制代码import pandas as pd
2
3 csvframe = pd.read_csv('pandas_data_test\myCSV_01.csv')
4 print(csvframe, "\n-----*-----")
5 csvframe1 = pd.read_table('pandas_data_test\myCSV_01.csv',sep=',')
6 print(csvframe1, "\n-----*-----")
7 csvframe2 = pd.read_csv('pandas_data_test\myCSV_02.csv',header=None) # 设置header为无,就不会用数据充当表头,此时添加默认表头
8 print(csvframe2, "\n-----*-----")
9 csvframe20 = pd.read_csv('pandas_data_test\myCSV_02.csv',names=['white','red','blue','green','animal']) # 指定表头
10 print(csvframe20, "\n-----*-----")
11
12 csvframe30 = pd.read_csv('pandas_data_test\myCSV_03.csv')
13 print(csvframe30, "\n-----*-----")
14 csvframe31 = pd.read_csv('pandas_data_test\myCSV_03.csv',index_col=['color','status']) #等级索引
15 print(csvframe31, "\n-----*-----")
16
17 txtframe4 = pd.read_table('pandas_data_test\ch05_04.txt',sep='\s+') # 根据正则解析
18 print(txtframe4, "\n-----*-----")
19 txtframe5 = pd.read_table('pandas_data_test\ch05_05.txt',sep=r'\D+',header=None,engine='python')
20 print(txtframe5, "\n-----*-----")
21 # 使用skiprows选项,可以排除多余的行。把要排除的行的行号放到数组中,赋给该选项即可。
22 txtframe6 = pd.read_table('pandas_data_test\ch05_06.txt',sep=',',skiprows=[0,1,3,6])
23 print(txtframe6)
24 Out[1]: 25 white red blue green animal
26 0 1 5 2 3 cat
27 1 2 7 8 5 dog
28 2 3 3 6 7 horse
29 3 2 2 8 3 duck
30 4 4 4 2 1 mouse
31 5 4 4 2 1 mou
32 -----*-----
33 white red blue green animal
34 0 1 5 2 3 cat
35 1 2 7 8 5 dog
36 2 3 3 6 7 horse
37 3 2 2 8 3 duck
38 4 4 4 2 1 mouse
39 5 4 4 2 1 mou
40 -----*-----
41 0 1 2 3 4
42 0 1 5 2 3 cat
43 1 2 7 8 5 dog
44 2 3 3 6 7 horse
45 3 2 2 8 3 duck
46 4 4 4 2 1 mouse
47 -----*-----
48 white red blue green animal
49 0 1 5 2 3 cat
50 1 2 7 8 5 dog
51 2 3 3 6 7 horse
52 3 2 2 8 3 duck
53 4 4 4 2 1 mouse
54 -----*-----
55 color status iteml item2 item3
56 0 black up 3 4 6
57 1 black down 2 6 7
58 2 white up 5 5 5
59 3 white down 3 3 2
60 4 white left 1 2 1
61 5 red up 2 2 2
62 6 red down 1 1 4
63 -----*-----
64 iteml item2 item3
65 color status
66 black up 3 4 6
67 down 2 6 7
68 white up 5 5 5
69 down 3 3 2
70 left 1 2 1
71 red up 2 2 2
72 down 1 1 4
73 -----*-----
74 white red blue green
75 0 1 5 2 3
76 1 2 7 8 5
77 2 3 3 6 7
78 -----*-----
79 0 1 2
80 0 0 123 122
81 1 1 124 321
82 2 2 125 333
83 -----*-----
84 white red blue green animal
85 0 1 5 2 3 cat
86 1 2 7 8 5 dog
87 2 3 3 6 7 horse
88 3 2 2 8 3 duck
89 4 4 4 2 1 mouse

复制代码

从TXT文件读取部分数据

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码 1 print(csvframe2, "\n-----*-----")
2 # nrows=2指定要获取的行数,skiprows=[2]删除对应行
3 csvfram20 = pd.read_csv('pandas_data_test\myCSV_02.csv',skiprows=[2],nrows=2,header=None)
4 print(csvfram20)
5 Out[2]: 6 0 1 2 3 4
7 0 1 5 2 3 cat
8 1 2 7 8 5 dog
9 2 3 3 6 7 horse
10 3 2 2 8 3 duck
11 4 4 4 2 1 mouse
12 -----*-----
13 0 1 2 3 4
14 0 1 5 2 3 cat
15 1 2 7 8 5 dog

复制代码

  另外一项既有趣又很常用的操作是切分想要解析的文本,然后遍历各个部分,逐一对其执行 某一特定操作。

  例如,对于一列数字,每隔两行取一个累加起来,最后把和插人到Series对象中„这个小例 子理解起来很简单,

    也没有实际应用价值,但是一旦领会了其原理,你就能将其用到更加复杂的情况。

复制代码

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
ini复制代码 1 csvframe1 = pd.read_table('pandas_data_test\myCSV_01.csv',sep=',')
2 print(csvframe1, "\n-----*-----")
3 out = pd.Series()
4 pieces = pd.read_csv('pandas_data_test\myCSV_01.csv',chunksize=4) # chunksize参数决定了每部分分割的行数
5 i = 0
6 for piece in pieces:
7 print(piece['white'])
8 out.at[i] = piece['white'].sum()
9 i += 1
10 print(out, "\n-----*-----")
11 Out[3]: 12 white red blue green animal
13 0 1 5 2 3 cat
14 1 2 7 8 5 dog
15 2 3 3 6 7 horse
16 3 2 2 8 3 duck
17 4 4 4 2 1 mouse
18 5 4 4 2 1 mou
19 -----*-----
20 0 1
21 1 2
22 2 3
23 3 2
24 Name: white, dtype: int64
25 4 4
26 5 4
27 Name: white, dtype: int64
28 0 8
29 1 8
30 dtype: int64

复制代码

往CSV文件写入数据

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码 1 print(csvframe1)
2 print(csvframe1.to_csv('pandas_data_test\ch05_07.csv'))
3 # 使用index和 header选项,把它们的值设置为False,可取消默认写入index和header
4 print(csvframe1.to_csv('pandas_data_test\ch05_07b.csv',index =False,header=False))
5 print(csvframe30.to_csv('pandas_data_test\ch05_08.csv'))
6 # 可以用to_csv()函数的na_rep选项把空字段替换为你需要的值。常用值有NULL、0和NaN
7 print(csvframe30.to_csv('pandas_data_test\ch05_09.csv',na_rep="空"))
8 Out[4]: 9 white red blue green animal
10 0 1 5 2 3 cat
11 1 2 7 8 5 dog
12 2 3 3 6 7 horse
13 3 2 2 8 3 duck
14 4 4 4 2 1 mouse
15 5 4 4 2 1 mou
16 None
17 None
18 None
19 None

进入文件夹我们可以看到相应的文件:

   

读写HTML文件

复制代码

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
less复制代码 1 frame = pd.DataFrame(np.arange(4).reshape(2,2))
2 print(frame.to_html())
3 frame2 = pd.DataFrame( np.random.random((4,4)),index = ['white','black','red','blue1'],columns = ['up','down','right','left'])
4 s = ['<HTML>']
5 s.append('<HEAD><TITLE>My DataFrame</TITLE></HEAD>')
6 s.append(' <B0DY>')
7 s.append(frame.to_html())
8 s.append('</BODY></HTML>')
9 html = ''.join(s)
10 html_file = open('pandas_data_test\myFrame.html','w')
11 html_file.write(html)
12 html_file.close()
13 web_frames = pd.read_html('pandas_data_test\myFrame.html')
14 print(web_frames[0])
15 ranking = pd.read_html('http://www.meccanismocomplesso.org/en/ eccanismo-complesso-sito-2/classifica-punteggio/')
16 print(ranking[0][1:10]) # 输出网页内容的前10行
17 Out[5]: 18 <table border="1" class="dataframe">
19 <thead>
20 <tr style="text-align: right;">
21 <th></th>
22 <th>0</th>
23 <th>1</th>
24 </tr>
25 </thead>
26 <tbody>
27 <tr>
28 <th>0</th>
29 <td>0</td>
30 <td>1</td>
31 </tr>
32 <tr>
33 <th>1</th>
34 <td>2</td>
35 <td>3</td>
36 </tr>
37 </tbody>
38 </table>
39 Unnamed: 0 0 1
40 0 0 0 1
41 1 1 2 3
42 # Nome Exp Livelli
43 1 2 admin 9029 NaN
44 2 3 BrunoOrsini 2124 NaN
45 3 4 Berserker 700 NaN
46 4 5 Dnocioni 543 NaN
47 5 6 albertosallusti 409 NaN
48 6 7 Jon 233 NaN
49 7 8 Mr.Y 180 NaN
50 8 9 michele sisinni 157 NaN
51 9 10 Selina 136 NaN

复制代码

从XML读取数据

  pandas的所有I/O API函数中,没有专门用来处理XML(可扩展标记语言)格式的。虽然没有, 但这种格式其实

    很重要,因为很多结构化数据都是以XML格式存储的。pandas没有专门的处理函 数也没关系,因为Python

    有很多读写XML格式数据的库(除了pandas)。其中一个库叫作lxml,它在大文件处理方面性能优异,因而从

    众多同类库之中脱颖而出。这 一节将介绍如何用它处理XML文件,以及如何把它和pandas整合起来,以最

    终从XML文件中获 取到所需数据并将其转换为DataFrame对象。

  XML源文件如下图所示

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码 1 from lxml import objectify
2
3 xml = objectify.parse('pandas_data_test\books.xml')
4 root = xml.getroot() # 获取根节点
5 print(root.Book.Author)
6 mes1 = root.Book.getchildren()
7 print("root.Book.getchildren()获取的子标签内容:\n", mes1)
8 mes2 = root.Book[1].getchildren() # 取第二个Book标签
9 print([child.tag for child in mes2]) # 获取子标签
10 print([child.text for child in mes2]) # 获取的子标签内容
11 Out[6]: 12 272103_l_EnRoss, Mark
13 root.Book.getchildren()获取的子标签内容:
14 [' 272103_l_EnRoss, Mark', 'XML Cookbook', 'Computer', 23.56, '2014-22-0l']
15 ['Author', 'Title', 'Genre', 'Price', 'PublishDate']
16 [' 272l03_l_EnBracket, Barbara', 'XML for Dummies', 'Computer', '35.95', '20l4-l2-l6']

复制代码

读写 Microsoft Excel文件

  read_excel()、to_excel(),能够读取.xls和.xlsx两种类型的文件。

读写JSON数据

read_json()、to_json()

HDF5格式

至此,已学习了文本格式的读写。若要分析大量数据,最好使用二进制格式。Python有多 种二进制数据处理

    工具。HDF5库在这个方面取得了一定的成功。HDF代表等级数据格式(hierarchical data format )。HDF5

    库关注的是HDF5文件的读写,这种文件的数据结构由节点组成,能够存储大量数据集。该库全部用c语言

    开发,提供了python/matlab和Java语言接口。它的迅速扩展得益于开发人 员的广泛使用,还得益于它的效

    率,尤其是使用这种格式存储大量数据,其效率很高。比起其他处理起二进制数据更为简单的格式,HDF5

    支持实时压缩,因而能够利用数据结构中的重复模式压缩文件。目前,Python提供两种操纵HDF5格式数据

    的方法:PyTables和h5py。这两种方法有几点不同,选用哪一种很大程度上取决于具体需求。

  h5py为HDF5的高级API提供接口。PyTables封装了很多HDF5细节,提供更加灵活的数据容器、索引表、搜索

    功能和其他计算相关的介质。pandas还有一个叫作HDFStore、类似于diet的类,它用PyTables存储pandas

    对象。使用HDF5格式之前,必须导人HDFStore类。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码 1 from pandas.io.pytables import HDFStore
2 # 注意这里需要tables这个包,没有请自行安装
3 frame = pd.DataFrame(np.arange(16).reshape(4,4),index=['white','black1','red','blue'],columns=['up','down','right','left'])
4 store = HDFStore('pandas_data_test\mydata.h5')
5 store['obj1'] = frame
6 frame1 = pd.DataFrame(np.random.rand(16).reshape(4,4),index=['white','black1','red','blue'],columns=['up','down','right','left'])
7 store['obj2'] = frame1
8 print(store['obj1'])
9 print(store['obj2'])
10 Out[7]: 11 up down right left
12 white 0 1 2 3
13 black1 4 5 6 7
14 red 8 9 10 11
15 blue 12 13 14 15
16 up down right left
17 white 0.251269 0.422823 0.619294 0.273534
18 black1 0.593960 0.353969 0.966026 0.104581
19 red 0.964577 0.625644 0.342923 0.638627
20 blue 0.246541 0.997952 0.414599 0.908750
21 Closing remaining open files:pandas_data_test\mydata.h5...done

复制代码

实现对象序列化

****pickle模块实现了一个强大的算法,能够对用Python实现的数据结构进行序列化(pickling) 和反序列化操作。

  序列化是指把对象的层级结构转换为字节流的过程。序列化便于对象的传输、存储和重建,仅用接收器就能重

  建对象,还能保留它的所有原始特征。

  用pandas库实现对象序列化(反序列化)很方便,所有工具都是现成的,无需在Python会话中导入cPickle模

  块,所有的操作都是隐式进行的。 pandas的序列化格式并不是完全使用ASCII编码。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码 1 import pickle
2 data = { 'color': ['white','red'], 'value': [5, 7]}
3 pickled_data = pickle.dumps(data)
4 print(pickled_data)
5 nframe = pickle.loads(pickled_data)
6 print(nframe)
7
8 # 用pandas序列化
9 frame = pd.DataFrame(np.arange(16).reshape(4,4), index = ['up','down','left','right'])
10 frame.to_pickle('pandas_data_test\frame.pkl') # 同json数据类似
11 print(pd.read_pickle('pandas_data_test\frame.pkl')) # 反序列化,读取数据
12 Out[8]: 13 b'\x80\x03}q\x00(X\x05\x00\x00\x00colorq\x01]q\x02(X\x05\x00\x00\x00whiteq\x03X\x03\x00\x00\x00redq\x04eX\x05\x00\x00\x00valueq\x05]q\x06(K\x05K\x07eu.'
14 {'color': ['white', 'red'], 'value': [5, 7]}
15 0 1 2 3
16 up 0 1 2 3
17 down 4 5 6 7
18 left 8 9 10 11
19 right 12 13 14 15

复制代码

对接数据库

****在很多应用中,所使用的数据来自于文本文件的很少,因为文本文件不是存储数据最有效的方式。

  数据往往存储于SQL类关系型数据库,作为补充,NoSQL数据库近来也已流行开来。

  从SQL数据库加载数据,将其转换为DataFrame对象很简单pandas提供的几个函数简化了该过程。

  pandas.io.sql模块提供独立于数据库、叫作sqlalchemy的统一接口。该接口简化了连接模式, 不管对于

什么类型的数据库,操作命令都只有一套。连接数据库使用create_engine()函数,你可以用它配置驱动器所

需的用户名、密码、端口和数据库实例等所有属性。 数据库URL的典型形式是:

                dialect+driver://username:password@host:port/database

名称的标识名称,例如sqlite,mysql,postgresql,oracle,或mssql。drivername是用于使用全小写字母连接

到数据库的DBAPI的名称。如果未指定,则将导入“默认”DBAPI(如果可用) - 此默认值通常是该后端可用的

最广泛的驱动程序。

复制代码

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
ini复制代码 1 from sqlalchemy import create_engine
2
3 # PostgreSQL数据库
4 # default
5 engine = create_engine('postgresql://scott:tiger@localhost/mydatabase')
6 # pg8000 驱动器
7 engine = create_engine('postgresql+pg8000://scott:tiger@localhost/mydatabase')
8 # psycopg2 驱动器
9 engine = create_engine('postgresql+psycopg2://scott:tiger@localhost/mydatabase')
10 # MySql
11 # default
12 engine = create_engine('mysql://scott:tiger@localhost/foo')
13
14 # mysql-python 注意驱动器后面的对应关系
15 engine = create_engine('mysql+mysqldb://scott:tiger@localhost/foo')
16
17 # MySQL-connector-python
18 engine = create_engine('mysql+mysqlconnector://scott:tiger@localhost/foo')
19 # OurSQL
20 engine = create_engine('mysql+oursql://scott:tiger@localhost/foo')
21 # Oracle
22 engine = create_engine('oracle://scott:tiger@127.0.0.1:1521/sidname')
23
24 engine = create_engine('oracle+cx_oracle://scott:tiger@tnsname')
25 # Microsoft SQL
26 # pyodbc
27 engine = create_engine('mssql+pyodbc://scott:tiger@mydsn')
28
29 # pymssql
30 engine = create_engine('mssql+pymssql://scott:tiger@hostname:port/dbname')

复制代码

  SQLite:

    由于SQLite连接到本地文件,因此URL格式略有不同。URL的“文件”部分是数据库的文件名。

    对于相对文件路径,这需要三个斜杠:engine = create_engine(‘sqlite:///foo.db’)

****对于绝对文件路径,三个斜杠后面是绝对路径:

      Unix/Mac - 4 initial slashes in total

      engine = create_engine(‘sqlite:absolute/path/to/foo.db’)

**Windows

      engine = create_engine(‘sqlite:///C:\path\to\foo.db’)

      Windows alternative using raw string

      engine = create_engine(r’sqlite:///C:\path\to\foo.db’)

  SQLite3数据读写

****学习使用Python内置的SQLite数据库sqlite3。SQLite3工具实现了简单、 轻量级的DBMS SQL,

  因此可以内置于用Python语言实现的任何应用。它很实用,你可以在单个文件中创建一个嵌入式数据库。

    若想使用数据库的所有功能而又不想安装真正的数据库,这个工具就是最佳选择。若想在使用真正

  的数据库之前练习数据库操作,或在单一程序中使用数据库存储数据而无需考虑接口, SQLite3都是不

  错的选择。

复制代码

1
2
3
4
5
6
7
8
9
10
11
python复制代码 1 from sqlalchemy import create_engine
2 frame = pd.DataFrame( np.arange(20).reshape(4,5),columns=['white','red','blue','black','green'])
3 # 连接SQLite3数据库
4 engine = create_engine('sqlite:///pandas_data_test/foo.db')
5 # 把DataFrame转换为数据库表。
6 # to_sql(self, name, con, schema=None, if_exists='fail', index=True,
7 # index_label=None, chunksize=None, dtype=None)
8 frame.to_sql('colors',engine)
9
10 # 读取
11 print(pd.read_sql('colors',engine))

复制代码

  运行结果:

  

本文转载自: 掘金

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

ElasticSearch 底层原理与分组查询 一、Elas

发表于 2021-11-18

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

一、ElasticSearch 文档分值 _score 计算底层原理

1)boolean model

根据用户的query条件,先过滤出包含指定 term(关键字) 的 doc(文档)

  • query “hello world” ‐‐> hello / world / hello & world
  • bool ‐‐> must/must not/should ‐‐> 过滤 ‐‐> 包含 / 不包含 / 可能包含
  • doc ‐‐> 不打分数 ‐‐> 正或反 true or false ‐‐> 为了减少后续要计算的doc的数量,提升性能

2)relevance score算法

简单来说,就是计算出,一个索引中的文本,与搜索文本, 他们之间的关联匹配程度

Elasticsearch使用的是 term frequency/inverse document frequency 算法,简称为 TF/IDF算法

Term frequency:搜索文本中的各个词条在field文本中出现了多少次,出现次数越多,就越相关
搜索请求:hello world

  • doc1:hello you, and world is very good
  • doc2:hello, how are you
  • Inverse document frequency:搜索文本中的各个词条在整个索引的所有文档中出现了多少次,出现的次数越多,就越不相关*

搜索请求:hello world

  • doc1:hello, tuling is very good
  • doc2:hi world, how are you

比如说,在index中有1万条document,hello这个单词在所有的document中,一共出现
了1000次;world这个单词在所有的document中,一共出现了100次
​

Field-length norm:field长度,field越长,相关度越弱

搜索请求:hello world

  • doc1:{ “title”: “hello article”, “content”: “…… N个单词” }
  • doc2:{ “title”: “my article”, “content”: “…… N个单词,hi world” }

hello world在整个index中出现的次数是一样多的
doc1更相关,title field更短

2、分析一个document上的_score是如何被计算出来的

1
2
3
4
5
6
7
8
shell复制代码GET /es_db/_doc/1/_explain 
{
"query": {
"match": {
"remark": "java developer"
}
}
}

二、分词器工作流程

1、切分词语,normalization

给你一段句子,然后将这段句子拆分成一个一个的单个的单词,同时对每个单词进行
normalization(时态转换,单复数转换),分词器
recall,召回率:搜索的时候,增加能够搜索到的结果的数量

1
2
3
4
5
6
7
8
9
10
shell复制代码character filter:在一段文本进行分词之前,先进行预处理,比如说最常见的就是,过滤html 
标签(
<span>hello<span> ‐‐> hello),& ‐‐> and(I&you ‐‐> I and you)

tokenizer:分词,hello you and me ‐‐> hello, you, and, me

token filter:lowercase,stop word,synonymom,liked ‐‐> like,Tom ‐‐> tom,a/th
e/an ‐‐> 干掉,small ‐‐> little
# 一个分词器,很重要,将一段文本进行各种处理,最后处理好的结果才会拿去建立倒
排索引

2、内置分词器的介绍

Set the shape to semi‐transparent by calling set_trans(5)

standard analyzer:set, the, shape, to, semi, transparent, by, calling, set_trans, 5(默认的是standard)

simple analyzer:set, the, shape, to, semi, transparent, by, calling, set, tran

whitespace analyzer:Set, the, shape, to, semi‐transparent, by, calling, set_trans(5)

stop analyzer:移除停用词,比如a the it等等

测试:

1
2
3
4
5
shell复制代码POST _analyze 
{
"analyzer":"standard",
"text":"Set the shape to semi‐transparent by calling set_trans(5)"
}

3、定制分词器

1)默认的分词器

standard
standard tokenizer:以单词边界进行切分
standard token filter:什么都不做
lowercase token filter:将所有字母转换为小写
stop token filer(默认被禁用):移除停用词,比如a the it等等

2)修改分词器的设置启用english停用词token filter

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
shell复制代码PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"es_std": {
"type": "standard",
"stopwords": "_english_"
}
}
}
}
}

GET /my_index/_analyze
{
"analyzer": "standard",
"text": "a dog is in the house"
}

GET /my_index/_analyze
{
"analyzer": "es_std",
"text": "a dog is in the house"
}

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
bash复制代码PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": [
"&=> and"
]
}
},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": [
"the",
"a"
]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": [
"html_strip",
"&_to_and"
],
"tokenizer": "standard",
"filter": [
"lowercase",
"my_stopwords"
]
}
}
}
}
}

GET /my_index/_analyze
{
"text": "tom&jerry are a friend in the house, <a>, HAHA!!",
"analyzer": "my_analyzer"
}

PUT /my_index/_mapping/my_type
{
"properties": {
"content": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}

3)ik分词器详解

ik配置文件地址:es/plugins/ik/config目录
IKAnalyzer.cfg.xml:用来配置自定义词库
main.dic:ik原生内置的中文词库,总共有27万多条,只要是这些单词,都会被分在
一起quantifier.dic:放了一些单位相关的词
suffix.dic:放了一些后缀
surname.dic:中国的姓氏
stopword.dic:英文停用词
ik原生最重要的两个配置文件
main.dic:包含了原生的中文词语,会按照这个里面的词语去分词
stopword.dic:包含了英文的停用词
停用词,stopword
a the and at but
一般,像停用词,会在分词的时候,直接被干掉,不会建立在倒排索引中

4)IK分词器自定义词库

(1)自己建立词库:每年都会涌现一些特殊的流行词,网红,蓝瘦香菇,喊麦,鬼畜,一般不会在ik的原生词典里
自己补充自己的最新的词语,到ik的词库里面去
IKAnalyzer.cfg.xml:ext_dict,custom/mydict.dic
补充自己的词语,然后需要重启es,才能生效
(2)自己建立停用词库:比如了,的,啥,么,我们可能并不想去建立索引,
让人家搜索
custom/ext_stopword.dic,已经有了常用的中文停用词,可以补充自己的停用词,然后重启es1 IK分词器源码下载:
github.com/medcl/elast…

5)IK热更新

每次都是在es的扩展词典中,手动添加新词语,很坑
(1)每次添加完,都要重启es才能生效,非常麻烦
(2)es是分布式的,可能有数百个节点,你不能每次都一个一个节点上面去修改
es不停机,直接我们在外部某个地方添加新的词语,es中立即热加载到这些新词语
IKAnalyzer.cfg.xml

1
2
3
4
5
6
7
8
9
10
11
shell复制代码<properties> 
<comment>IK Analyzer 扩展配置</comment>
<!‐‐用户可以在这里配置自己的扩展字典 ‐‐>
<entry key="ext_dict">location</entry>
<!‐‐用户可以在这里配置自己的扩展停止词字典‐‐>
<entry key="ext_stopwords">location</entry>
<!‐‐用户可以在这里配置远程扩展字典 ‐‐>
<entry key="remote_ext_dict">words_location</entry>
<!‐‐用户可以在这里配置远程扩展停止词字典‐‐>
<entry key="remote_ext_stopwords">words_location</entry>
</properties>

三. 高亮显示

在搜索中,经常需要对搜索关键字做高亮显示,高亮显示也有其常用的参数,在这个案例中做一些常用参数的介绍。
现在搜索cars索引中remark字段中包含“大众”的document。并对“XX关键字”做高亮显示,高亮效果使用html标签,并设定字体为红色。如果remark数据过长,则只显示前 20 个字符。

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
shell复制代码PUT /news_website
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"content": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}



PUT /news_website
{
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}




PUT /news_website/_doc/1
{
"title": "这是我写的第一篇文章",
"content": "大家好,这是我写的第一篇文章,特别喜欢这个文章门户网站!!!"
}

查询 title : “文章”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码GET /news_website/_doc/_search
{
"query":{
"match":{
"title":"文章"

}
},
"highlight":{
"fields":{
"title":{}}

}
}

查询结果

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
shell复制代码{
"took" : 878,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "news_website",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,
"_source" : {
"title" : "这是我写的第一篇文章",
"content" : "大家好,这是我写的第一篇文章,特别喜欢这个文章门户网站!!!"
},
"highlight" : {
"title" : [
"这是我写的第一篇<em>文章</em>"
]
}
}
]
}
}

表现,会变成红色,所以说你的指定的field中,如果包含了那个搜索词的话,就会在
那个field的文本中,对搜索词进行红色的高亮显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shell复制代码GET /news_website/_doc/_search 
{
"query": {
"bool": { "should": [
{
"match": {
"title": "文章"
}
},
{
"match": {
"content": "文章"
}
}
]
}
},
"highlight": {
"fields": {
"title": {},
"content": {}
}
}
}

highlight中的field,必须跟query中的field一一对齐的

2、常用的highlight介绍
plain highlight,lucene highlight,

默认
posting highlight,index_options=offsets

(1)性能比plain highlight要高,因为不需要重新对高亮文本进行分词

(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
shell复制代码DELETE news_website 
PUT /news_website
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
}, "content": {
"type": "text",
"analyzer": "ik_max_word",
"index_options": "offsets"
}
}
}
}

PUT /news_website/_doc/1
{
"title": "我的第一篇文章",
"content": "大家好,这是我写的第一篇文章,特别喜欢这个文章门户网站!!!"
}

GET /news_website/_doc/_search
{
"query": {
"match": {
"content": "文章"
}
},
"highlight": {
"fields": {
"content": {}
}
}
}

fast vector highlight

index‐time term vector设置在mapping中,就会用fast verctor highlight

(1)对大field而言(大于1mb),性能更高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shell复制代码
DELETE /news_website

PUT /news_website
{
"mappings": {
"properties": {
"title": {
"type": "text", "analyzer": "ik_max_word"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"term_vector" : "with_positions_offsets"
}
}
}
}

强制使用某种highlighter,比如对于开启了term vector的field而言,可以强制使用plain hlight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码GET /news_website/_doc/_search 
{
"query": {
"match": {
"content": "文章"
}
},
"highlight": {
"fields": {
"content": {
"type": "plain"
}
}
}
}

总结一下,其实可以根据你的实际情况去考虑,一般情况下,用plain highlight也就足够了,
不需要做其他额外的设置
如果对高亮的性能要求很高,可以尝试启用posting highlight
如果field的值特别大,超过了1M,那么可以用fast vector highlight

3、设置高亮html标签,默认是<em>标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shell复制代码GET /news_website/_doc/_search 
{
"query": {
"match": {
"content": "文章"
}
}, "highlight": {
"pre_tags": ["<span color='red'>"],
"post_tags": ["</span>"],
"fields": {
"content": {
"type": "plain"
}
}
}
}

4、高亮片段fragment的设置

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码
GET /news_website/_doc/_search
{
"query" : {
"match": { "content": "文章" }
}
,
"highlight" : {
"fields" : {
"content" : {"fragment_size" : 150, "number_of_fragments" : 3 }
}
}
}

fragment_size: 你一个Field的值,比如有长度是1万,但是你不可能在页面上显示这么长啊。。。设置要显示出来的fragment文本判断的长度,默认是100
number_of_fragments:你可能你的高亮的fragment文本片段有多个片段,你可以指定就显示几个片段

四、 聚合搜索技术深入

bucket 和 metric 概念简介

bucket就是一个聚合搜索时的数据分组。如:销售部门有员工张三和李四,开发部门有员工王五和赵六。那么根据部门分组聚合得到结果就是两个bucket。销售部门 bucket中有张三和李四, 开发部门 bucket中有王五和赵六。
metric就是对一个bucket数据执行的统计分析。如上述案例中,开发部门有2个员工,销售部门有2个员工,这就是metric。metric有多种统计,如:求和,最大值,最小值,平均值等。
1 用一个大家容易理解的SQL语法来解释,如:select count() from table group by colum n。那么group by column分组后的每组数据就是bucket。对每个分组执行的count()就是metric。

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
shell复制代码DELETE /cars
PUT /cars
{
"mappings": {
"properties": {
"price": {
"type": "long"
},
"color": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"model": {
"type": "keyword"
},
"sold_date": {
"type": "date",
"format": ["yyyy-MM-dd"]
},
"remark" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}

批量写入数据,注意日期格式

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
shell复制代码

GET /cars/_doc/_search
{
"query": {
"match_all": {}
}
}

POST /cars/_bulk
{"index":{}}
{ "price" : 258000, "color" : "金色", "brand":"大众", "model" : "大众迈腾", "sold_date" : "2015-01-11","remark" : "大众中档车" }
{"index":{}}
{ "price" : 123000, "color" : "金色", "brand":"大众", "model" : "大众速腾", "sold_date" : "2015-02-11","remark" : "大众神车" }
{"index":{}}
{ "price" : 239800, "color" : "白色", "brand":"标志", "model" : "标志508", "sold_date" : "2015-03-11","remark" : "标志品牌全球上市车型" }
{"index":{}}
{ "price" : 148800, "color" : "白色", "brand":"标志", "model" : "标志408", "sold_date" : "2015-04-11","remark" : "比较大的紧凑型车" }
{"index":{}}
{ "price" : 1998000, "color" : "黑色", "barand":"大众", "model" : "大众辉腾", "sold_date" : "2015-05-11","remark" : "大众最让人肝疼的车" }
{"index":{}}
{ "price" : 218000, "color" : "红色", "brand":"奥迪", "model" : "奥迪A4", "sold_date" :"2015-06-11","remark" : "小资车型" }
{"index":{}}
{ "price" : 489000, "color" : "黑色", "brand":"奥迪", "model" : "奥迪A6", "sold_date" : "2015-07-11","remark" : "政府专用?" }
{"index":{}}
{ "price" : 1899000, "color" : "黑色", "brand":"奥迪", "model" : "奥迪A 8", "so ld_date" : "2021-10-11" ,"remark" : "很贵的大A6。。。" }

1、根据color分组统计销售数量

只执行聚合分组,不做复杂的聚合统计。在ES中最基础的聚合为terms,相当于
SQL中的count。
在ES中默认为分组数据做排序,使用的是doc_count数据执行降序排列。可以使用
_key元数据,根据分组后的字段数据执行不同的排序方案,也可以根据_count元数
据,根据分组后的统计值执行不同的排序方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码GET /cars/_search
{
"size" : 8, # 显示数据条数
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"_count": "desc"
}
}
}
}
}

2、统计不同color车辆的平均价格

本案例先根据color执行聚合分组,在此分组的基础上,对组内数据执行聚合统计,这个组内数据的聚合统计就是metric。同样可以执行排序,因为组内有聚合统计,且对统计数据给予了命名avg_by_price,所以可以根据这个聚合统计数据字段名执行排序逻辑。
场景:下钻分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码GET /cars/_search 
{
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"avg_by_price": "asc"
}
},
"aggs": {
"avg_by_price": {
"avg": {
"field": "price"
}
}
}
}
}
}

size可以设置为0,表示不返回ES中的文档,只返回ES聚合之后的数据,提高查询速度,当然如果你需要这些文档的话,也可以按照实际情况进行设置

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
shell复制代码GET /cars/_search
{
"size" : 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"group_by_brand" : {
"terms": {
"field": "brand",
"order": {
"avg_by_price": "desc"
}
},
"aggs": {18 "avg_by_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}

3、统计不同 color 不同 brand 中车辆的平均价格

先根据color聚合分组,在组内根据brand再次聚合分组,这种操作可以称为下钻
分析。
Aggs如果定义比较多,则会感觉语法格式混乱,aggs语法格式,有一个相对固定
的结构,简单定义:aggs可以嵌套定义,可以水平定义。
嵌套定义称为下钻分析。水平定义就是平铺多个分组方式。

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
shell复制代码# 语法
GET /index_name/type_name/_search
{
"aggs" : {
"定义分组名称(最外层)": {
"分组策略如:terms、avg、sum" : {
"field" : "根据哪一个字段分组",
"其他参数" : ""
},
"aggs" : {
"分组名称1" : {},
"分组名称2" : {}
}
}
}
}

# 实践案例
GET /cars/_search
{
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"avg_by_price_color": "asc"
}
},
"aggs": {
"avg_by_price_color" : {
"avg": {
"field": "price"
}
},
"group_by_brand" : {
"terms": {
"field": "brand",
"order": {
"avg_by_price_brand": "desc"
}
},
"aggs": {
"avg_by_price_brand": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}

4、统计不同color中的最大和最小价格、总价

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
shell复制代码GET /cars/_search 
{
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"max_price": {
"max": {
"field": "price"
}
},
"min_price" : {
"min": {
"field": "price"
}
},
"sum_price" : {
"sum": {
"field": "price"
}
}
}
}
}
}

在常见的业务常见中,聚合分析,最常用的种类就是统计数量,最大,最小,平均,
总计等。通常占有聚合业务中的60%以上的比例,小型项目中,甚至占比85%以上。

5、统计不同品牌汽车中价格排名最高的车型

在分组后,可能需要对组内的数据进行排序,并选择其中排名高的数据。那么可
以使用s来实现:top_top_hithits中的属性size代表取组内多少条数据(默认为
10);sort代表组内使用什么字段什么规则排序(默认使用_doc的asc规则排序);
_source代表结果中包含document中的那些字段(默认包含全部字段)。

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
shell复制代码GET cars/_search 
{
"size" : 0,
"aggs": {
"group_by_brand": {
"terms": {
"field": "brand"
},
"aggs": {
"top_car": {
"top_hits": {
"size": 1,
"sort": [
{
"price": {
"order": "desc"
}
}
],
"_source": {
"includes": ["model", "price"]
}
}
}
}
}
}
}

6、histogram 区间统计

histogram类似terms,也是进行bucket分组操作的,是根据一个field,实现数据
区间分组。
如:以100万为一个范围,统计不同范围内车辆的销售量和平均价格。那么使用
histogram的聚合的时候,field指定价格字段price。区间范围是100万-interval :
1000000。这个时候ES会将price价格区间划分为: [0, 1000000), [1000000,
2000000), [2000000, 3000000)等,依次类推。在划分区间的同时,histogram会类似
terms进行数据数量的统计(count),可以通过嵌套aggs对聚合分组后的组内数据做
再次聚合分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shell复制代码GET /cars/_search
{
"aggs": {
"histogram_by_price": {
"histogram": {
"field": "price",
"interval": 1000000
},
"aggs": {
"avg_by_price": {
"avg": {
"field": "price"
}
}
}
}
}
}

7、date_histogram区间分组

date_histogram可以对date类型的field执行区间聚合分组,如每月销量,每年销量等。
如:以月为单位,统计不同月份汽车的销售数量及销售总金额。这个时候可以使
用date_histogram实现聚合分组,其中field来指定用于聚合分组的字段,interval指
定区间范围(可选值有:year、quarter、month、week、day、hour、minute、
second),format指定日期格式化,min_doc_count指定每个区间的最少document(如
果不指定,默认为0,当区间范围内没有document时,也会显示bucket分组),
extended_bounds指定起始时间和结束时间(如果不指定,默认使用字段中日期最小值
所在范围和最大值所在范围为起始和结束时间)。

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
shell复制代码#ES7.x之前的语法 
GET /cars/_search
{
"aggs": {
"histogram_by_date" : {
"date_histogram": {
"field": "sold_date",
"interval": "month",
"format": "yyyy‐MM‐dd",
"min_doc_count": 1,
"extended_bounds": {
"min": "2021‐01‐01",
"max": "2022‐12‐31"
}
},
"aggs": {
"sum_by_price": {
"sum": {
"field": "price"
}
}
}
}
}
}

#执行后出现
#! Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_inter
val] or [calendar_interval] in the future.

#7.X之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shell复制代码GET /cars/_search
{
"aggs": {
"histogram_by_date": {
"date_histogram": {
"field": "sold_date",
"calendar_interval": "month",
"format": "yyyy‐MM‐dd",
"min_doc_count": 1,
"extended_bounds": {
"min": "2021‐01‐01",
"max": "2022‐12‐31"
}
},
"aggs": {
"sum_by_price": {
"sum": {
"field": "price"
}
}
}
}
}
}

8、_global bucket

在聚合统计数据的时候,有些时候需要对比部分数据和总体数据。
如:统计某品牌车辆平均价格和所有车辆平均价格。global 是用于定义一个全局 bucket,这个 bucket会忽略 query 的条件,检索所有 document 进行对应的聚合统计。

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
shell复制代码GET /cars/_search
{
"size": 0,
"query": {
"match": {
"brand": "大众"
}
},
"aggs": {
"volkswagen_of_avg_price": {

"avg": {
"field": "price"
}
},
"all_avg_price": {
"global": {},
"aggs": {
"all_of_price": {
"avg": {
"field": "price"
}
}
}
}
}
}

9、aggs+order

对聚合统计数据进行排序。
如:统计每个品牌的汽车销量和销售总额,按照销售总额的降序排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码GET /cars/_search 
{
"aggs": {
"group_of_brand": {
"terms": {
"field": "brand",
"order": {
"sum_of_price": "desc"
}
},
"aggs": {
"sum_of_price": {
"sum": {
"field": "price"
}
}
}
}
}
}

如果有多层aggs,执行下钻聚合的时候,也可以根据最内层聚合数据执行排序。
如:统计每个品牌中每种颜色车辆的销售总额,并根据销售总额降序排列。这就像
SQL中的分组排序一样,只能组内数据排序,而不能跨组实现排序。

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
shell复制代码GET /cars/_search 
{
"aggs": {
"group_by_brand": {
"terms": {
"field": "brand"
},
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"sum_of_price": "desc"
}
},
"aggs": {
"sum_of_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
}
}

10、search+aggs

聚合类似SQL中的group by子句,search类似SQL中的where子句。在ES中是完全可
以将search和aggregations整合起来,执行相对更复杂的搜索统计。
如:统计某品牌车辆每个季度的销量和销售额。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shell复制代码GET /cars/_search 
{
"query": {4 "match": {
"brand": "大众"
}
},
"aggs": {
"histogram_by_date": {
"date_histogram": {
"field": "sold_date",
"calendar_interval": "quarter",
"min_doc_count": 1
},
"aggs": {
"sum_by_price": {
"sum": {
"field": "price"
}
}
}
}
}
}

11、filter+aggs

在ES中,filter也可以和aggs组合使用,实现相对复杂的过滤聚合分析。
如:统计10万~50万之间的车辆的平均价格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell复制代码GET /cars/_search 
{
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 100000,
"lte": 500000
}
}
}
}
},
"aggs": {
"avg_by_price": {
"avg": {18 "field": "price"
}
}
}
}

12、聚合中使用filter

filter也可以使用在aggs句法中,filter的范围决定了其过滤的范围。
如:统计某品牌汽车最近一年的销售总额。将filter放在aggs内部,代表这个过滤器
只对query搜索得到的结果执行filter过滤。如果filter放在aggs外部,过滤器则会过
滤所有的数据。
12M/M 表示 12 个月。
1y/y 表示 1年。
d 表示天

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
shell复制代码GET /cars/_search 
{
"query": {
"match": {
"brand": "大众"
}
},
"aggs": {
"count_last_year": {
"filter": {
"range": {
"sold_date": {
"gte": "now‐12M"
}
}
},
"aggs": {
"sum_of_price_last_year": {
"sum": {
"field": "price"
}
}
}
}
}
}

本文转载自: 掘金

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

MySQL中组合字段之concat()

发表于 2021-11-18

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

1、简介

MySQL是关系型数据库,我们在使用的时候往往会将对象的属性映射成列存储在表中,因此查询的到的结果在不做任何处理的情况下,也是一个个单独的属性;如果我们希望在MySQL中查询返回的结果集,能够将多个字段(列)的值组合起来返回、或者经过特定的计算后再返回,就可以使用MySQL提供的字段计算功能。

字段计算经常会用到如下两种:

  • 字段拼接
  • 字段执行算术计算

2、正文

MySQL中实现的字段组合都可以在客户机中完成,但是直接在MySQL服务器中实现字段组合会比客户机速度更快。

2.1 字段拼接

准备一张user表,插入几条数据,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sql复制代码SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`nation` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '民族',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '李子捌', '汉族');
INSERT INTO `user` VALUES (2, '张三', '回族');
INSERT INTO `user` VALUES (3, '李四', '维吾尔族');
INSERT INTO `user` VALUES (4, '王五', '蒙古族');

SET FOREIGN_KEY_CHECKS = 1;

需求:

获取用户的姓名和民族组合信息

语句:

1
2
3
4
5
6
7
8
9
lua复制代码mysql> select concat(name, '(',nation, ')')   from user;
+---------------------------------+
| concat(name, '(',nation, ')') |
+---------------------------------+
| 李子捌(汉族) |
| 张三(回族) |
| 李四(维吾尔族) |
| 王五(蒙古族) |
+---------------------------------+

解析:

这里使用了concat()函数,函数中可以组合任意多个元素,这些元素可以是表字段、固定字符等,元素之间使用,分隔,组合的顺序和concat()函数中字符的顺序一致。

关于组合之后的字段名问题?

细心的小伙伴发现组合之后的字段名使用的是concat()函数的整个函数体,显然这种显示不是我们想要的呀!如果想要指定自己想要的字段名,只需要使用别名即可!

1
2
3
4
5
6
7
8
9
sql复制代码mysql> select concat(name, '(',nation, ')') as user_message  from user;
+------------------+
| user_message |
+------------------+
| 李子捌(汉族) |
| 张三(回族) |
| 李四(维吾尔族) |
| 王五(蒙古族) |
+------------------+

别名的用法就是使用as,后面紧跟着的就是你想要指定的字段名。

2.2 字段执行算术计算

组合字段我们往往不只是简单的字符串拼接,可能会涉及到字段与字段之间的算术预算,此时我们就需要使用到MySQL中的算术操作符。MySQL提供了加减乘除操作符如下所示:

操作符 说明
+ 加
- 减
* 乘
/ 除

准备一张product表,插入几条数据,如下所示:

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
sql复制代码SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`product_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '产品名称',
`price` decimal(10, 2) UNSIGNED NOT NULL COMMENT '产品价格',
`number` int(11) NOT NULL COMMENT '产品数量',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of product
-- ----------------------------
INSERT INTO `product` VALUES (1, 'Apple iPhone 13 (A2634)', 6799.00, 22);
INSERT INTO `product` VALUES (2, 'HUAWEI P50 Pro', 6488.00, 88);
INSERT INTO `product` VALUES (3, 'MIX4', 4999.00, 30);
INSERT INTO `product` VALUES (4, 'OPPO Find X3', 3999.00, 15);
INSERT INTO `product` VALUES (5, 'vivo X70 Pro+', 5999.00, 27);

SET FOREIGN_KEY_CHECKS = 1;

需求:

查询目前库存产品总值

语句:

1
2
3
4
5
6
7
8
9
10
lua复制代码mysql> select product_name, concat(price * number) as gross_value from product;
+-------------------------+-------------+
| product_name | gross_value |
+-------------------------+-------------+
| Apple iPhone 13 (A2634) | 149578.00 |
| HUAWEI P50 Pro | 570944.00 |
| MIX4 | 149970.00 |
| OPPO Find X3 | 59985.00 |
| vivo X70 Pro+ | 161973.00 |
+-------------------------+-------------+

运算符顺序问题:

MySQL中运算符也是有顺序的,和普通运算符运算顺序一样, (* / ) > (+ -) ,如果在使用组合运算符时一定要注意运算符的顺序,合理使用()可以约束运算符的执行顺序。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lua复制代码mysql> select concat(12 - 3 * 4);
+--------------------+
| concat(12 - 3 * 4) |
+--------------------+
| 0 |
+--------------------+
1 row in set (0.00 sec)

mysql> select concat((12 - 3) * 4);
+----------------------+
| concat((12 - 3) * 4) |
+----------------------+
| 36 |
+----------------------+
1 row in set (0.00 sec)

值得注意的是MySQL中,被除数为0,并不会抛出异常,但是会返回NULL,这是MySQL内部对运算做了异常处理。

1
2
3
4
5
6
7
sql复制代码mysql> select concat(12 / 0);
+----------------+
| concat(12 / 0) |
+----------------+
| NULL |
+----------------+
1 row in set, 1 warning (0.00 sec)

本文转载自: 掘金

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

leetcode 986 Interval List In

发表于 2021-11-18

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

描述

You are given two lists of closed intervals, firstList and secondList, where firstList[i] = [starti, endi] and secondList[j] = [startj, endj]. Each list of intervals is pairwise disjoint and in sorted order.

Return the intersection of these two interval lists.

A closed interval [a, b] (with a <= b) denotes the set of real numbers x with a <= x <= b.

The intersection of two closed intervals is a set of real numbers that are either empty or represented as a closed interval. For example, the intersection of [1, 3] and [2, 4] is [2, 3].

Example 1:

1
2
lua复制代码Input: firstList = [[0,2],[5,10],[13,23],[24,25]], secondList = [[1,5],[8,12],[15,24],[25,26]]
Output: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]

Example 2:

1
2
lua复制代码Input: firstList = [[1,3],[5,9]], secondList = []
Output: []

Example 3:

1
2
lua复制代码Input: firstList = [], secondList = [[4,8],[10,12]]
Output: []

Example 4:

1
2
lua复制代码Input: firstList = [[1,7]], secondList = [[3,10]]
Output: [[3,7]]

Note:

  • 0 <= firstList.length, secondList.length <= 1000
  • firstList.length + secondList.length >= 1
  • 0 <= starti < endi <= 109
  • endi < starti+1
  • 0 <= startj < endj <= 109
  • endj < startj+1

解析

根据题意,给定两个闭区间列表,firstList 和 secondList,其中 firstList[i] = [starti, endi] 和 secondList[j] = [startj, endj]。 每个区间列表都是成对不相交的,并按排序顺序排列。题目要求我们返回这两个区间列表的交集。

题目中还给出了闭区间和交集的概念,闭区间 [a, b](a <= b)表示实数 x 的集合,a <= x <= b。两个闭区间的交集是一组实数,它们要么为空,要么表示为闭区间。 例如,[1, 3] 和 [2, 4] 的交集是 [2, 3]。

思路很简单,直接遍历所有的闭区间找交集即可:

  • 如果 A 或者 B 有一个为空列表,直接返回空列表
  • 初始化两个指针 i 和 j 都为 0 ,结果 result 为空列表
  • 当 i 小于 A 的长度并且 j 小于 B 的长度时候一直进行 while 循环
1
2
3
4
css复制代码  如果 A[i][1] < B[j][0] 说明 A[i] 和 B[j] 没有交集,i 加一进行下一次循环
同理 B[j][1] < A[i][0] 也说明 A[i] 和 B[j] 没有交集 ,j 加一进行下一次循环
如果有交集,直接将其存入 result 中
如果 A[i][1] < B[j][1] 说明需要查看 A[i+1] 与 B[j] 是否存在交集,所以 i 加一,否则 j 加一,一样的道理
  • 循环结束返回 result 即可。

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
less复制代码class Solution(object):
def intervalIntersection(self, A, B):
"""
:type A: List[List[int]]
:type B: List[List[int]]
:rtype: List[List[int]]
"""
if not A or not B: return []
i = j = 0
result = []
while i < len(A) and j < len(B):
if A[i][1] < B[j][0]:
i += 1
continue
if B[j][1] < A[i][0]:
j += 1
continue
result.append([max(A[i][0], B[j][0]), min(A[i][1], B[j][1])])
if A[i][1] < B[j][1]:
i += 1
else:
j += 1
return result

运行结果

1
2
erlang复制代码Runtime: 116 ms, faster than 90.07% of Python online submissions for Interval List Intersections.
Memory Usage: 14.3 MB, less than 69.98% of Python online submissions for Interval List Intersections.

解析

还可以有简化一下代码,原理同上。

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码class Solution(object):
def intervalIntersection(self, A, B):
"""
:type A: List[List[int]]
:type B: List[List[int]]
:rtype: List[List[int]]
"""
if not A or not B: return []
i = j = 0
result = []
while i < len(A) and j < len(B):
L = max(A[i][0], B[j][0])
R = min(A[i][1], B[j][1])
if L<=R:
result.append([L, R])
if A[i][1] < B[j][1]:
i += 1
else:
j += 1
return result

运行结果

1
2
erlang复制代码Runtime: 112 ms, faster than 96.69% of Python online submissions for Interval List Intersections.
Memory Usage: 14.3 MB, less than 69.98% of Python online submissions for Interval List Intersections.

原题链接:leetcode.com/problems/in…

您的支持是我最大的动力

本文转载自: 掘金

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

MySQL中使用正则表达式

发表于 2021-11-18

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

1、简介

MySQL中支持正则表达式匹配,在复杂的过滤条件中,可以考虑使用正则表达式。使用正则表达式需要掌握一些正则表达式的语法和指令,小捌推荐一个学习地址和在线工具,在学习MySQL中使用正则表达式之前,去了解一下正则表达式的语法和指令。

正则表达式学习网址:

www.runoob.com/regexp/rege…

正则表达式在线测试:

c.runoob.com/front-end/8…

值得注意的是,MySQL支持的正则表达式仅仅是正则表达式众多实现的一个子集,在使用正则表达式之前,建议先测试一下。 测试的时候不一定要先建立表、插入数据,可以直接使用select省略form子句,以简便的方式处理表达式,比如如下方式:

1
2
3
4
5
6
lua复制代码mysql> select '我爱你中国' regexp '我爱你';
+------------------------------+
| '我爱你中国' regexp '我爱你' |
+------------------------------+
| 1 |
+------------------------------+
1
2
3
4
5
6
lua复制代码mysql> select '12306' regexp '[:digit:]';
+----------------------------+
| '12306' regexp '[:digit:]' |
+----------------------------+
| 1 |
+----------------------------+

2、正文

首先准备一张product表,DDL和表数据如下所示,可以直接复制使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sql复制代码SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`product_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '产品名称',
`price` decimal(10, 2) UNSIGNED NOT NULL COMMENT '产品价格',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of product
-- ----------------------------
INSERT INTO `product` VALUES (1, 'Apple iPhone 13 (A2634)', 6799.00);
INSERT INTO `product` VALUES (2, 'HUAWEI P50 Pro', 6488.00);
INSERT INTO `product` VALUES (3, 'MIX4', 4999.00);
INSERT INTO `product` VALUES (4, 'OPPO Find X3', 3999.00);
INSERT INTO `product` VALUES (5, 'vivo X70 Pro+', 5999.00);

SET FOREIGN_KEY_CHECKS = 1;

初始数据如下所示:

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> select * from product;
+----+-------------------------+---------+
| id | product_name | price |
+----+-------------------------+---------+
| 1 | Apple iPhone 13 (A2634) | 6799.00 |
| 2 | HUAWEI P50 Pro | 6488.00 |
| 3 | MIX4 | 4999.00 |
| 4 | OPPO Find X3 | 3999.00 |
| 5 | vivo X70 Pro+ | 5999.00 |
+----+-------------------------+---------+

2.1 语句顺序

正则表达式的作用是文本匹配,使用一个正则表达式与一个文本内容进行比较,可以校验文本是否符合正则表达式阐述的规则。在MySQL中,正则表达式用在where子句中,可以对select查询的数据进行过滤。

select * from table_name where regexp ‘你的正则表达式’ order by cloumn_name;


需求:

查询产品表中,产品名称中包含3的产品

语句:

1
csharp复制代码mysql> select * from product where product_name regexp '3';

结果:

1
2
3
4
5
6
diff复制代码+----+-------------------------+---------+
| id | product_name | price |
+----+-------------------------+---------+
| 1 | Apple iPhone 13 (A2634) | 6799.00 |
| 4 | OPPO Find X3 | 3999.00 |
+----+-------------------------+---------+

2.2 如何区分大小写

MySQL使用正则表达式默认不区分大小写,但是大部分情况下我们需要明确英文的大小写进行匹配,这个时候我们可以使用binary关键字。

需求:

查询产品表中,产品名称包含huawei的产品

语句:

1
csharp复制代码mysql> select * from product where product_name regexp  'huawei';

结果:

1
2
3
4
5
diff复制代码+----+----------------+---------+
| id | product_name | price |
+----+----------------+---------+
| 2 | HUAWEI P50 Pro | 6488.00 |
+----+----------------+---------+

此时查询结果默认不区分大小写,所以可以直接查询出来,如果我们希望查询区分大小写,此时只需要在regexp后面加上binary关键字即可。

语句:

1
sql复制代码mysql> select * from product where product_name regexp binary 'huawei';

结果:

1
sql复制代码Empty set (0.00 sec)

由于product表中并没有包含小写huawei的产品,所以返回结果为Empty set

2.3 正则表达式与like的区别

相信有些小伙伴发现上面实现的功能,其实用like也能实现。很多场景下我们使用like来对字符串进行匹配,但是这些场景往往非常简单,而正则表达式是一个非常强大的文本检索过滤工具,它的所能实现的功能比like操作符强大太多啦。总之like能做的正则表达式都能做,正则表示能做的like基本上做不了(要么非常棘手)。

比如如下需求,使用正则表达式可以轻松实现,但是like操作符却不知道怎么实现了。

需求:

查询产品表中,产品名称中v至少出现一次产品信息

语句:

1
csharp复制代码mysql> select * from product where product_name regexp 'v+';

结果:

1
2
3
4
5
diff复制代码+----+---------------+---------+
| id | product_name | price |
+----+---------------+---------+
| 5 | vivo X70 Pro+ | 5999.00 |
+----+---------------+---------+

注意:正则表达式重复元字符的匹配都是整个连续出现。下面给出这几个重复元字符,感觉有些小伙伴会理解错。

重复元字符

元字符 说明
* 0个或多个匹配,效果与{0,}一致
+ 1个或多个匹配,效果与{1,}一致
? 1个或0匹配,效果与{0,1}一致
{n} 等于n个匹配数目
{n,} 大于等于n个匹配
{n,m} 大于等于n小于等于m个,m<255

本文转载自: 掘金

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

IDEA Plugin,写一个看股票指数和K线的插件

发表于 2021-11-18

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

没招了,不写点刺激的,你总是不好好看!

以前,我不懂。写的技术就是技术内容,写的场景就是场景分析,但从读者的阅读我发现,大家更喜欢的是技术与场景结合,尤其是用技术结合那些羞羞答答的场景,虽然嘴上都不说。

本章节主要是想给大家介绍关于在 IDEA 插件开发中如何使用工具栏和Tab页,来填充在 IDEA 中底部的窗体,就像 IDEA 中的控制台一样。但就这么写好像是够呛能有人看,因为大家只从技术点来看,很难发现这里面有什么应用场景,即使看了好像也不知道这技术能干啥。

那咋办,整点刺激的吧。大家不是喜欢赔钱炒股吗,那就把股票行情和K线展示结合到IDEA插件开发中。可以让学习插件的伙伴,都能在写代码疲惫的时候还能看一眼股票,也许在关键的时候还能提醒你赶紧抛出去!

二、需求目的

安全起见,需要在 IDEA 中以一个比较隐秘的角落,开发股票插件,让炒股的同学可以在紧张编码CRUD之余,不要忘记自己关注的股票购买和抛售。

那么为了解决这个问题,我们需要几个技术点,包括:股票数据接口、查询对象封装、IDEA 底部窗体和工具栏开发、定时任务扫描、Swing UI、股票代码配置和保存、窗体按钮事件监听等。

接下来我们就结合这些技术点,来解决实际的场景问题,看看如何在 IDEA 中开发一个股票插件。

三、案例开发

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
java复制代码guide-idea-plugin-tab
├── .gradle
└── src
├── main
│ └── java
│ └── cn.bugstack.guide.idea.plugin
│ ├── domain
│ │ ├── model
│ │ │ ├── aggregates
│ │ │ │ └── StockResult.java
│ │ │ └── vo
│ │ │ ├── Data.java
│ │ │ ├── GoPicture.java
│ │ │ └── Stock.java
│ │ └── service
│ │ ├── impl
│ │ │ └── StockImpl.java
│ │ └── IStock
│ ├── factory
│ │ └── TabFactory.java
│ ├── infrastructure
│ │ ├── DataSetting.java
│ │ └── DataState.java
│ ├── module
│ │ ├── RefreshBar.java
│ │ ├── SettingBar.java
│ │ └── ViewBars.java
│ └── ui
│ │ ├── ConsoleUI.java
│ │ ├── ConsoleUI.form
│ │ ├── GidConfig.java
│ │ └── GidConfig.form
│ └── Config
├── resources
│ └── META-INF
│ └── plugin.xml
├── build.gradle
└── gradle.properties

源码获取:#公众号:bugstack虫洞栈 回复:idea 即可下载全部 IDEA 插件开发源码

在此 IDEA 插件工程中,主要分为5块区域:

  • domain:领域层,提供查询股票接口的数据服务,如果你是做的一些其他工具型功能,也可以把业务类的内容放到 domain 中实现。
  • factory:工厂层,这里主要提供的是一个工具窗体生成的入口对象,来创建出我们自己添加的窗体内容。
  • infrastructure:基础层,提供了数据存放对象,这个数据对象是一个可以落盘的操作,创建好的类配置到 plugin.xml 中即可。这样我们配置好股票代码后,关机重启 IDEA 也可以把配置读取出来
  • module:模块,提供用于 UI 窗体使用的一些工具页操作。比如这里的 ViewBars 会在 TabFactory 中实例化,用于展示出你添加的窗体。
  • ui:这一部分使用的是 IDEA 中自动拖拽生成的窗体,免去了手写的复杂性,一些简单的页面直接拖拽就可以。这也是一种低代码哦!

接下来,我们就分别看下每个核心功能点的实现过程,这个过程中你可以提前把代码下载下来,对照着学习会更加容易理解。

  1. 实现 ToolWindowFactory 开发一个底部的窗体,用于承载所需的内容
  2. 左侧是侧边工具栏,配置自选股、刷新股票指数
  3. 右侧是2个 tab 页,分别用于展示股票数据和K线图,这里的数据则需要通过股票接口来提供

2. tab 页窗体

首先这里我们先使用 IDEA 插件开发中,Swing UI 功能,拖拽出2个简单的窗体。有了这样的一个基本结构大家的脑子里应该就可以有画面了。

2.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
java复制代码public class GidConfig implements Configurable {

private JPanel mainPanel;
private JPanel settingPanel;
private JLabel gidLabel;
private JTextField gidTextField;

private ConsoleUI consoleUI;

public GidConfig(ConsoleUI consoleUI){
this.consoleUI = consoleUI;
}

public JTextField getGidTextField() {
return gidTextField;
}

@Override
public void apply() throws ConfigurationException {
List<String> gidList = DataSetting.getInstance().getGids();
gidList.clear();
String[] gids = gidTextField.getText().trim().split(",");
for (String gid : gids) {
gidList.add(gid.trim());
}
// 刷新数据
consoleUI.addRows(gidList);
}

}
  • 在 GidConfig 对应的 java 类中,可以对一些窗体中出现的属性进行获取。当用户点击这个窗体的确认按钮后,我们可以在 apply 中拿到用户配置的股票代码,并对其进行读取和设置股票数据。

2.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
java复制代码public class ConsoleUI {

private JTabbedPane tabbedPane1;
private JPanel one;
private JPanel two;
private JLabel picMin;
private JTable table;
private JLabel picDay;

// 查询数据服务
private IStock stock = new StockImpl();

private DefaultTableModel defaultTableModel = new DefaultTableModel(new Object[][]{}, new String[]{"股票", "代码", "最新", "涨跌", "涨幅"});

public ConsoleUI() {
// 初始数据
table.setModel(defaultTableModel);
addRows(DataSetting.getInstance().getGids());

// 添加事件
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
int row = table.getSelectedRow();
Object value = table.getValueAt(row, 1);
GoPicture goPicture = stock.queryGidGoPicture(value.toString());
try {
// 分钟K线
picMin.setSize(545, 300);
picMin.setIcon(new ImageIcon(new URL(goPicture.getMinurl())));

// 当日K线
picDay.setSize(545, 300);
picDay.setIcon(new ImageIcon(new URL(goPicture.getDayurl())));
} catch (MalformedURLException m) {
m.printStackTrace();
}
}
});
}

public JTabbedPane getPanel() {
return tabbedPane1;
}

public void addRows(List<String> gids) {
// 查询
List<Data> dataList = stock.queryPresetStockData(gids);

// 清空
int rowCount = defaultTableModel.getRowCount();
for (int i = 0; i < rowCount; i++) {
defaultTableModel.removeRow(0);
}

// 添加
for (Data data : dataList) {
defaultTableModel.addRow(new String[]{data.getName(), data.getGid(), data.getNowPri(), data.getIncrease(), data.getIncrePer()});
table.setModel(defaultTableModel);
}
}

}
  • 展示股票的窗体对应的 ConsoleUI 类,主要负责数据的渲染、更新和对每条数据的事件操作,当用户点击某一条数据以后,就可以到 K线 页中看到对应的股票指数了。

3. 股票框体设置

在开发完 UI 窗体后,我们还需要使用一个 SimpleToolWindowPanel 的继承实现类,承载工具栏和页面的设置。

3.1 设置-工具栏

cn.bugstack.guide.idea.plugin.module.SettingBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class SettingBar extends DumbAwareAction {

private ViewBars panel;

public SettingBar(ViewBars panel) {
super("配置股票", "Click to setting", IconLoader.getIcon("/icons/config.svg"));
this.panel = panel;
}

@Override
public void actionPerformed(@NotNull AnActionEvent e) {
ShowSettingsUtil.getInstance().editConfigurable(panel.getProject(), new GidConfig(panel.getConsoleUI()));
}

}
  • 设置工具栏位于自定义插件面板中最左侧的位置,用于设置自选股票代码。
  • 通过在方法 actionPerformed 中使用 ShowSettingsUtil 工具类启动 UI 窗体。

3.2 刷新-工具栏

cn.bugstack.guide.idea.plugin.module.RefreshBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class RefreshBar extends DumbAwareAction {

private ViewBars panel;

public RefreshBar(ViewBars panel) {
super("刷新指数", "Click to refresh", IconLoader.getIcon("/icons/refresh.svg"));
this.panel = panel;
}

@Override
public void actionPerformed(@NotNull AnActionEvent e) {
panel.getConsoleUI().addRows(DataSetting.getInstance().getGids());
}

}
  • 在刷新工具栏中主要是用于手动触发刷新股票最新结果,之所以使用手动刷新主要是这个接口有查询次数限制,如果是定时任务一直跑,一会100次的查询限制就用完了。不过我们这里也是为了体现专栏内对技术的使用,增加多个设置按钮,就更容易知道如何添加了

3.3 窗体填充面板

cn.bugstack.guide.idea.plugin.module.ViewBars

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
java复制代码public class ViewBars extends SimpleToolWindowPanel {

private Project project;
private ConsoleUI consoleUI;

public ViewBars(Project project) {
super(false, true);
this.project = project;
consoleUI = new ConsoleUI();

// 设置窗体侧边栏按钮
DefaultActionGroup group = new DefaultActionGroup();
group.add(new SettingBar(this));
group.add(new RefreshBar(this));

ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("bar", group, false);
toolbar.setTargetComponent(this);
setToolbar(toolbar.getComponent());

// 添加
JBSplitter splitter = new JBSplitter(false);
splitter.setSplitterProportionKey("main.splitter.key");
splitter.setFirstComponent(consoleUI.getPanel());
splitter.setProportion(0.3f);
setContent(splitter);
}

public Project getProject() {
return project;
}

public ConsoleUI getConsoleUI() {
return consoleUI;
}
}
  • 在填面板中主要是在我们自定义的插件中,在左侧添加工具栏,其余位置添加股票展示面板。
  • DefaultActionGroup 中可以以此添加设置和刷新按钮,并最终填充到 ActionToolbar 里去,这样就设置完成了。
  • JBSplitter 是一个分割线,右侧填充上我们的股票指数展示面板 splitter.setFirstComponent(consoleUI.getPanel())

4. 填充主面板到IDEA工具栏

4.1 窗体工厂

cn.bugstack.guide.idea.plugin.factory.TabFactory

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
java复制代码public class TabFactory implements ToolWindowFactory {

@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
// 窗体
ViewBars viewPanel = new ViewBars(project);
// 获取内容工厂的实例
ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
// 获取 ToolWindow 显示的内容
Content content = contentFactory.createContent(viewPanel, "股票", false);
// 设置 ToolWindow 显示的内容
toolWindow.getContentManager().addContent(content, 0);

// 定时任务,自动刷新股票
/* 因每日查询次数限制,这里就不开定时任务了,用户可以自行申请 https://dashboard.juhe.cn/home
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
viewPanel.getConsoleUI().addRows(DataSetting.getInstance().getGids());
}
}, 3000, 2000);*/
}

}
  • 在 TabFactory 中主要包括两部分,一个是把 ViewBars 填充到整个 toolWindow 中,另外一个是我们目前已经注释掉的刷新股票数据的定时任务。
  • 这里由于股票接口查询次数限制,所以就把定时任务注释掉了,否则一会就把可用次数跑没了。

4.2 配置窗体

plugin.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<idea-plugin>

<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<toolWindow id="XUtil"
canCloseContents="true"
anchor="bottom"
factoryClass="cn.bugstack.guide.idea.plugin.factory.TabFactory"
icon="/icons/stock.png"
/>

</extensions>

</idea-plugin>
  • 这里我们把窗体配置到整个 IDEA 界面的最下方 anchor="bottom" 这个位置既方便又最安全

5. 数据持久化配置

  • 当我们使用 IDEA 进行配置一些基本参数后,例如:Maven、Gradle、Git、签名信息等,在日常的关闭和重启 IDEA 时,这些配置信息是会保存下来的,而不会说关闭就没了。
  • 那么我们开发的这款插件需要做的一些自选股票代码配置,也要进行保存,否则不能每次都在 IDEA 启动时重新设置。所以这里我们需要用到 plugin.xml 中 applicationService 配置上实现了 PersistentStateComponent 的数据设置存放类。

5.1 对象数据

cn.bugstack.guide.idea.plugin.infrastructure.DataState

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class DataState {

private List<String> gids = new ArrayList<>();

public List<String> getGids() {
return gids;
}

public void setGids(List<String> gids) {
this.gids = gids;
}

}
  • 这个是数据对象类,你可以在这里设置你需要的属性存放,就像 gids 一样,用于存放用户配置的股票代码集合。

5.2 持久数据

cn.bugstack.guide.idea.plugin.infrastructure.DataSetting

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
java复制代码@State(name = "DataSetting",storages = @Storage("plugin.xml"))
public class DataSetting implements PersistentStateComponent<DataState> {

private DataState state = new DataState();

public static DataSetting getInstance() {
return ServiceManager.getService(DataSetting.class);
}

@Nullable
@Override
public DataState getState() {
return state;
}

@Override
public void loadState(@NotNull DataState state) {
this.state = state;
}

public List<String> getGids(){
return state.getGids();
}

}
  • DataSetting 类需要使用到 IDEA 插件开发的提供的注解 @State 配置持久对象
  • 此外还需要提供一个 getInstance 方法来获取数据对象实例,那么在我们实际使用的时候就可以拿到我们配置的对象了并进行设置和读取数据。

5.3 plugin.xml 配置

1
2
3
4
xml复制代码<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<applicationService serviceImplementation="cn.bugstack.guide.idea.plugin.infrastructure.DataSetting"/>
</extensions>
  • 在创建好数据设置类以后,则需要使用 applicationService 标签把你的类配置到 plugin.xml 中 extensions 里面。

6. 股票接口

6.1 技术调研

  • 无论什么功能开发,在开始之前都需要把这些零碎的事情处理完,才能进行代码开发,这个过程也叫做技术调研到设计和评审。就像现在我们需要进行股票信息的查询,那么就需要找到一个可以提供数据查询的接口,看看这个接口如何申请使用,以及返回的对象都有哪些字段,是否符合我们的预期。
  • 这里小傅哥找到了一个聚合数据的接口,不过只能免费100次/天调用,如果你有更好的可以更换下。

  • 接口:web.juhe.cn:8080/finance/sto… - 这里的 key 需要自己申请
  • 数据:

6.2 服务封装

有了股票的查询接口,接下来就可以对数据做一个查询和对象转换了。

cn.bugstack.guide.idea.plugin.domain.service.impl.StockImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class StockImpl implements IStock {

// 自行申请,股票API,替换key即可【一天可免费调用100次】:https://dashboard.juhe.cn/home/
private final String key = "4bc57728***********f0595";

@Override
public List<Data> queryPresetStockData(List<String> gids) {
List<Data> dataList = new ArrayList<>();
for (String gid : gids) {
StockResult stockResult = JSON.parseObject(HttpUtil.get("http://web.juhe.cn:8080/finance/stock/hs?gid=" + gid + "&key=" + key), StockResult.class);
Stock[] stocks = stockResult.getResult();
for (Stock stock : stocks) {
dataList.add(stock.getData());
}
}
return dataList;
}

}
  • 这里我们在 domain 领域层中定义数据 vo 对象,以及提供股票查询服务的封装。这样调用方就可以直接使用这份数据了,如果你是其他厂商提供的股票查询接口,也可以进行封装和更换,做一个接口适配层。

四、测试验证

  • 如果你下载工程后没有 Plugin 和一个绿色箭头,那么可以按照图自己配置 :runIde 这样就可以运行了。

运行效果 - 激动人心的时刻到了,再也不用担心写代码影响看股票了哦

1. 配置股票

  • 首先你需要在这里配置你关注的股票代码,我配置了3个我看好的。

2. 自选股指数

  • 配置好以后你就可以看到自己的自选股指数了,选中一条以后,在点击K线。
  • 当你需要看最新数据的时候,可以点左侧的刷新按钮。

3. K线

  • 现在这个就是对应的 K线,是不是还挺香的。目前是加了最近K线和日K线,你还可以扩展其他维度的图。

五、总结

  • 本章节我们使用到了在 IDEA 窗体中添加稍微复杂一些的页面结构,有侧边栏、有Tab页,并在需要在这些页面体中进行交互和通信。此外还是用到了数据的存储设置,这个在很多时候开发IDEA插件里都会用到。
  • 像是这样的技术实践不只是可以用于展示股票数据,你还可以结合自己所需扩展属于你实际场景中需要的内容,比如开发一个数据集中查询插件,可以查询数据库、ES、Redis等,也可以是所有的工具类集合页,这些内容会更有技术价值。
  • 当你自己开始主动的向你学习到的一些源码、框架、组件、项目等中添加自己想要的功能时,就是你真的开始学习了,否则一个内容看过没多久也就忘记了。

六、系列推荐

  • IDEA 插件怎么开发?
  • 基于IDEA插件开发和字节码插桩技术,实现研发交付质量自动分析
  • 调研字节码插桩技术,用于系统监控设计和实现
  • 《手撸 Spring》PDF,全书260页6.5万字,完稿&发版!
  • 认知自己的技术栈盲区

本文转载自: 掘金

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

1…298299300…956

开发者博客

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