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

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


  • 首页

  • 归档

  • 搜索

二维数组中的查找 二维数组中的查找

发表于 2021-11-26

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

二维数组中的查找​​

面试题3:

image-20210910134358545

似题:

我做过这个类似的有杨氏矩阵为背景的,实际上是一样的

暴力遍历

二维数组暴力遍历的话时间复杂度为O(n^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
c复制代码#include<stdio.h>
//const 就是因为二维数组是定死的
int search(const int arr[4][4], int num,unsigned int* prow,unsigned int* pcol)
{
int i = 0;
//扫描行
for (i = 0; i < *prow; i++)
{
//扫描列
int j = 0;
for (j = 0; j < *pcol; j++)
{
//与所查数比较判断,有一样的就直接返回
if (arr[i][j] == num)
{
*prow = i;//把坐标传回去
*pcol = j;
return 1;//一次返回,之后就不看了,因为已经证明到有这个数了,没必要在做无用功了
}
}
}
return 0;
}
int main()
{
int arr[4][4] = { {1,2,8,9},{2,4,9,12},{4,7,10,13},{6,8,11,15} };
int num = 0;
while (1)
{
unsigned int row = sizeof(arr) / sizeof(arr[0]);
unsigned int col = sizeof(arr[0]) / sizeof(arr[0][0]); //把row,col拉进来就是为了每次再来是更新一次
//长宽,因为下面我们就是用row,col变量没有用其他变量
printf("请输入你想要找的数:>");
scanf("%d", &num);
if (search(arr, num, &row, &col))//把长宽传地址过去用指针prow,pcol接收
{
printf("有这个数\n");
printf("坐标为(%d,%d)\n", row, col);
}
else
{
printf("没有这个数\n");
}
}
return 0;
}

image-20210910163325915

动态基点操作

暴力操作肯定拿不下面试官的心,没有思想,应该优化程序,减小时间复杂度

image-20210910165659391

image-20210910170432274

image-20210910170800065

然后把上面search函数改改就可以了

时间复杂度也降为O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
c复制代码#include<stdio.h>
//const 就是因为二维数组是定死的
int search(const int arr[4][4], int num,unsigned int* prow,unsigned int* pcol)
{
int i = 0;
unsigned int x = 0;
unsigned int y = *pcol-1;
while ((x<*prow)&&(y>=0))
{
if (arr[x][y] - num > 0)
{
y--;
}
else if (arr[x][y] - num < 0)
{
x++;
}
else
{
*prow = x;
*pcol = y;
return 1;
}
}
return 0;
}
int main()
{
int arr[4][4] = { {1,2,8,9},{2,4,9,12},{4,7,10,13},{6,8,11,15} };
int num = 0;
while (1)
{
unsigned int row = sizeof(arr) / sizeof(arr[0]);
unsigned int col = sizeof(arr[0]) / sizeof(arr[0][0]); //把row,col拉进来就是为了每次再来是更新一次
//长宽,因为下面我们就是用row,col变量没有用其他变量
printf("请输入你想要找的数:>");
scanf("%d", &num);
if (search(arr, num, &row, &col))//把长宽传地址过去用指针prow,pcol接收
{
printf("有这个数\n");
printf("坐标为(%d,%d)\n", row, col);
}
else
{
printf("没有这个数\n");
}
}
return 0;
}

image-20210910172040278

结果也是不错的

本文转载自: 掘金

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

Kubernetes vs Docker 意想不到的结局!

发表于 2021-11-26

在这之前,Kubernetes开发团队宣布,他们正在弃用docker。这则新闻通过科技界和社交网络广为流传。Kubernetes 群集是否会中断,如果是,我们将如何运行我们的应用程序?我们现在该怎么办?今天,我们将审查所有这些问题和更多。

让我们从头开始。如果你已经熟悉docker和kubernetes,并希望直接了解关键信息,跳到docker弃用对你有什么影响?

什么是容器?

尽管Docker被用作容器的同义词,但现实情况是,它们早在docker成为东西之前就已经存在了。Unix 和 Linux 自 70 年代末开始引入 chroot 以来,一直有某种形式的容器。Chroot 允许系统管理员在一种但并非真正孤立的文件系统中运行程序。后来,这个想法被提炼和增强到集装箱引擎,如免费 BSD Jails , OpenVZ ,或Linux Containers(LXC) 。

但是什么是容器呢?

容器是一个逻辑分区,我们可以运行与系统其余部分分离的应用程序。每个应用程序都有自己的专用网络和不与其他容器或主机共享的虚拟文件系统。

)

运行容器应用程序比安装和配置软件方便得多。首先,容器是便携式的:我们可以在一台服务器中构建,并相信它将在任何服务器中工作。另一个优点是,我们可以同时运行同一程序的多个副本,而不会发生冲突或重叠,否则确实很难做到。

然而,要使这一切发挥作用,我们需要一个容器runtime,一个能够运行容器的软件。

什么是docker?

docker是最受欢迎的容器runtime-从长远来看。这并不奇怪,因为它将容器的概念引入主流,这反过来又激发了像Kubernetes这样的平台的创建。

在Docker之前,运行容器确实是可能的,但这是艰苦的工作。Docker 使事情变得简单,因为它是一个完整的技术堆栈,可以:

管理容器生命周期。

代理请求来回容器。

监视和记录容器活动。

安装共享目录。

对容器设置资源限制。

生成镜像。Dockerfile是构建容器镜像的格式文件。

从注册处推送和拉取图像。

在第一次迭代中,Docker 使用 Linux 容器(LXC)作为运行时间后端。随着项目的发展,LXC被容器所取代,docker自己的实施。现代docker安装分为两个服务:containerd,负责管理容器;dockerd,处理剩余的部分。

)

什么是kubernetes?

kubernetes采取容器的想法,并把它一个缺口。Kubernetes 不是在单个服务器中运行容器化应用程序,而是将其分布在一组机器上。在 Kubernetes 中运行的应用程序的外观和行为都像一个单元,尽管在现实中,它们可能由松散耦合的容器排列而成。

Kubernetes在容器顶部添加分布式计算功能:

吊舱:吊舱是共享内存、CPU、存储和网络等资源的逻辑容器组。

自动缩放:Kubernetes 可根据需要启动和停止吊舱,从而自动适应不断变化的工作负载。

自我修复:容器在故障时被监控并重新启动。

负载均衡:请求分布在健康的可用吊舱上。

推出:kubernetes支持自动推出和回滚。使 Canary 和 Blue-Green 等复杂程序变得微不足道。我们可以将Kubernetes的架构视为两架飞机的组合:

控制面板是集群的协调大脑。它有一个控制器,管理节点和服务,调度器分配吊舱的节点,和API服务,处理通信。配置和状态存储在一个高度可用的数据库称为etcd。工人节点是运行容器的机器。每个工人节点运行几个组件,如kubelet代理、网络代理和容器运行时。Kubernetes版本 v1.20 的默认容器运行时是 Docker。

)

容器格式

在启动容器之前,我们需要构建或下载一个容器镜像,这是一个文件系统,里面装满了应用程序所需的一切:代码、二进制文件、配置文件、库和依赖项。

容器的普及表明需要开放的镜像标准。因此,Docker 公司和 CoreOS 于 2015 年建立了开放式容器计划(OCI) ,其使命是生产供应商中立格式。这一努力的结果是创造了两项标准:

定义镜像二进制格式的镜像规范。

描述如何拆开和运行容器的运行时规范。OCI 维护称为runc的参考实现。容器和 CRI-O 都使用背景中的流体生成容器。OCI 标准带来了不同容器解决方案之间的互操作性。因此,一个系统内置的图像可以在任何其他合规堆栈中运行。

OCI 标准带来了不同容器解决方案之间的互操作性。因此,一个系统内置的镜像可以在任何其他合规堆栈中运行。

Docker Vs. Kubernetes

这里是事情变得更加技术性的地方。我说每个Kubernetes工人节点都需要一个容器运行时。在其第一个原始设计 ,Docker是离不开Kubernetes,因为它是唯一的运行时支持。

然而,Docker从未被设计成在Kubernetes内运行。意识到这个问题,Kubernetes开发人员最终实现了一个名为容器运行时间接口(CRI) 的 API。此界面允许我们在不同的容器运行时之间进行选择,使平台更加灵活,对 Docker 的依赖性更小。

)

这一变化给Kubernetes团队带来了新的困难, 因为Docker不知道CRI或支持CRI 。因此,在引入 API 的同时,他们不得不编写一个名为Dockershim的适配器,以便将 CRI 消息转换为 Docker 特定命令。

弃用Dockershim

虽然Docker是一段时间以来第一个也是唯一支持的引擎,但是它从来不在长期计划内。Kubernetes V 1.20弃用了dockershim , 拉开了离开docker的过渡的序幕。

一旦过渡完成,堆栈就会变小得多。它从这个:

)

结果是每个工人节点所需的膨胀更少,依赖性也更少。

那么,为什么要改变呢?

简单地说,Docker很重。我们得到更好的性能与轻量级集装箱运行时,如容器或CRI-O 。最近的例子是,谷歌的基准显示,容器消耗的内存和CPU更少,而吊舱的启动时间也比Docker少。

此外,在某些方面,Docker本身可以被认为是技术债务。事实上,Kubernetes需要的是容器运行时:容器。其余的,至少就Kubernetes而言,是额外的开销。

Kubernetes弃用Docker对你有什么影响?

事情并不像听起来那么戏剧化。让我们在整节的开头说,在v1.20中唯一改变的是,你会得到一个弃用警告,只有当你运行Docker。就这样。

我还能使用Docker进行开发吗?

是的,你绝对可以,现在和在可预见的未来。你看,Docker不运行Docker特定的镜像:它运行符合OCI标准的容器。只要Docker继续使用这种格式,Kubernetes将继续接受它们。

我仍然可以用Docker打包我的生产应用程序吗?

是的,原因与上一个问题相同。与Docker打包的应用程序将继续运行-那里没有变化。因此,您仍然可以使用您了解和喜爱的工具构建和测试容器。您不需要更改CI/CD 管道或切换到其他镜像注册,Docker 制作的镜像将继续像始终一样在群集中工作。

我需要改变什么?

现在什么都没有如果您的群集使用 Docker 作为运行时,则升级到 v1.20 后将获得弃用警告。但这一变化是Kubernetes社区发出的一个明确信号,表明他们想采取的方向。是时候开始规划未来了。

变革何时发生?

该计划是在2021年底将所有Docker依赖关系完全删除v1.23。

当Kubernetes离开时,会发生什么?

届时,Kubernetes 集群管理员将被迫切换到符合 CRI 标准的容器运行时。

如果你是一个最终用户,没有很多变化给你。除非你运行某种节点自定义,否则你可能不必做任何特别的事情。仅测试您的应用程序与新的容器运行时配合使用。

这些是升级到 v1.23 后会导致问题或中断的一些事情:

使用Docker特定的日志记录和监视。即,从日志中解析 Docker 消息或投票 Docker API。

使用Docker优化。

运行依赖docker CLI 的脚本。

运行docker命令在特权吊舱。例如:构建镜像。有关替代解决方案,请参阅卡尼科等项目。docker build

使用docker工人设置。

运行窗口容器。容器确实在 Windows 中工作, 但它的支持水平还没有达到 Docker 的。目标是通过集装箱版本 1.20为 Windows 提供稳定的容器释放。

如果您在 AWS EKS、Google GKE 或 Azure AKS 等云提供商上使用托管集群,请在 Docker 支持消失之前检查您的集群是否使用了支持的运行时。有些云供应商落后几个版本,因此您可能有更多的时间来计划。因此,请咨询您的提供商。举个例子,谷歌云宣布,他们正在改变默认运行时从Docker到容器的所有新创建的工人节点,但你仍然可以选择Docker。

如果您运行自己的集群:除了检查上述要点外,您还需要评估移动到与 CRI 完全兼容的另一个容器运行时。Kubernetes文档详细解释了这些步骤:

切换到容器

切换到CRI-O 或者,如果你想继续使用Docker过去的版本1.23,按照cri-dockerd项目,它计划保持Docker作为一个可行的运行时选择。

结论

Kubernetes正在成长,但这种变化并不需要是一次创伤性的经历。大多数用户不必采取任何行动。对于那些这样做的人,还有时间测试和计划。

本文转载自: 掘金

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

Java代理模式之Java观察者模式 Java观察者模式

发表于 2021-11-26

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

Java观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

介绍

意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

如何解决:使用面向对象技术,可以将这种依赖关系弱化。

关键代码:在抽象类里有一个 ArrayList 存放观察者们。

应用实例: ① 拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。 ② 西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。

优点: ① 观察者和被观察者是抽象耦合的。 ② 建立一套触发机制。

缺点: ① 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 ② 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 ③ 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

使用场景:

• 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。

• 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。

• 一个对象必须通知其他对象,而并不知道这些对象是谁。

• 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

注意事项: ① JAVA 中已经有了对观察者模式的支持类。 ② 避免循环引用。 ③ 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。

实现

观察者模式使用三个类 Subject、Observer 和 Client。Subject 对象带有绑定观察者到 Client 对象和从 Client 对象解绑观察者的方法。我们创建 Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类。

ObserverPatternDemo,我们的演示类使用 Subject 和实体类对象来演示观察者模式。

步骤 1

创建 Subject 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码import java.util.ArrayList;
import java.util.List;

public class Subject {

private List observers
= new ArrayList();
private int state;

public int getState() {
return state;
}

public void setState(int state) {
this.state = state;
notifyAllObservers();
}

public void attach(Observer observer){
observers.add(observer);
}

public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}

步骤 2

创建 Observer 类。

1
2
3
4
csharp复制代码public abstract class Observer {
protected Subject subject;
public abstract void update();
}

步骤 3

创建实体观察者类。

1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码public class BinaryObserver extends Observer{

public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}

@Override
public void update() {
System.out.println( "Binary String: "
+ Integer.toBinaryString( subject.getState() ) );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码public class OctalObserver extends Observer{

public OctalObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}

@Override
public void update() {
System.out.println( "Octal String: "
+ Integer.toOctalString( subject.getState() ) );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码public class HexaObserver extends Observer{

public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}

@Override
public void update() {
System.out.println( "Hex String: "
+ Integer.toHexString( subject.getState() ).toUpperCase() );
}
}

步骤 4

使用 Subject 和实体观察者对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();

new HexaObserver(subject);
new OctalObserver(subject);
new BinaryObserver(subject);

System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}

步骤 5

执行程序,输出结果:

1
2
3
4
5
6
7
8
yaml复制代码First state change: 15
Hex String: F
Octal String: 17
Binary String: 1111
Second state change: 10
Hex String: A
Octal String: 12
Binary String: 1010

本文转载自: 掘金

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

Spring Cloud Gateway源码解析-09-结合

发表于 2021-11-26

示例

SCG配置

1
2
3
4
5
6
7
8
9
10
11
json复制代码spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 默认关闭
url-expression: "'lb://'+serviceId" #路由目标地址表达式,默认值就是"'lb://'+serviceId"
#配置nacos注册中心
nacos:
discovery:
server-addr: 127.0.0.1:8848 #地址

假如我们已经在nacos中注册了一个user-service,user-service有一个接口为/api/hello,当我们请求SCG /user-service/api/hello时就会请求到user-service。

在这里插入图片描述

原理

核心类之RouteDefinition的装配

在这里插入图片描述

补充了下之前的图
在这里插入图片描述

右上角红色的部分为结合注册中心涉及的类。从图中可以看到它们之间的关系,DiscoveryLocatorProperties与GatewayProperties类似用于读取discovery相关的配置,通过DiscoveryLocatorPropertis装配DiscoveryClientRouteDefinitionLocator,DiscoveryClientRouteDefinitionLocator是RouteDefinitionLocator的子类,也是用来存放RouteDefinition的,最终会同PropertiesRouteDefinitionLocator一样被组合到CompositeRouteDefinitionLocator中。

DiscoveryLocatorProperties和DiscoveryClientRouteDefinitionLocator
是在GatewayDiscoveryClientAutoConfiguration装配的。

DiscoveryLocatorProperties

DiscoveryRouteDefinition会使用PathRoutePredicateFactory和RewritePathGatewayFilterFactory,进行Path匹配和请求Path重写。

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
json复制代码@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
public class DiscoveryLocatorProperties {

//开启标识,默认关闭
private boolean enabled = false;

/**
* 路由ID前缀,默认为DiscoveryClient的类名称 {@link org.springframework.cloud.client.discovery.DiscoveryClient}
*/
private String routeIdPrefix;

//是否使用SpEL表达式
private String includeExpression = "true";

//用来创建路由Route的uri表达式,最终会被解析为类似uri=lb://user-service,可覆盖
private String urlExpression = "'lb://'+serviceId";

/**
* Option to lower case serviceId in predicates and filters, defaults to false. Useful
* with eureka when it automatically uppercases serviceId. so MYSERIVCE, would match
* /myservice/**
*/
private boolean lowerCaseServiceId = false;

private List<PredicateDefinition> predicates = new ArrayList<>();

private List<FilterDefinition> filters = new ArrayList<>();

GatewayDiscoveryClientAutoConfiguration

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
json复制代码public static List<PredicateDefinition> initPredicates() {
ArrayList<PredicateDefinition> definitions = new ArrayList<>();
// add a predicate that matches the url at /serviceId/**
PredicateDefinition predicate = new PredicateDefinition();
//设置Predicate名称,Path,DiscoveryRouteDefinition会使用PathRoutePredicateFactory
predicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class));
//设置Path参数,serviceId会在DiscoveryClientRouteDefinitionLocator#getRouteDefinition中替换为注册中心上的服务名,例如user-service
predicate.addArg(PATTERN_KEY, "'/'+serviceId+'/**'");
definitions.add(predicate);
return definitions;
}

public static List<FilterDefinition> initFilters() {
ArrayList<FilterDefinition> definitions = new ArrayList<>();

// add a filter that removes /serviceId by default
FilterDefinition filter = new FilterDefinition();
//设置使用的过滤器,此处使用RewritePathGatewayFilterFactory,因为后边会重写请求Path
filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));
//同Predicate,会在DiscoveryClientRouteDefinitionLocator#getRouteDefinition中将'service-id'替换为注册中心上的服务名,例如 /user-service/(?<remaining>.*)
String regex = "'/' + serviceId + '/(?<remaining>.*)'";
String replacement = "'/${remaining}'";
filter.addArg(REGEXP_KEY, regex);
filter.addArg(REPLACEMENT_KEY, replacement);
definitions.add(filter);

return definitions;
}

@Bean
public DiscoveryLocatorProperties discoveryLocatorProperties() {
DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();
//设置Predicate
properties.setPredicates(initPredicates());
//设置GatewayFilter
properties.setFilters(initFilters());
return properties;
}

结合注册中心其实有两种DiscoveryClient使用,一种是原始的DiscoveryClient,一种是ReactiveDiscoveryClient,不同的注册中心都有相应的实现,如nacos的NacosReactiveDiscoveryClient。可以通过配置spring.cloud.discovery.reactive.enabled=true来开启使用Reactive模式的。

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
json复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.cloud.discovery.reactive.enabled",
matchIfMissing = true)
public static class ReactiveDiscoveryClientRouteDefinitionLocatorConfiguration {

/**
*
* @param discoveryClient Reactive的实现,如果使用nacos,这里注入的为 {@link com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClient}
*/
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
ReactiveDiscoveryClient discoveryClient,
DiscoveryLocatorProperties properties) {
return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
}

}

/**
* @deprecated In favor of the native reactive service discovery capability.
*/
@Configuration(proxyBeanMethods = false)
@Deprecated
@ConditionalOnProperty(value = "spring.cloud.discovery.reactive.enabled",
havingValue = "false")
public static class BlockingDiscoveryClientRouteDefinitionLocatorConfiguration {

@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
}

}

DiscoveryClientRouteDefinitionLocator

DiscoveryClientRouteDefinitionLocator的主要工作是获取到所有的注册中心上的服务实例,根据服务信息创建PredicateDefnition->FilterDefinition->RouteDefinition。供CompositeRouteDefinitionLocator获取。
每一个服务都会生成一个RouteDefinition。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
json复制代码public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLocator {

private final DiscoveryLocatorProperties properties;

private final String routeIdPrefix;

private final SimpleEvaluationContext evalCtxt;

private Flux<List<ServiceInstance>> serviceInstances;

public DiscoveryClientRouteDefinitionLocator(ReactiveDiscoveryClient discoveryClient,
DiscoveryLocatorProperties properties) {
this(discoveryClient.getClass().getSimpleName(), properties);
//通过对应注册中心的discoveryClient获取到所有的服务实例
serviceInstances = discoveryClient.getServices()
.flatMap(service -> discoveryClient.getInstances(service).collectList());
}

private DiscoveryClientRouteDefinitionLocator(String discoveryClientName,
DiscoveryLocatorProperties properties) {
this.properties = properties;
//判断是否有路由ID前缀,如果没有则
if (StringUtils.hasText(properties.getRouteIdPrefix())) {
routeIdPrefix = properties.getRouteIdPrefix();
}
else {
routeIdPrefix = discoveryClientName + "_";
}
evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods()
.build();
}

@Override
public Flux<RouteDefinition> getRouteDefinitions() {

return serviceInstances.filter(instances -> !instances.isEmpty())
.map(instances -> instances.get(0)).filter(includePredicate)
.map(instance -> {
//创建RouteDefinition
RouteDefinition routeDefinition = buildRouteDefinition(urlExpr,
instance);

final ServiceInstance instanceForEval = new DelegatingServiceInstance(
instance, properties);

for (PredicateDefinition original : this.properties.getPredicates()) {
//根据服务信息重新构建PredicateDefinition
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName(original.getName());
for (Map.Entry<String, String> entry : original.getArgs()
.entrySet()) {
//将Path参数值的service-id替换为服务名称,如/user-service/**
String value = getValueFromExpr(evalCtxt, parser,
instanceForEval, entry);
predicate.addArg(entry.getKey(), value);
}
routeDefinition.getPredicates().add(predicate);
}

for (FilterDefinition original : this.properties.getFilters()) {
FilterDefinition filter = new FilterDefinition();
filter.setName(original.getName());
for (Map.Entry<String, String> entry : original.getArgs()
.entrySet()) {
//将Filter的regex -> '/' + serviceId + '/(?<remaining>.*)' 中的serviceId替换为服务ID 如user-service
String value = getValueFromExpr(evalCtxt, parser,
instanceForEval, entry);
filter.addArg(entry.getKey(), value);
}
routeDefinition.getFilters().add(filter);
}

return routeDefinition;
});
}

protected RouteDefinition buildRouteDefinition(Expression urlExpr,
ServiceInstance serviceInstance) {
//获取服务ID,默认小写
String serviceId = serviceInstance.getServiceId();
RouteDefinition routeDefinition = new RouteDefinition();
//设置路由ID
routeDefinition.setId(this.routeIdPrefix + serviceId);
//通过Spel解析器生成RouteUri
String uri = urlExpr.getValue(this.evalCtxt, serviceInstance, String.class);
routeDefinition.setUri(URI.create(uri));
//设置元数据信息,包括权重、健康状态等
routeDefinition.setMetadata(new LinkedHashMap<>(serviceInstance.getMetadata()));
return routeDefinition;
}
}

请求处理

RewritePathGatewayFilterFactory

上边讲到了当结合注册中心时SCG会为每个路由添加PathRoutePredicateFactory
和RewritePathGatewayFilterFactory。PathRoutePredicateFactory用来计算请求是否符合当前路由的条件,RewritePathGatewayFilterFactory用来重写请求Path,参数regexp=/user-service/(?<remaining>.*),replacement=$(remaining),例如请求的Path为/user-service/api/hello,会被重写为/api/hello。

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
json复制代码public class RewritePathGatewayFilterFactory
extends AbstractGatewayFilterFactory<RewritePathGatewayFilterFactory.Config> {

/**
* Regexp key.
*/
public static final String REGEXP_KEY = "regexp";

/**
* Replacement key.
*/
public static final String REPLACEMENT_KEY = "replacement";

public RewritePathGatewayFilterFactory() {
super(Config.class);
}

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(REGEXP_KEY, REPLACEMENT_KEY);
}

@Override
public GatewayFilter apply(Config config) {
String replacement = config.replacement.replace("$\\", "$");
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
ServerHttpRequest req = exchange.getRequest();
//每次进行重写时,都在上下文中保留一次原址的请求URI
addOriginalRequestUrl(exchange, req.getURI());
String path = req.getURI().getRawPath();
//根据配置的正则进行替换
// regexp=/user-service/(?<remaining>.*),replacement=$(remaining),例如请求的Path为/user-service/api/hello,会被重写为/api/hello。
String newPath = path.replaceAll(config.regexp, replacement);
//基于重写后的Path构建新的请求
ServerHttpRequest request = req.mutate().path(newPath).build();
//将新的请求URI放入上下文中,供后边的Filter使用
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());

return chain.filter(exchange.mutate().request(request).build());
}

@Override
public String toString() {
return filterToStringCreator(RewritePathGatewayFilterFactory.this)
.append(config.getRegexp(), replacement).toString();
}
};
}

RouteToRequestUrlFilter

在RewritePathGatewayFilterFactory重写完请求Path后会执行GlobalFilterRouteToRequestUrlFilter,该Filter在结合注册中心的情况下,主要是用来将RewritePathGatewayFilterFactory生成的新的request的scheme修改为路由的lb,例如RewritePathGatewayFilterFactory生成的请求URI为http://locahost:8080/api/hello,RouteToRequestUrlFilter会将其修改为lb://user-service/api/hello,供lbClicentFilter使用。

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
json复制代码@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
//判断上下中是否有GATEWAY_ROUTE_ATTR,在RoutePredicateHandlerMapping中放入的
//如果没有则不执行
if (route == null) {
return chain.filter(exchange);
}
log.trace("RouteToRequestUrlFilter start");
//获取请求的URI
URI uri = exchange.getRequest().getURI();
//判断是否包含编码的部分,如%
boolean encoded = containsEncodedParts(uri);
//获取Route的uri
URI routeUri = route.getUri();

//判断是否为其他类型的协议
if (hasAnotherScheme(routeUri)) {
// this is a special url, save scheme to special attribute
// replace routeUri with schemeSpecificPart
//将当前请求的schema放入上下文中
exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR,
routeUri.getScheme());
routeUri = URI.create(routeUri.getSchemeSpecificPart());
}
//如果RouteUri以lb开头,必须请求中带有host
if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
// Load balanced URIs should always have a host. If the host is null it is
// most
// likely because the host name was invalid (for example included an
// underscore)
throw new IllegalStateException("Invalid host: " + routeUri.toString());
}
//生成RequestURL,并放入上下文中
//使用RouteUri的scheme,如果使用lb的话,那么此处生成的mergedUrl则是lb://xxxxxx
URI mergedUrl = UriComponentsBuilder.fromUri(uri)
// .uri(routeUri)
.scheme(routeUri.getScheme()).host(routeUri.getHost())
.port(routeUri.getPort()).build(encoded).toUri();
//将新的URL放入请求上下文
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
return chain.filter(exchange);
}

}

LoadBalancerClientFilter

到目前为止,还没有获取到真正要调用的服务信息,LoadBalancerClientFilter就是做这件事的。

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
json复制代码@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
//如果不是lb的请求,则不执行
if (url == null
|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
// preserve the original url
//保留原始的请求地址
addOriginalRequestUrl(exchange, url);

if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url before: " + url);
}
//负载均衡获取真实的服务信息
final ServiceInstance instance = choose(exchange);

if (instance == null) {
throw NotFoundException.create(properties.isUse404(),
"Unable to find instance for " + url.getHost());
}

URI uri = exchange.getRequest().getURI();

// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
// if the loadbalancer doesn't provide one.
String overrideScheme = instance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
//使用最终调用服务信息构建URI
URI requestUrl = loadBalancer.reconstructURI(
new DelegatingServiceInstance(instance, overrideScheme), uri);

if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}
//将请求URI放入上下文,供NettyRoutingFilter使用
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
return chain.filter(exchange);
}

protected ServiceInstance choose(ServerWebExchange exchange) {
//此处调用RibbonLoadBalancer负载均衡获取真实服务信息
return loadBalancer.choose(
((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
}

动态路由刷新

事件机制。
在SCG中有RouteRefreshListener用来监听刷新的事件,比如Nacos使用NacosWatch来发送HeartbeatEvent。

1
2
3
4
5
6
7
json复制代码public void nacosServicesWatch() {

// nacos doesn't support watch now , publish an event every 30 seconds.
this.publisher.publishEvent(
new HeartbeatEvent(this, nacosWatchIndex.getAndIncrement()));

}

RouteRefreshListener中监听到HeartbeatEvent后会发送RefreshRoutesEvent,CachingRouteLocator中监听了该事件,而后触发DiscoveryClientRouteDefinitionLocator#getRouteDefinition从注册中心重新获取一次服务信息,生成RouteDefinition。

本文转载自: 掘金

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

Java代理模式之Java空对象模式 Java空对象模式

发表于 2021-11-26

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

Java空对象模式

在空对象模式(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查。Null 对象不是检查空值,而是反应一个不做任何动作的关系。这样的 Null 对象也可以在数据不可用的时候提供默认的行为。

在空对象模式中,我们创建一个指定各种要执行的操作的抽象类和扩展该类的实体类,还创建一个未对该类做任何实现的空对象类,该空对象类将无缝地使用在需要检查空值的地方。

实现

我们将创建一个定义操作(在这里,是客户的名称)的 AbstractCustomer 抽象类,和扩展了 AbstractCustomer 类的实体类。工厂类 CustomerFactory 基于客户传递的名字来返回 RealCustomer 或 NullCustomer 对象。

NullPatternDemo,我们的演示类使用 CustomerFactory 来演示空对象模式的用法。

步骤 1

创建一个抽象类。

1
2
3
4
5
csharp复制代码public abstract class AbstractCustomer {
protected String name;
public abstract boolean isNil();
public abstract String getName();
}

步骤 2

创建扩展了上述类的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码public class RealCustomer extends AbstractCustomer {

public RealCustomer(String name) {
this.name = name;
}

@Override
public String getName() {
return name;
}

@Override
public boolean isNil() {
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码public class NullCustomer extends AbstractCustomer {

@Override
public String getName() {
return "Not Available in Customer Database";
}

@Override
public boolean isNil() {
return true;
}
}

步骤 3

创建 CustomerFactory 类。

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

public static final String[] names = {"Rob", "Joe", "Julie"};

public static AbstractCustomer getCustomer(String name){
for (int i = 0; i < names.length; i++) {
if (names[i].equalsIgnoreCase(name)){
return new RealCustomer(name);
}
}
return new NullCustomer();
}
}

步骤 4

使用 CustomerFactory,基于客户传递的名字,来获取 RealCustomer 或 NullCustomer 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public class NullPatternDemo {
public static void main(String[] args) {

AbstractCustomer customer1 = CustomerFactory.getCustomer("Rob");
AbstractCustomer customer2 = CustomerFactory.getCustomer("Bob");
AbstractCustomer customer3 = CustomerFactory.getCustomer("Julie");
AbstractCustomer customer4 = CustomerFactory.getCustomer("Laura");

System.out.println("Customers");
System.out.println(customer1.getName());
System.out.println(customer2.getName());
System.out.println(customer3.getName());
System.out.println(customer4.getName());
}
}

步骤 5

执行程序,输出结果:

1
2
3
4
5
复制代码Customers
Rob
Not Available in Customer Database
Julie
Not Available in Customer Database

本文转载自: 掘金

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

性能监控之 JMeter 分布式压测轻量日志解决方案

发表于 2021-11-26

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

一、引言

在前文中我们已经介绍了使用JMeter非GUI模式进行压测的时候,我们可以使用 InfluxDB+Grafana 进行实时性能测试结果监控,也可以用 Tegraf+InfluxDB+Grafana 进行实现服务器性能监控。尽管Grafana看板可以显示事务执行的请求数和失败率。但是我们也想知道它失败的原因。

在这里插入图片描述

并不是所有的HTTP请求失败都是500引起的,有时候也可能是200,响应断言只是检查响应数据是否存在给定的字符串,如果不满足那么就是请求失败。但是这段时间我们实际的响应数据是什么?要知道在性能测试期间调试应用可是非常重要的。 我们经常使用阿里云或者物理机集群来压测,即使我们将响应数据记录在日志里面,我们也可能无法立即获取数据。我们只能等待压测结束去ssh/ftp访问主机去检查日志。我们不能像性能测试结果一样使用InfluxDB收集这些大量的非结构文本数据。因为InfluxDB作为时序数据库并不是为检索文本设计的。

其中一个简单的轻量日志解决方案就是使用ElasticSearch+FileBeats+Kibana去收集分析错误响应数据。

二、背景

1、Filebeat

Filebeat是ELK协议栈的新成员,一个轻量级开源日志文件数据搜集器,用GO语言实现。 Filebeat安装在服务器上做为代理监视日志目录或者特定的日志文件,要么将日志转发到Logstash进行解析,要么直接发送到ElasticSearch进行索引。 Filebeat文档完善,配置简单,天然支持ELK,为Apache,Nginx,System,MySQL等服务产生的日志提供默认配置,采集,分析和展示一条龙。

如下所示,Filebeat的配置简单易懂

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
yml复制代码filebeat:
spool_size: 1024 # 最大可以攒够 1024 条数据一起发送出去
idle_timeout: "5s" # 否则每 5 秒钟也得发送一次
registry_file: "registry" # 文件读取位置记录文件,会放在当前工作目录下。
config_dir: "path/to/configs/contains/many/yaml" # 如果配置过长,可以通过目录加载方式拆分配置
prospectors: # 有相同配置参数的可以归类为一个 prospector
-
fields:
log_source: "sample" # 类似logstash的 add_fields,此处的"log_source"用来标识该日志来源于哪个项目
paths:
- /var/log/system.log # 指明读取文件的位置
- /var/log/wifi.log
include_lines: ["^ERR", "^WARN"] # 只发送包含这些字样的日志
exclude_lines: ["^OK"] # 不发送包含这些字样的日志
-
document_type: "apache" # 定义写入ES时的 _type 值
ignore_older: "24h" # 超过24小时没更新内容的文件不再监听。
scan_frequency: "10s" # 每10秒钟扫描一次目录,更新通配符匹配上的文件列表
tail_files: false # 是否从文件末尾开始读取
harvester_buffer_size: 16384 # 实际读取文件时,每次读取16384字节
backoff: "1s" # 每1秒检测一次文件是否有新的一行内容需要读取
paths:
- "/var/log/apache/*" # 可以使用通配符
exclude_files: ["/var/log/apache/error.log"]
-
input_type: "stdin" # 除了 "log",还有 "stdin"
multiline: # 多行合并
pattern: '^[[:space:]]'
negate: false
match: after
output.elasticsearch:
hosts: ["127.0.0.1:9200"] # The elasticsearch host

Filebeat 发送的日志,会包含以下字段:

  • beat.hostname:beat运行的主机名
  • beat.name:shipper配置段设置的name,如果没设置,等于beat.hostname
  • @timestamp:读取到该行内容的时间
  • type 通过:document_type设定的内容
  • input_type:来自”log”还是”stdin”
  • source:具体的文件名全路径
  • offset:该行日志的起始偏移量
  • message:日志内容
  • fields:添加的其他固定字段都存在这个对象里面

2、Elasticsearch

Elasticsearch是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器。Elasticsearch强在全文搜索,InfluxDB擅长时序数据,所以还是具体需求具体分析。如果需要保存日志并经常查询的,Elasticsearch比较合适,比如我们的JMeter log。如果只依赖日志做状态展示,偶尔查询,InfluxDB比较合适。

3、Kibana

Kibana 是一个开源的分析和可视化平台,旨在与 Elasticsearch 合作。Kibana 提供搜索、查看和与存储在 Elasticsearch 索引中的数据进行交互的功能。用户可以轻松地执行高级数据分析,并在各种图表、表格和地图中可视化数据。Fibana在图表展示上没有Grafana美观,但Kibana从Elasticsearch中检索日志非常方便。

三、整体架构

在这里插入图片描述

四、日志采集架构

在这里插入图片描述

五、安装及配置

1、下载及配置ElasticSearch

可以直接参考官网的教程,此处就不重复造轮子了
官网教程地址:www.elastic.co/downloads/e…

安装完成后,确认可以通过使用http://elasticsearch-host-ip:9200访问elasticsearch

2、下载及配置Kibana

参考官网教程: www.elastic.co/downloads/k…

更新 config/kibana.yml 配置文件以获取 elasticsearch 数据
运行 kibana.bat/.sh 确保可以使用 http://kibana-host-ip:5601 访问kibana主页

3、下载及配置FileBeat

参考官网教程 www.elastic.co/downloads/b…
我们需要为每个压力机部署一个FileBeat节点,FileBeat主要负责收集日志数据,并发送给elasticsearch存储。

更新filebeat.yml文件

1
2
3
4
5
6
7
8
9
10
yml复制代码filebeat.inputs:
- type: log
enabled: true
paths:
- D:\BaiduNetdiskDownload\jmeter\apache-jmeter-4.0\bin\jmeter.log
multiline.pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}
multiline.negate: true
multiline.match: after
output.elasticsearch:
hosts: ["127.0.0.1:9200"]

默认情况下,FileBeat将日志文件中的每一行记录为单独的日志条目。有时JMeter异常可能跨越多行。所以我们需要使用多行模式配置filebeat.yml。

在这里插入图片描述

JMeter.log每个日志条目都带有其时间戳(yyyy-MM-dd)。所以,我们可以将模式配置为从时间戳开始截取,如果没有时间戳,FileBeat可以根据配置将该行附加到上一行。

启动FileBeat后将开始监视日志文件,每当更新日志文件时,数据将被发送到ElasticSearch存储。

六、JMeter日志采集

我们创建了一个非常简单的测试,如下所示,只有有Debug Sampler,使用BeanShell Assertion监听在发生任何错误时在日志文件中写入返回数据。

在这里插入图片描述

压测开始后,FileBeat将开始收集从日志文件中的信息,并转发到ElasticSearch存储,我们可以通过Kibana检索详细日志。

在这里插入图片描述

如果我们点击小箭头展开细节,下面的消息部分将显示我们感兴趣的日志详细内容。

在这里插入图片描述

七、小结

除了实时性能测试结果和实时性能数据外,我们还能够实时收集失败请求的响应数据。当我们在长时间运行的分布式负载测试时,上述设置非常有用。当请求事务突然失败时,此设置可帮助我们检查响应数据以便了解应用的情况和测试工具行为。

本文只抛砖引玉,大家有兴趣的话,可以参照教程深入实践。

相关资料:

  • github.com/zuozewei/bl…

本文转载自: 掘金

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

(翻译)性能监控之百分位数监控

发表于 2021-11-26

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

原文链接:www.adfpm.com/adf-perform…

一、前言

在性能监控中什么是最好的度量—平均数还是百分位数?从统计学上讲,有很多方法可以确定应用程序提供的整体体验有多好。平均值被广泛使用。它们很容易理解和计算——但它们可能会产生误导。
这篇文章是关于百分位数的。我将解释什么是百分位数,以及如何使用它们更好地理解应用程序性能。与平均值相比,百分位数告诉我们应用程序响应时间有多一致。百分位数可以做出很好的近似,可用于趋势分析,SLA 协议监视以及每天评估/对性能进行故障排除。

服务级别协议(英语:service-level agreement,缩写 SLA)也称服务等级协议、服务水平协议,是服务提供商与客户之间定义的正式承诺。SLA的概念,对互联网公司来说就是网站服务可用性的一个保证。

二、平均值如何造成误导

在这里插入图片描述

我们可以从平均数得出错误的结论。例如:让我们假设一个国家的工人的平均月薪在 2000 美元左右(这似乎不算太坏)。然而,仔细观察我们就会发现,这个国家的大多数人都是外来务工人员,也就是 10 个人中有 9 个人是外来务工人员。他们只赚 1000 美元左右。每 10 个(当地居民)中就有 1 个月能挣11000美元左右(这太简单了,但你懂的)。如果你计算一下,你会发现这个数字的平均值确实在 2000 年左右,但我们都能理解,这并不代表一个现实的“平均”工资。这也适用于统计监控应用程序性能和监控 SLA 协议。非常高的值对平均值的影响非常大。在现实中,大多数应用程序都有一些非常重要的异常值,这些异常值对平均值的影响很大。

三、百分位数说明

当您想从高级角度了解应用程序的执行情况时,理解百分位数的概念是很有用的。百分位是统计中使用的一种度量,表示一组观察中某一特定百分比的观察值低于该值。例如,低于 90 %响应时间值的 HTTP 请求的响应时间称为 9 0百分位响应时间。下面的截图是 3.0 秒(所以 90 %的请求都是在 3.0 秒或更短的时间内处理的:

在这里插入图片描述

要获得某个单击操作的 90 %响应时间值,请按递增顺序对该单击操作发起的请求的所有响应时间值进行排序。把这一组的前 90 %拿出来。该集合中具有最大值的响应时间是单击操作请求的 90 %。

假设对于一个单击操作,有 10 个HTTP响应时间值可用:1、2、3、4、5、6、7、8、9和 10 秒。排序之后,如果我把 90 %的响应时间值作为一个单独的集合取出来,我将得到:1、2、3、4、5、6、7、8和 9。这里的 9 是最大值,因此是该点击操作的 90 %值。

当然,我们希望尽可能多的 HTTP 请求都有非常快的响应时间;所以,在一个理想的世界里,第 50、95、99 甚至是第 100 百分位的人会尽可能快。

四、百分比在性能监控

请看 2018 年 6月月度概述的百分位数图表(右下角):

在这里插入图片描述

图中用蓝色表示平均响应时间,用黑色、灰色和浅灰色绘制第 50、90 和 95 百分位数:

在这里插入图片描述

x 轴为 2018 年 6 月的天数,y 轴为 HTTP 响应时间(以秒为单位)。

我们可以看到以下模式:

  • 第 50 百分位的响应时间大约是 1 秒(对于网页中的某个点击动作)。这意味着 50 %的 HTTP 请求在 1 秒或更短的时间内得到处理。
  • 第 90 百分位大约是 2.75 秒( 90 %在 2.75 秒内处理)
  • 第 95 百分位在 3.25 秒内达到最大值(95 %在 3.25 秒内处理)
  • 平均响应时间大约是 2.0 秒(蓝线)。周二(6月5日、12 日、19 日和 26 日)的峰值约为 2.5 秒
  • 周末的平均响应时间比工作日( 2.0 秒)低 1.6 秒。
  • 我们可以看到,在周二,当平均反应时间达到峰值时,而第 50、90 和 95 百分位则更稳定。

这告诉我们什么?

  • 可能有一些非常慢的请求(外围程序)对平均值有很大的影响。在这种情况下,最终用户在星期二运行许多非常慢的报告。周二是一种“报告日”,平均响应时间“混乱”。
  • 这完全取决于我们的 SLA 协议以及我们的应用程序必须执行得多好。如果对于您的应用程序或 SLA 协议,有许多响应时间在2.0 到 3.25 秒之间的 HTTP 请求是可以接受的,那么您可能做得很好。然后,除了分析异常缓慢的请求( HTTP 请求中耗时超过 3.25 秒的 5 %)并确定是否可以提高它们的速度外,您无需做太多工作。
  • 如果您需要在 2.0 秒内完成大多数 HTTP 请求,那么您需要做大量的工作来优化您的系统,因为如此多的请求花费的时间超过2.0 秒。

五、月概述-活跃用户和会话

一个关于活动终端用户和 HTTP 会话的图表——这对于评估一个托管服务器上活动的终端用户和会话数量或所有托管服务器上活动的终端用户和会话数量非常有用。稍后,我们可以将这些值性能监控图中的所有其他指标进行比较,如 JVM、SLA 协议指标、在层中花费的时间等,但现在还可以将其与百分比进行比较:

在这里插入图片描述

x 轴为 2018 年 6 月的天数,y 轴为活动会话数和最终用户数:

在这里插入图片描述

我们可以看到以下模式:

  • 对于大多数终端用户和会话来说,周二是最繁忙的日子;我们在 2018 年 6 月 5 日、12日、19 日和 26 日看到峰值
  • 在最繁忙的一天(6月19日),有超过80个唯一的 HTTP 会话处于活动状态,70 个唯一的最终用户。
  • 周末很少有终端用户活动(大约 10 个独立终端用户,大约 15 次会话)

六、趋势分析

我们可以在各种绩效评估中使用百分位数。特别是对于新版本发布后的回归和趋势分析。我们真的提高了性能吗?有时在新版本发布后性能会上升或下降——如果我们能够看到并认识到这一点将会很有用。如果是的话,第 50、90 和 95 百分位线应该在您提高生产性能后减少——这意味着更快的响应时间:

在这里插入图片描述

如图所示。6月17日发布了一个新的版本,据说性能有所改善。在那之后,在6月剩下的几天里,我们看到平均响应时间,第 50、90 和 95 百分位数下降了——这表明新版本确实提高了性能。

七、周、日、小时概述

与每月的方式相同,周、日和小时的终端用户/会话和百分比概述。以下是一个关于 Day 概述的例子:

在这里插入图片描述

八、结论

与平均值相比,百分位数告诉我们应用程序响应时间有多一致。
当平均响应时间看起来非常高,单个数据集看起来很正常时,这对于在不受异常缓慢请求影响的情况下分析性能非常有用。
百分位数非常适合用于趋势分析、SLA 协议监控和日常性能评估。

本文转载自: 掘金

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

【python】函数装饰器 与 常用装饰器 函数装饰器 标准

发表于 2021-11-26

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

函数装饰器

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

举个栗子:

1
2
3
4
5
6
7
8
9
python复制代码@decorate
def target():
print('running target()')

# 相当于
def target():
print('running target()')

target = decorate(target)

装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,它们在被装饰的函数定义之后立即运行;这通常是在导入时(即 Python 加载模块时)。

装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。

大多数装饰器会在内部定义一个函数,然后将其返回。

标准库中的装饰器

Python 内置了三个用于装饰方法的函数:property 、classmethod 和 staticmethod 。

另一个常见的装饰器是 functools.wraps ,它的作用是协助构建行为良好的装饰器。标准库中最值得关注的两个装饰器是 lru_cache 和 singledispatch (Python 3.4 新增)。 这两个装饰器都在 functools 模块中定义。

functools.lru_cache

functools.lru_cache 实现了备忘 (memoization)功能。它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU 三个字母是 “Least Recently Used” 的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。

lru_cache 可以使用两个可选的参数来配置。它的签名是:

1
python复制代码functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。 typed 参数如果设为 True ,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0 )区分开。

因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。

functools.singledispatch

因为 Python 不支持重载方法或函数,所以我们不能使用不同的签名定义同一函数名的函数的变体,也无法使用不同的方式处理不同的数据类型。在 Python 中,一种常见的做法是使用一串 if/elif/elif ,调用专门的函数,如 functionA_str 、functionA_int ,等等。这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数 functionA 会变得很大,而且它与各个专门函数之间的耦合也很紧密。

functools.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function): 根据第一个参数的类型,以不同方式执行相同操作的一组函数。

可以在系统的任何地方和任何模块中注册专门函数。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码from functools import singledispatch
import numbers
import html

@singledispatch # 标记处理 object 类型的基函数
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)

@htmlize.register(str) # 各个专门函数使用 @«base_function».register(«type») 装饰。
def _(text): # 专门函数的名称无关紧要;_ 是个不错的选择
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)

参数化装饰器

让装饰器接受其他参数:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码registry = set()

def register(active=True): # 接受一个可选的关键字参数
def decorate(func): # 真正的装饰器
print('running register(active=%s)->decorate(%s)' % (active, func))

if active: # 只有 active 参数的值(从闭包中获取)是 True 时才注册 func
registry.add(func)
else: # 如果 active 不为真,而且 func 在 registry 中,那么把它删除
registry.discard(func)
return func # decorate 是装饰器,必须返回一个函数

return decorate

@register(active=False) # @register 工厂函数必须作为函数调用,并且传入所需的参数
def f1():
print('running f1()')

@register() # 即使不传入参数,register 也必须作为函数调用,即要返回真正的装饰器 decorate
def f2():
print('running f2()')

本文转载自: 掘金

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

【第三方库】Captcha captcha

发表于 2021-11-26

captcha

文档:官方

这个第三方的库挺有意思的。他具备很多优点:

  • 可自定义验证码长度
  • 生成的图片不需要额外内存存储,通过 path 生成即可
  • 使用本地 cache,可 reload 验证码
  • 通过hash生成随机数作为验证码 ID

快速使用

这个第三方库,他已经将很多东西进行了封装,你只需要按照你的需求进行调用。

生成验证码 ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码type Captcha struct {
Id string `json:"id,omitempty"` //验证码ID
ImgUrl string `json:"img_url,omitempty"` //验证码图像地址
Refresh string `json:"refresh,omitempty"` //重新获取
Verify string `json:"verify,omitempty"` //验证
}

//GenerateId
/* @Description: 生成验证码Id
* @receiver c
* @param ctx
*/
func (c *Captcha) GenerateId(ctx *gin.Context) {
//获取验证码验证数字长度
var length = variable.ConfigYml.GetInt("Captcha.Length")
//自定义验证码数字长度
captchaId := captcha.NewLen(length)
//提供相关信息
c.Id = captchaId
c.ImgUrl = captchaId + ".png"
c.Refresh = c.ImgUrl + "?reload=1"
c.Verify = captchaId + "/这里替换为正确的验证码进行验证"
response.Success(ctx, "验证码信息", c)
}

ID 可以生成相对应的验证码图片,然后以键的形式去校验验证码。这个还是很重要的。

获取图片

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
go复制代码//GetImg
/* @Description: 生成验证码的图片
* @receiver c
* @param ctx
*/
func (c *Captcha) GetImg(ctx *gin.Context) {
//通过param获取验证码Id
captchaId := ctx.Param(captchaIdKey)
//根据路径的最后一个斜杠, 将路径划分为目录部分和文件名部分。 例 http://localhost:20201/captcha/ JUpvESBqAcPxvSVz5Cy9.png
_, file := path.Split(ctx.Request.URL.Path)
//获取扩展名 .png
ext := path.Ext(file)
//获取路径上的Id
id := file[:len(file)-len(ext)]
//验证参数是否获取正常
if ext == "" || captchaId == "" {
response.Fail(ctx, http.StatusBadRequest, consts.CaptchaGetParamsInvalidCode, consts.CaptchaGetParamsInvalidMsg)
return
}
//当获取到 Reload 就重新加载一个
if ctx.Query(Reload) != "" {
//重载为给定的验证码生成并记住新的数字。
captcha.Reload(id)
}
//设置http协议
ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.Header("Expires", "0")
var vBytes bytes.Buffer
if ext == ".png" {
ctx.Header("Content-Type", "image/png")
//写入图片的二进制信息
_ = captcha.WriteImage(&vBytes, id, Width, Height)
//读取文件内容并输出的方法
http.ServeContent(ctx.Writer, ctx.Request, id+ext, time.Time{}, bytes.NewReader(vBytes.Bytes()))
}
}

Captcha 他通过验证码 ID 去生成验证码图像的二进制,你只需要将二进制信息放到 http 协议中,然后进行显示就可,这个操作不需要你利用额外的空间存储图片。

校验验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码//CheckCode
/* @Description: 校验验证码传来的数字
* @receiver c
* @param ctx
*/
func (c *Captcha) CheckCode(ctx *gin.Context) {
//获取验证码ID 与验证码的值
captchaId := ctx.Param(captchaIdKey)
value := ctx.Param(captchaValueKey)
//去存储器中进行验证
if captcha.VerifyString(captchaId, value) {
response.Success(ctx, consts.CaptchaCheckParamsOk)
} else {
response.Fail(ctx, http.StatusBadRequest, consts.CaptchaCheckParamsInvalidCode, consts.CaptchaCheckParamsInvalidMsg)
}
}

根据请求路径获取的字段值,Captcha他会将 captchaId 与 value 放到本地的存储器中进行查询,如果可以查到就返回 true。

创建路由

然后你就可以直接创建验证码的路由。

1
2
3
4
5
6
7
8
go复制代码	// 创建一个验证码路由
verifyCode := router.Group("captcha")
{
// 验证码业务,该业务无需专门校验参数,所以可以直接调用控制器
verifyCode.GET("/", (&captcha.Captcha{}).GenerateId) // 获取验证码ID
verifyCode.GET("/:captcha_id", (&captcha.Captcha{}).GetImg) // 获取图像地址
verifyCode.GET("/:captcha_id/:captcha_value", (&captcha.Captcha{}).CheckCode) // 校验验证码
}

这个整体流程其实不难。主要是明白,他这个流程是怎么样的!

扩展

更改存储器

他设置了两个常量 CollectNum 和 Expiration 。

  • CollectNum 代表创建验证码的数量会去触发垃圾回收。(一次清理过期验证码的数量)
  • Expiration 代表验证码的过期时间
1
2
3
4
5
6
7
8
9
10
11
go复制代码func init() {
//对存储器进行自定义设置
//设置一次清理过期验证码的数量
collectNum := variable.ConfigYml.GetInt("Captcha.CollectNum")
//过期时间
expiration := variable.ConfigYml.GetDuration("Captcha.Expiration")
// 返回一个新的标准内存存储器
s := captcha.NewMemoryStore(collectNum, expiration)
//设置一个新的存储器
captcha.SetCustomStore(s)
}

创建音频验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
go复制代码func (c *Captcha) GetAudio(ctx *gin.Context) {
//通过param获取验证码Id
captchaId := ctx.Param(captchaIdKey)
//根据路径的最后一个斜杠, 将路径划分为目录部分和文件名部分。 例 http://localhost:20201/captcha/ JUpvESBqAcPxvSVz5Cy9.wav
_, file := path.Split(ctx.Request.URL.Path)
//获取扩展名 .wav
ext := path.Ext(file)
//获取路径上的Id
id := file[:len(file)-len(ext)]
//验证参数是否获取正常
if ext == "" || captchaId == "" {
response.Fail(ctx, http.StatusBadRequest, consts.CaptchaGetParamsInvalidCode, consts.CaptchaGetParamsInvalidMsg)
return
}
ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.Header("Pragma", "no-cache")
ctx.Header("Expires", "0")
//当获取到 Reload 就重新加载一个
if ctx.Query(Reload) != "" {
//重载为给定的验证码生成并记住新的数字。
captcha.Reload(id)
}
var vBytes bytes.Buffer
if ext == ".wav" {
ctx.Header("Content-Type", "audio/wav")
//写入audio的二进制信息
_ = captcha.WriteAudio(&vBytes, id, Lang)
//读取文件内容并输出的方法
http.ServeContent(ctx.Writer, ctx.Request, id+ext, time.Time{}, bytes.NewReader(vBytes.Bytes()))
}
}

参考文章

captcha验证码总结

Golang Http 验证码示例

本文转载自: 掘金

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

正则表达式 与 XPath 语法领域细解,初学阶段的你,该怎

发表于 2021-11-26

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

在 Python 爬虫采集领域,正则表达式到底要学多深?
同理,XPath 表达式要学多少才够用?
这两个问题是很多爬虫新人的疑问,而且这两个问题还没有标准答案……

本篇博客为你梳理,在初学爬虫阶段,二者你应该学到何种地步,给你定一个小目标,即初学的范围。

正则表达式

正则表达式在 Python 中,主要配合 re 模块使用,该模块应用难度不大,但正则表达式编写却困扰很多朋友。

正则难写的原因如下:

  1. 它也算是一门独立的编程语言,有自己的规范;
  2. 正则表达式独立于任何编程语言,也表示它可以和任意编程语言结合;
  3. 每个人写出的表达式不一致,即“每题”的答案都不唯一;
  4. 正则有语法,有修饰符,有元字符,有运算符的优先级,初学阶段概念有点庞杂,无从下手。

既然已经分析到正则难写的原因,那接下来逐一克服即可。

首先要明确第一个概念,正则是干什么的?

简单理解,正则表达式就是一种文本(字符串)的检索模式,它可以从一个长字符串中,匹配出目标字符串。

例如从 abc1234dfertg 中提取 1234。

初学正则

初学阶段,上来要做的事情是了解正则的基本语法,从字符开始进行学习。

普通字符
这个是非常简单的,例如 a,1 就是一个普通字符,应用到正则表达式里面,就可以去匹配指定字符串的 a 或者 1。

顺着字符就扩展出来正则的核心内容了,元字符。

元字符
元字符就是,在正则中有特殊的含义的一些语法表述。

常见的元字符如下:

  • \d:匹配一个数字;
  • \w:匹配字母、数字、下划线;

这里就会出现一个学习阶段的门槛,记忆元字符。

在初学阶段,尤其是初学 Python 爬虫采集阶段,那按照如下顺序记忆即可(必须掌握)。

  • .:匹配除换行符(\n、\r)之外的任何单个字符,一般比较简单的正则用 . 能匹配大多数内容了;
  • *:匹配前面的表达式 0 次~无限次;
  • +:匹配前面的表达式 1 次~无限次;
  • ?:匹配前面的子表达式 0 次或 1 次,这个 ? 还有一个用途为【当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的,非贪婪模式表示尽可能少的匹配所搜索的字符串】,这个有点绕,后文有参考案例;
  • \:转义符,例如你想要匹配 .,需要用 \.;
  • \s:注意是小写 s,匹配任何空白字符,包括空格、制表符、换页符等等,在解析 HTML 的时候,很常用,因为网页源码中经常出现换行情况;
  • [xyz]:匹配中框好中的任意一个字符;
  • (pattern):分组,并匹配出 pattern;

掌握并熟练的使用以上 8 个元字符,在初学爬虫阶段,一般的网页就能畅通无阻的进行解析了。

解释一下贪婪模式。

如果存在一个字符串为 www.csdn.com,你编写了正则如下:w+,此时能匹配到 www,该正则会尽可能多的去匹配 w 字符,当修改正则为:w+? 时,匹配到的结果为 w,即尽可能少的匹配,也就是 元字符 ? 使得正则变为了非贪婪模式。

初学正则在爬虫中的落地

有了上述元字符概念,你在去复盘爬虫 120 例之前的正则部分文章,会发现出现过很多次如下正则: (.*?),此时就能理解其含义,并且你也能知道,这是最普通的正则表达式,只能算是偷懒之作,但是写起来确实方便。

如果碰到的网页出现了换行或者空格,那上述正则将演变为 (.|\s)*?,结合上文的必背元字符,你能理解其含义吗?

理解不了去学习就对了,毕竟我们又引入了一个元字符 |,.|\s 表示选择,匹配 . 或 \s,即匹配任意字符在加上空格。

基本的元字符熟悉之后,才能进行更多的扩展,学习其它元字符,使得你的正则表达式写的标准与高效。

除了元字符外,对于正则还需要学习的是修饰符,该内容不多,有如下几种:

  • i:忽略大小写;
  • g:全局匹配;
  • m:多行匹配;
  • s:. 圆点符号,支持匹配空格。

这些内容不重点讲解的原因是:不同的编程语言有其特殊的实现方式,具体需要依旧语言本身来实现,例如 Python 的 re 模块,就有针对性的实现,你可以去检索 re 模块相关用法。

其余进阶内容,请重点围绕正则分组进行学习,该部分将在后期总结篇展现。

XPath 表达式

XPath 即 XML Path,一种在 XML 文档中查找节点元素的语言。

如果深入研究,XPath 依旧有非常多的知识点需要补充,但是作为初学爬虫采集,优先掌握如下内容即可。

初学阶段必会语法

XPath 路径表达式

这个路径与电脑硬盘路径获取基本一致。

先区分好 / 与 //,它们分别表示从根节点选择,或者从任意位置的某个节点进行选择。

例如存在如下 XML 文档,根阶段为 root,其它内容如下所示

1
2
3
4
5
6
7
8
xml复制代码<root>
<book bid="1">
<author>橡皮擦(擦姐)</author>
</book>
<book bid="2">
<author>橡皮擦(擦哥)</author>
</book>
</root>

例如 /root/book,表示从根节点开始选择 book 节点,如果使用 /book,则无法匹配到任何数据。

使用 //book,则可以匹配到所有 book 元素。

直接使用 book 也可以匹配出所有的 book 节点。

XPath 测试方法,可以创建一个 HTML 文件,在后在开发者工具中使用 Ctrl+F 换出搜索框,即可测试,如下图所示。
正则表达式 与 XPath 语法领域细解,初学阶段的你,该怎么学?

当然浏览器会自动生成 HTML ,HEAD,BODY 节点,正常读取即可。

有了根节点概念之后,就可以类别出 . 表示当前节点,.. 表示当前节点父节点。当然还有 @ 可以选择某个属性,例如下述 XPath 表示提取 book 节点中 bid=1 的节点,语法格式如下:

1
xpath复制代码/html/body/root/book[@bid=1]

正则表达式 与 XPath 语法领域细解,初学阶段的你,该怎么学?
提炼一下语法格式如下:

1
xpath复制代码标签名[@属性=‘属性值’]	 # 如果是属性值为数字,去掉双引号也是可以的

如果直接使用 @属性,表示提取具备该属性的节点。

其它选择元素的方法

选择未知元素

在 XPath 中,可以使用 * 选择未知的节点,例如 /book/*/name,表示选择 book 节点下所有节点的 name 节点。

选择谓语

谓语表示查找某个特定的节点,或者包含某个指定的值的节点,谓语嵌套在 [] 中,例如:

选择第一个元素 /root/book[1],选择最后一个元素 /root/book[last()],上文提及的属性选择,也属于谓语的一种用法。

提取属性值或者标签中的文本值

在爬虫采集的时候,经常会用到提取标签的属性值,或者提取标签内部的文本值,提取标签属性值可参考如下案例:/book/@cid,提取标签文本,可参考案例为://book/text()。

以上内容即为初学阶段需要掌握的 XPath 相关知识,当然多多益善,有的地方会告诉你可以直接从开发者工具复制 XPath,如下所示:

正则表达式 与 XPath 语法领域细解,初学阶段的你,该怎么学?

上述办法复制出来的 XPath 表达式,冗余内容非常多,建议还是自己编写完成。

下述内容为直接复制出的表达式。

1
python复制代码/html/body/div[2]/div[5]

收藏时间

本文为《爬虫 120 例》的第 19 篇博客,虽然没有实际采集某个站点,但为你梳理了学习爬虫初期,正则表达式与 XPath 该学习哪些内容,相信这篇文章一定利于下个阶段的学习~

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

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

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

本文转载自: 掘金

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

1…173174175…956

开发者博客

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