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

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


  • 首页

  • 归档

  • 搜索

NumPy-快速上手 NumPy-快速上手

发表于 2021-11-17

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

NumPy-快速上手

数组的创建

NumPy的特点是其N维数组对象ndarray。

ndarray是一系列同类型数据的集合。

ndarray对象用于存放同类型元素的多维数组。

1
2
3
4
python复制代码import numpy as np
a=np.array([1,2,3])
print(a)
print(type(a))

image-20211116150331995

创建区间数组

arange参数(初始值,结束值,步长) 不包含结束值

1
2
3
4
5
python复制代码import numpy as np
a=np.arange(1,10,3)
print(a)
b=np.arange(1,10,0.5)
print(b)

image-20211116150606860

创建二维数组

ndim 维度

shape 行数和列数

size 元素个数

1
2
3
4
5
6
python复制代码import numpy as np
arr=np.array([[1,2],[4,5],[7,8]])
print(arr)
print(arr.ndim)
print(arr.shape)
print(arr.size)

image-20211116151219055

指定每个维度的长度

ones为生成全1数组

1
2
3
python复制代码import numpy as np
arr=np.ones(shape=(3,4,5))
print(arr)

image-20211116151531068

数组属性

数组对象的常用属性有:ndim、shape、dtype、size和itemsize。

ndim用来返回数组的维度

shape用来返回数组的形状

dtype用来返回数组的数据类型

size用来返回数组中元素的个数

itemsize用来返回数组中每个元素占用的空间大小(以字节为单位)

1
2
3
4
5
6
7
python复制代码import numpy as np
arr=np.array([[1,2],[3,6]])
print(arr.ndim)
print(arr.shape)
print(arr.dtype)
print(arr.size)
print(arr.itemsize)

image-20211116152528919

数据类型

在创建数组时,可以使用dtype来指定数组中元素的类型。

没有指定元素的类型,则会根据元素类型进行推断。

若元素的类型不同,则会选择一种兼容的类型。

1
2
3
4
5
python复制代码import numpy as np
a=np.array([1,5,9.0],dtype=np.float32)
b=np.array([1,'a',2])
print(a.dtype)
print(b.dtype)

image-20211116153838657

astype( )进行类型转换

1
2
3
4
5
6
python复制代码import numpy as np
a=np.array([1,5,9],dtype=np.string_)
print(a)
a=a.astype(np.uint)
print(a)
print(a.dtype)

image-20211116154146610

reshape( )方法改变数组的形状。

1
2
3
4
5
python复制代码import numpy as np
a=np.arange(15)
print(a)
b=np.reshape(a,(3,5))
print(b)

image-20211116154642905

设置多维

维度-1表示自动计算该维度的大小

1
2
3
4
5
python复制代码import numpy as np
a=np.arange(20)
print(a)
b=np.reshape(a,(-1,2,5))
print(b)

image-20211116154906477

索引与切片

选取多个元素

1
2
3
4
5
6
7
python复制代码import numpy as np
a=np.arange(20)
print(a)
print(a[0],a[10])
a=a.reshape((4,5))
print(a)
print(a[2,0])

image-20211116155918621

切片返回的是原数组对象的视图

1
2
3
4
5
6
python复制代码import numpy as np
a=np.arange(20)
b=a[0:5]
a[2]=33
print(a)
print(b)

image-20211116160752059

如果希望数组能够实现真正的复制 copy( )返回数组的副本

1
2
3
4
5
6
7
8
python复制代码import numpy as np
a=np.arange(20)
print(a)
b=a.copy()
b=b[0:5]
a[2]=33
print(a)
print(b)

image-20211116161107841

条件索引

1
2
3
4
5
python复制代码import numpy as np
a=np.arange(20)
print(a)
b=a[a%2==0]
print(b)

image-20211116161520733

数组扁平化

ravel( )返回的是原数组的视图

flatten( )返回的是原数组的副本

1
2
3
4
5
6
7
8
python复制代码import numpy as np
a=np.arange(15).reshape(3,5)
b=a.ravel()
c=a.flatten()
a[0,0]=1
print(a)
print(b)
print(c)

image-20211116161741783

存储顺序

order参数来指定数组元素的存储顺序

1
2
3
4
5
6
7
python复制代码import numpy as np
a=np.array([[1,2,3],[4,5,6]])
b=a.reshape((3,2),order="C")
c=a.reshape((3,2),order="F")
print(a)
print(b)
print(c)

image-20211116162638906

函数

统计

当数组是二维数组时,axis=0按照竖直的方向进行统计, axis=1按照水平的方向进行统计

1
2
3
4
python复制代码import numpy as np
a=np.array([[1,2,3],[4,5,6]])
print(np.sum(a,axis=0))
print(np.sum(a,axis=1))

image-20211116173111846

常用的统计函数如下有:

①mean( )/sum( )/median( )。平均值/合/中位数

②max( )/min( )/amax( )/amin( )。最大值/最小值/最大值/最小值

③argmax( )/argmin( )/std( )/var( )。最大值的索引/最小值的索引/标准差/方差

④cumsum( )/cumprod( )。累加/累积

随机

常用的随机函数有:

①np.random.rand( )

②np.random.random( )

③np.random.randn( )

④np.random.normal( ) 高斯分布的概率密度函数

⑤np.random.randint( ) 随机整型数

⑥np.random.seed( ) 随机数种子

⑦np.random.shuffle( ) 随机排列

⑧np.random.uniform( ) 随机采样

1
2
3
4
5
6
7
python复制代码import numpy as np
a=np.random.rand(2,2)
b=np.random.random(size=(2,2))
c=np.random.randn(2,2)
print(a)
print(b)
print(c)

image-20211116183811821

连接

concatenate( )对多个数组按指定轴的方向进行连接

1
2
3
4
5
6
7
python复制代码import numpy as np
a=np.arange(6).reshape((2,3))
b=np.arange(6).reshape((2,3))
print(a)
print(b)
print(np.concatenate((a,b),axis=0))
print(np.concatenate((a,b),axis=1))

image-20211116184547675

其他

any( ):如果数组中有任何一个元素为True(或者能转换为True),则返回True,否则返回False。

all( ):如果数组中所有元素为True(或者能转换为True),则返回True,否则返回False。

transpose(T)函数在不指定参数时,默认是矩阵转置。

指定参数transpose((0,1))表示按照原坐标轴改变序列,也就是保持不变。

transpose((1,0))表示交换0轴和1轴。

1
2
3
4
python复制代码import numpy as np
a=np.arange(6).reshape((3,2))
print(a)
print(a.transpose())

image-20211116195529739

本文转载自: 掘金

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

leetcode 1910 Remove All Occu

发表于 2021-11-17

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

描述

Given two strings s and part, perform the following operation on s until all occurrences of the substring part are removed:

  • Find the leftmost occurrence of the substring part and remove it from s.
    Return s after removing all occurrences of part.

A substring is a contiguous sequence of characters in a string.

Example 1:

1
2
3
4
5
6
7
ini复制代码Input: s = "daabcbaabcbc", part = "abc"
Output: "dab"
Explanation: The following operations are done:
- s = "daabcbaabcbc", remove "abc" starting at index 2, so s = "dabaabcbc".
- s = "dabaabcbc", remove "abc" starting at index 4, so s = "dababc".
- s = "dababc", remove "abc" starting at index 3, so s = "dab".
Now s has no occurrences of "abc".

Example 2:

1
2
3
4
5
6
7
8
ini复制代码Input: s = "axxxxyyyyb", part = "xy"
Output: "ab"
Explanation: The following operations are done:
- s = "axxxxyyyyb", remove "xy" starting at index 4 so s = "axxxyyyb".
- s = "axxxyyyb", remove "xy" starting at index 3 so s = "axxyyb".
- s = "axxyyb", remove "xy" starting at index 2 so s = "axyb".
- s = "axyb", remove "xy" starting at index 1 so s = "ab".
Now s has no occurrences of "xy".

Note:

1
2
3
yaml复制代码1 <= s.length <= 1000
1 <= part.length <= 1000
s​​​​​​ and part consists of lowercase English letters.

解析

根据题意,给出一个字符串 s 和一个字符串 part ,对 s 执行以下操作,直到删除所有出现的子字符串 part:

  • 找到最左边出现的子字符串 part 并将其从 s 中删除

删除所有出现的 part 后返回 s。同时题目还好心给出了子字符串的含义:子字符串是字符串中连续的字符序列。

其实看完题目之后我们就知道了,这个题很简单,就是考察字符串中的对字符的查找和索引的基本操作,使用一个 while 循环,使用 python 的内置函数 find 如果在 s 中能找到最左边 part 出现的起始索引 i 就让,就让 s[:i] + s[i+len(part):] 替换旧的 s ,直到 while 循环无法从 s 中找到 part 为止,最后返回 s 即可。

解答

1
2
3
4
5
6
7
8
9
10
11
python复制代码class Solution(object):
def removeOccurrences(self, s, part):
"""
:type s: str
:type part: str
:rtype: str
"""
while s.find(part)!=-1:
i = s.find(part)
s = s[:i] + s[i+len(part):]
return s

运行结果

1
2
erlang复制代码Runtime: 16 ms, faster than 94.40% of Python online submissions for Remove All Occurrences of a Substring.
Memory Usage: 13.6 MB, less than 61.60% of Python online submissions for Remove All Occurrences of a Substring.

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

您的支持是我最大的动力

本文转载自: 掘金

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

双指针+归并排序!图解排序链表!

发表于 2021-11-17

前言

大家好,我是捡田螺的小男孩,今天我们来看一道很经典的leetcode真题:排序链表

  • 公众号:捡田螺的小男孩

题目

给你链表的头结点head ,请将其按 升序 排列并返回 排序后的链表 。要求时间复杂度是O(n log n)

实例1:

1
2
ini复制代码输入:head = [4,2,1,3]
输出:[1,2,3,4]

实例2:

1
2
ini复制代码输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

分析

排序算法选定

时间复杂度要求是O(n log n),我们很容易想到快速排序,以及归并排序。

我们先来回顾下快速排序,它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

也就是说,快速排序需要找到基准值,一部分数据比这个基准值小,一部分数据比这个基准值大。因为这个是个链表,发现即使找完基准值,也不好操作。因此,可以考虑使用归并排序算法。

归并排序算法核心步骤

归并排序核心步骤如下:

  • 把长度为n的要排序的序列,分成两个长度为n/2的子序列;
  • 对这两个子序列,分别采用归并排序;
  • 将两个排序好的子序列,合并成一个最终有序的排序序列。

对于链表来说,不同于一般的数据序列,它找到中间节点之后,需要切断一下。因此用归并排序算法,去排链表的操作大概是这样:

  • 遍历链表,找到中间节点。
  • 找到中间节点后,切断
  • 分别再用归并排序,排左右子链表
  • 合并子链表

使用归并排序算法,如何找到中间节点?

我们可以使用双指针法,即一个快指针,一个慢指针。

快指针每次走两步,而慢指针一次只走一步。当快指针走到链表末节点的时候,慢指针是不是走到中间节点啦。

因此,找到链表中间节点的代码,我们可以这么写:

1
2
3
4
5
6
ini复制代码ListNode slow = head;
ListNode fast = head;
while(fast.next!=null && fast.next.next !=null){
fast = fast.next.next;
slow = slow.next;
}

找到中间节点后,切断

找到中间节点后,如何切断呢?

我们可以把slow节点的next指针,赋给新的变量mid,然后再slow的next指针指向空,就可以切断啦。如图:

因此代码可以这么写:

1
2
3
ini复制代码//slow节点的next指针,指向mid
ListNode mid = slow.next;
slow.next = null;

分别再用归并排序,排左右子链表

假设我们本来的排序方法是sortList,那我们找到head和mid子链表后,那需要用同样方法区排序这两个子链表嘛。

1
2
3
ini复制代码//排序左右子链表
ListNode leftList = sortList(head);
ListNode rightList = sortList(mid);

合并子链表

我们要合并两个都有序的子链表,把它变成一个新的有序的链表,其实可以借助一个中间链表,然后分别遍历左、右子链表,比较两个链表当前节点的值,哪个小的话,把它写到中间链表,并且值小的节点所在的链表,继续遍历下一个节点。

图解合并过程

我还是画个示意图吧,这样好理解一点。

假设要排序的左、右链表,以及中间链表如下:

首先取左链表的值1,与右链表的值3比较,因为1<3,所以把值为1的节点放到中间链表,并且左链表移动一个节点,中间链表也右移一个节点,如图:

接着呢,取左链表的值5,与右链表的值3比较,因为5>3,所以把值为3的节点放到中间链表,并且右链表移动一个节点,中间链表也移动一个节点,如图:

接下来呢,取左链表的值5,与右链表的值4比较,因为5>4,所以把值为4的节点放到中间链表,并且右链表移动一个节点,中间链表也移动一个节点,如图:

紧接者,取左链表的值5,与右链表的值6比较,因为5<6,所以把值为5的节点放到中间链表,并且左链表移动一个节点,中间链表也移动一个节点,如图:

最后,因为左链表已经遍历完啦,右链表还没有,因此把右链表放到中间链表即可(官方点来说,就是中间链表的next指针指向右链表),如图:

这时候,链表已经合并完啦,但是还不是我们想要的,因为多了个初始节点0,并且指针指向了5。怎么办呢?我们要的是1-> 3-> 4 ->5 ->6。

我们可以初始化的时候,把链表head赋给一个temp嘛,然后让temp去移动,最后返回head的next就可以啦,如图:

合并代码实现

我们根据图解的过程,来实现下代码吧,如下:

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
ini复制代码public ListNode merge(ListNode left, ListNode right) {
//初始化head链表,合并用
ListNode head = new ListNode(0);
//把head 赋给中间变量temp
ListNode temp = head;
while (left != null && right != null) {
//比较左、右子链表,当前指针指向节点的值大小
if (left.val <= right.val) {
//temp 指向值小的节点(左链表的值小)
temp.next = left;
//左链表右移一个节点
left = left.next;
} else {
//temp 指向值小的节点(右链表的值小)
temp.next = right;
//右链表右移一个节点
right = right.next;
}
//中间链表指针跟着往下移动一个节点
temp = temp.next;
}
//退出循环后,表示左子链表和右子链表至少一个遍历完了
//如果左子链表不为空,那把temp的next指针指向它
if (left != null) {
temp.next = left;
//如果右子链表不为空,那把temp的next指针指向它
} else if (right != null) {
temp.next = right;
}
//最后返回head的next指针,就是合并后的链表啦
return head.next;
}

如果代码还有不理解的地方,可以回头再看看图解过程哈。

完整代码实现

通过遍历链表找到中间节点、中间节点切断链表、分别用归并排序排左右子链表、合并子链表,我们可以整合一下,得出完整代码,如下:

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
ini复制代码class Solution {
public ListNode sortList(ListNode head) {
//如果链表为空,或者只有一个节点,直接返回即可,不用排序
if (head == null || head.next == null)
return head;

//快慢指针移动,以寻找到中间节点
ListNode slow = head;
ListNode fast = head;
while(fast.next!=null && fast.next.next !=null){
fast = fast.next.next;
slow = slow.next;
}
//找到中间节点,slow节点的next指针,指向mid
ListNode mid = slow.next;
//切断链表
slow.next = null;

//排序左子链表
ListNode left = sortList(head);
//排序左子链表
ListNode right = sortList(mid);

//合并链表
return merge(left,right);
}

public ListNode merge(ListNode left, ListNode right) {
ListNode head = new ListNode(0);
ListNode temp = head;
while (left != null && right != null) {
if (left.val <= right.val) {
temp.next = left;
left = left.next;
} else {
temp.next = right;
right = right.next;
}
temp = temp.next;
}
if (left != null) {
temp.next = left;
} else if (right != null) {
temp.next = right;
}
return head.next;
}
}

去leetcode提交一下,找找成就感,通过啦,哈哈。

本文转载自: 掘金

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

Hystrix Dashboard

发表于 2021-11-17

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

1、简介

Hystrix Dashboard是一个通过收集actuator端点提供的Hystrix流数据,并将其图表化的客户端。如果需要通过图表化的界面查看被断路器保护的方法相关调用信息、或者实时监控这些被断路器保护的应用的健康情况,就可以使用Hystrix Dashboard。

2、正文

2.1 启动Hystrix Dashboard

创建一个Spring Boot项目添加Hystrix Dashboard 依赖

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

在启动类上使用@EnableHystrixDashboard注解来启动Hystrix Dashboard

1
2
3
4
5
6
7
8
less复制代码@SpringBootApplication
@EnableHystrixDashboard
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

指定Hystrix Dashboard服务启动端口

1
2
yaml复制代码server:
port: 17777

启动服务,访问/hystrix端点,看到如下界面则证明Hystrix Dashboard启动成功

2.2 启用Actuator端点

Hystrix Dashboard是通过实时获取Actuator端点提供的Hystrix流来实时监控这些被断路器保护的应用的健康情况;因此在这些被断路器保护的应用中需要开启Hystrix流的Actuator端点(注意:如下所有配置都是在已经集成了Hystrix的服务中进行的相关操作)。

引入actuator依赖

1
2
3
4
5
xml复制代码<!--actuator 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

暴露hystrix端点

1
2
3
4
5
6
yaml复制代码# 暴露hystrix端点
management:
endpoints:
web:
exposure:
include: 'hystrix.stream'

在Hystrix Dashboard中输入服务暴露的hystrix流地址http://localhost:18888/actuator/hystrix.stream

点击Monitor Stream之后进入的初始界面如下所示(如果没出现页面,只是显示loading,那就说明断路器方法未被调用,只需向断路器方法发送一次请求即可):

2.3 Hystrix Dashboard面板参数介绍

Hystrix Dashboard面板可分上下两部分来查询,上面部分是断路器方法调用的相关信息,Circuit,下面部分是Hystrix为断路器方法提供的线程池相关信息,Thread Pools。

Circuit:

Circuit这里展示的当前方法的相对信息,如果有多个方法被断路器保护,那么这里将会依此展示多个方法的相关信息。

在图表中,左上角的圆圈代表了该方法的流量和状态:

  • 圆圈越大代表方法流量越大
  • 圆圈为绿色代表断路器健康、黄色代表断路器偶发故障、红色代表断路器故障

右上角的计数器(三列数字):

第一列从上到下

  • 绿色代表当前成功调用的数量
  • 蓝色代表短路请求的数量
  • 蓝绿色代表错误请求的数量

第二列从上到下

  • 黄色代表超时请求的数量
  • 紫色代表线程池拒绝的数量
  • 红色代表失败请求的数量

第三列

  • 过去10s的错误请求百分比

Thread Pools:

Hystrix会针对一个受保护的类创建一个对应的线程池,这样做的目的是Hystrix的命令被调用的时候,不会受方法请求线程的影响(或者说Hystrix的工作线程和调用者线程相互之间不影响)

在图表中,左上角的圆圈代表了该线程池的流量和状态:

  • 圆圈越大代表线程池越活越,流量越大
  • 圆圈颜色代表的是线程池的健康状况

左下角从上至下:

  • Active代表线程池中活跃线程的数量
  • Queued代表排队的线程数量,该功能默认禁止,因此默认情况下始终为0
  • Pool Size代表线程池中线程的数量(上面图我搞错了,困得死MMP)

右下角从上至下:

  • Max Active代表最大活跃线程,这里展示的数据是当前采用周期中,活跃线程的最大值
  • Execcutions代表线程池中线程被调用执行Hystrix命令的次数
  • Queue Size代表线程池队列的大小,默认禁用,无意义

本文转载自: 掘金

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

Python生成博客分享图

发表于 2021-11-17

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

一、前言

我们分享博客的方式有很多种,最常见的无非就是分享链接。或者是编辑一条消息,写上标题链接等东西。但是这种方式都不够直观,相比之下图片的方式要更引人注目。CSDN移动端提供了分享图的功能,但是展示的内容是固定的,所以我就想到用Python自己生成分享图。本文只是技术分享,所以在效果上没有下太多功夫,生成的图片比官方是要丑得多,还需包含。

二、爬取信息

我们要生成博客分析图,就需要先获得一些信息,像是作者的名字,头像,文章的摘要等。这就需要使用到爬虫了,先选取本人的一篇博客:学会这些Python美图技巧,就等着女朋友夸你吧,我们在浏览器打开,右击检查就可以看到下图:

在这里插入图片描述

在左上角的框我们可以看到作者的头像和名字,那就是我们需要的内容。我们先点击右边红框,然后在网页中点击我们需要的内容,比如ZackSock,这样浏览器在源码部分会自动定位到该标签:

在这里插入图片描述

我们可以看到该标签是一个span,而且class设置为name,这个时候我们就可以用BeautifulSoup解析,安装语句如下:

1
bash复制代码pip install BeautifulSoup4

然后进行爬取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码import requests
from bs4 import BeautifulSoup
# 要生成分享图的博客地址
url = 'https://blog.csdn.net/ZackSock/article/details/105833676'
# 浏览器头信息
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
}
# 发送请求,获取网页源码
response = requests.get(self.url, headers=headers)
# 获取BeautifulSoup对象
bs = BeautifulSoup(response.text, 'html.parser')
# 找到源码中class为name的span标签
name = bs.find('span', {'class':'name'})
# 获取标签里面的文字
name = name.text.strip()

这样我们就将博主的名字爬了出来。通过这个方法我们还可以爬取头像,但是摘要就不知道怎么爬了。进行我的不专业分析,发现文章的主体都在一个id为content_views的div中,如果文章格式比较规范的话,第一段非标题文字就在div中第一个非空p标签中。于是我们就可以用下面代码分析出摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码import requests
from bs4 import BeautifulSoup
# 要生成分享图的博客地址
url = 'https://blog.csdn.net/ZackSock/article/details/105833676'
# 浏览器头信息
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
}
# 发送请求,获取网页源码
response = requests.get(self.url, headers=headers)
# 获取BeautifulSoup对象
bs = BeautifulSoup(response.text, 'html.parser')
# 获取正文的html
content = bs.find('div', {'id':'content_views'})
# 获取正文中的p
p_s = content.find_all('p')
# 将正文第一个非空p输出
for p in p_s:
if p.text != '':
print(p.text)

爬取头像的算法也非常简单,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码import requests
from bs4 import BeautifulSoup
# 要生成分享图的博客地址
url = 'https://blog.csdn.net/ZackSock/article/details/105833676'
# 浏览器头信息
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
}
# 发送请求,获取网页源码
response = requests.get(self.url, headers=headers)
# 获取BeautifulSoup对象
bs = BeautifulSoup(response.text, 'html.parser')
# 找到显示头像的img标签
head_img = bs.find('img', {'class': 'avatar_pic'})
with open('head.jpg', 'wb') as f:
# 保存图片
f.write(requests.get(head_img['src']).content)

但是我们爬到的图片是正方形的,我们需要进行一个处理。

三、处理我们需要的内容

首先我们需要生成一个圆形的头像,这就需要用到Pillow模块,安装如下:

1
python复制代码pip install pillow

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码from PIL import ImageDraw
from PIL import Image
# 读取头像图片
im = Image.open('head.jpg').convert('RGBA')
# 创建一个和头像大小一样的图片
bg = Image.new('RGBA', im.size, (230, 230, 230, 255))
# 在创建的图片上抠一个透明圆形
drawer = ImageDraw.Draw(bg)
drawer.ellipse((0, 0, bg.size[0], bg.size[0]), fill=(0, 0, 0, 0), width=3)

r, g, b, a = bg.split()
# 将头像和创建的头像合并,就合成了一个圆形图片
im.paste(bg, (0, 0), mask=a)
# 保存
im.convert('RGB').save('head.jpg')

另外,我们需要用一个二维码让别人可以跳转到我们的博客,这需要用到qrcode模块:

1
python复制代码pip install qrcode

生成二维码的代码如下,我们需要在add_data方法中传入博客地址:

1
2
3
4
5
6
7
8
9
10
python复制代码import qrcode
qr = qrcode.QRCode(
version=5, # 二维码的大小,取值1-40
box_size=10, # 二维码最小正方形的像素数量
error_correction=qrcode.constants.ERROR_CORRECT_H, # 二维码的纠错等级
border=1 # 白色边框的大小
)
qr.add_data('博客地址') # 设置二维码数据
img = qr.make_image() # 创建二维码图片
img.save('qrcode.png')

对qrcode模块有兴趣的读者可以观看:https://blog.csdn.net/ZackSock/article/details/105222763。

四、生成分享图

上面我们把准备工作做完了,可以开始我们的整合了。大家前期可以获取一些自己需要的信息然后按照自己的布局整合,这里我就是按照从上到下依次头像、名称、摘要、二维码的排序:

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
python复制代码import re
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw

bg_im = Image.new('RGB', (350, 600), (230, 230, 230))
# 放置头像
head_im = Image.open('head.jpg')
bg_im.paste(head_im, (140, 70))

# 放置名字
drawer = ImageDraw.Draw(bg_im)
font = ImageFont.truetype('simsun.ttc', 14)
w, h = drawer.textsize(name)
drawer.text(((bg_im.size[0]-w)/2, 160), name, font=font, fill=(15, 15, 15))

# 放置摘要
st = re.findall(r'.{20}', abstract)
line = 0
for i in st:
w, h = drawer.textsize(i.encode('utf-8'))
drawer.text(((bg_im.size[0]-w)/2-20, 220+line*16), i, font=font, fill=(15, 15, 15))
line += 1

# 放置二维码
qrcode = Image.open('qrcode.png')
qrcode = qrcode.resize((100, 100))
bg_im.paste(qrcode, ((bg_im.size[0]-100)//2, 220+line*16+30))

# 保存
bg_im.save('results.jpg')

因为摘要比较长,所以我把摘要分成了数个长度为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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
python复制代码import re
import qrcode
import requests
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
from bs4 import BeautifulSoup


class SharedGenerator():

def __init__(self, url):
self.size = (350, 600)
self.url = url

def get_bs(self):
# 浏览器头信息
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
}
# 发送请求
response = requests.get(self.url, headers=headers)
bs = BeautifulSoup(response.text, 'html.parser')

return bs

def get_img(self, bs):

head_img = bs.find('img', {'class': 'avatar_pic'})

with open('head.jpg', 'wb') as f:
f.write(requests.get(head_img['src']).content)

# 将头像转换成圆框
im = Image.open('head.jpg').convert('RGBA')
bg = Image.new('RGBA', im.size, (230, 230, 230, 255))

drawer = ImageDraw.Draw(bg)
drawer.ellipse((0, 0, bg.size[0], bg.size[0]), fill=(0, 0, 0, 0), width=3)

r, g, b, a = bg.split()

im.paste(bg, (0, 0), mask=a)

im.convert('RGB').save('head.jpg')


def get_name(self, bs):
name = bs.find('span', {'class':'name'})
return name.text.strip()

def get_abstract(self, bs):
# 获取正文的html
content = bs.find('div', {'id':'content_views'})
# 获取正文中的p
p_s = content.find_all('p')
# 将正文第一个非空p输出
for p in p_s:
if p.text != '':
return p.text

def get_qrcode(self):
qr = qrcode.QRCode(
version=5, # 二维码的大小,取值1-40
box_size=10, # 二维码最小正方形的像素数量
error_correction=qrcode.constants.ERROR_CORRECT_H, # 二维码的纠错等级
border=1 # 白色边框的大小
)
qr.add_data(self.url) # 设置二维码数据
img = qr.make_image() # 创建二维码图片
img.save('qrcode.png')


def generate(self, name, abstract):
bg_im = Image.new('RGB', self.size, (230, 230, 230))
# 放置头像
head_im = Image.open('head.jpg')
bg_im.paste(head_im, (140, 70))
# 放置名字
drawer = ImageDraw.Draw(bg_im)
font = ImageFont.truetype('simsun.ttc', 14)
w, h = drawer.textsize(name)
drawer.text(((bg_im.size[0]-w)/2, 160), name, font=font, fill=(15, 15, 15))
# 放置摘要
st = re.findall(r'.{20}', abstract)
line = 0
for i in st:
w, h = drawer.textsize(i.encode('utf-8'))
drawer.text(((bg_im.size[0]-w)/2-20, 220+line*16), i, font=font, fill=(15, 15, 15))
line += 1

qrcode = Image.open('qrcode.png')
qrcode = qrcode.resize((100, 100))

bg_im.paste(qrcode, ((bg_im.size[0]-100)//2, 220+line*16+30))

bg_im.save('results.jpg')

if __name__ == '__main__':
url = 'https://blog.csdn.net/ZackSock/article/details/105833676'
# 创建生成器对象
generator = SharedGenerator(url)
# 获取BeautifulSoup对象
bs = generator.get_bs()
# 下载并处理头像
generator.get_img(bs)
# 获取名字
name = generator.get_name(bs)
# 获取摘要
abstract = generator.get_abstract(bs)
# 生成二维码
generator.get_qrcode()
# 生成分享图
generator.generate(name, abstract)

上面就完整的实现了分享图的实现,下面是效果图:

在这里插入图片描述

我把原本的二维码替换成了图中的美女。我没有什么艺术细胞,大家可以发挥自己的想象定制一个更美观的分享图。

本文转载自: 掘金

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

剑指 Offer II 014 字符串中的变位词

发表于 2021-11-17

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

给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。

换句话说,第一个字符串的排列之一是第二个字符串的 子串 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码示例 1:

输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例 2:

输入: s1= "ab" s2 = "eidboaoo"
输出: False
 

提示:

1 <= s1.length, s2.length <= 104
s1 和 s2 仅包含小写字母

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/MP…

思路

  • 首先异位词的题目,一般都可以空间换时间去解决
    • 所以我们可以使用哈希表去解决
    • 但是题目中说到是小写字母,所以我们用固定大小的26位数组去标记计数
  • 我们用ab和cba来举例
    • 当出现ab和abc这样的样例我们是最好解决的
    • 只要对应位置在数组中加一后再减一,就可以得出ab是abc的子序列
    • 所以我们代码中第一次的判断就是做一个这样的情况
    • 面对ab和cba的情形,我们使用双指针去解决
    • 最开始是下面的情形

image.png

  • 然后对应字符串2的情况是下图所示

image.png

  • 将s1的字符串为滑动窗口的长度,每次进行扫描

image.png

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复制代码class Solution {
public boolean checkInclusion(String s1, String s2) {
boolean flag = true;
if (s2.length() < s1.length()) {
return false;
}
int[] counts = new int[26];
for (int i = 0; i < s1.length(); i++) {
counts[s1.charAt(i) - 'a']++;
counts[s2.charAt(i) - 'a']--;
}
if (check(counts)) return true;

for (int i = s1.length(); i < s2.length(); i++) {
counts[s2.charAt(i) - 'a']--;
counts[s2.charAt(i - s1.length()) - 'a']++;
if (check(counts)) return true;
}

return false;
}
boolean check(int[] arr) {
for (int i : arr) {
if (i != 0) return false;
}
return true;
}
}

下一篇我们会去介绍,这道题的变种题,这篇主要使用双指针+标记法📌

本文转载自: 掘金

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

PHP-redis操作类

发表于 2021-11-17

关于PHP操作redis的一些命令之类的,我将其封装成了一个类,其中包含redis五种数据类型的操作,基本功能大概都是有了。下边是类的代码。

文末有资源,可下载。

Redis.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
php复制代码<?php

/**
* redis操作类
* 说明,任何为false的串,存在redis中都是空串。
* 只有在key不存在时,才会返回false。
* 这点可用于防止缓存穿透
*
*/
class Redis
{
private $redis;

//当前数据库ID号
protected $dbId=0;

//当前权限认证码
protected $auth;

/*
* 实例化的对象,单例模式.
* @var iphpdbRedis
*/
static private $_instance=array();

private $k;

//连接属性数组
protected $attr=array(
//连接超时时间,redis配置文件中默认为300秒
'timeout'=>30,
//选择的数据库。
'db_id'=>0,
);

//什么时候重新建立连接
protected $expireTime;

protected $host;

protected $port;


private function __construct($config,$attr=array())
{
$this->attr = array_merge($this->attr,$attr);
$this->redis = new Redis();
$this->port = $config['port'] ? $config['port'] : 6379;
$this->host = $config['host'];
$this->redis->connect($this->host, $this->port, $this->attr['timeout']);

if($config['auth'])
{
$this->auth($config['auth']);
$this->auth = $config['auth'];
}

$this->expireTime = time() + $this->attr['timeout'];
}

/*
* 得到实例化的对象.
* 为每个数据库建立一个连接
* 如果连接超时,将会重新建立一个连接
* @param array $config
* @param int $dbId
* @return iphpdbRedis
*/
public static function getInstance($config, $attr = array())
{
//如果是一个字符串,将其认为是数据库的ID号。以简化写法。
if(!is_array($attr))
{
$dbId = $attr;
$attr = array();
$attr['db_id'] = $dbId;
}

$attr['db_id'] = $attr['db_id'] ? $attr['db_id'] : 0;


$k = md5(implode('', $config).$attr['db_id']);
if(! (static::$_instance[$k] instanceof self))
{

static::$_instance[$k] = new self($config,$attr);
static::$_instance[$k]->k = $k;
static::$_instance[$k]->dbId = $attr['db_id'];

//如果不是0号库,选择一下数据库。
if($attr['db_id'] != 0){
static::$_instance[$k]->select($attr['db_id']);
}
}
elseif( time() > static::$_instance[$k]->expireTime)
{
static::$_instance[$k]->close();
static::$_instance[$k] = new self($config,$attr);
static::$_instance[$k]->k = $k;
static::$_instance[$k]->dbId= $attr['db_id'];

//如果不是0号库,选择一下数据库。
if($attr['db_id']!=0){
static::$_instance[$k]->select($attr['db_id']);
}
}
return static::$_instance[$k];
}

private function __clone(){}

/**
* 执行原生的redis操作
* @return Redis
*/
public function getRedis()
{
return $this->redis;
}

/*****************hash表操作函数*******************/

/**
* 得到hash表中一个字段的值
* @param string $key 缓存key
* @param string $field 字段
* @return string|false
*/
public function hGet($key,$field)
{
return $this->redis->hGet($key,$field);
}

/**
* 为hash表设定一个字段的值
* @param string $key 缓存key
* @param string $field 字段
* @param string $value 值。
* @return bool
*/
public function hSet($key,$field,$value)
{
return $this->redis->hSet($key,$field,$value);
}

/**
* 判断hash表中,指定field是不是存在
* @param string $key 缓存key
* @param string $field 字段
* @return bool
*/
public function hExists($key,$field)
{
return $this->redis->hExists($key,$field);
}

/**
* 删除hash表中指定字段 ,支持批量删除
* @param string $key 缓存key
* @param string $field 字段
* @return int
*/
public function hdel($key,$field)
{
$fieldArr=explode(',',$field);
$delNum=0;

foreach($fieldArr as $row)
{
$row=trim($row);
$delNum+=$this->redis->hDel($key,$row);
}

return $delNum;
}

/**
* 返回hash表元素个数
* @param string $key 缓存key
* @return int|bool
*/
public function hLen($key)
{
return $this->redis->hLen($key);
}

/**
* 为hash表设定一个字段的值,如果字段存在,返回false
* @param string $key 缓存key
* @param string $field 字段
* @param string $value 值。
* @return bool
*/
public function hSetNx($key,$field,$value)
{
return $this->redis->hSetNx($key,$field,$value);
}

/**
* 为hash表多个字段设定值。
* @param string $key
* @param array $value
* @return array|bool
*/
public function hMset($key,$value)
{
if(!is_array($value))
return false;
return $this->redis->hMset($key,$value);
}

/**
* 为hash表多个字段设定值。
* @param string $key
* @param array|string $value string以','号分隔字段
* @return array|bool
*/
public function hMget($key,$field)
{
if(!is_array($field))
$field=explode(',', $field);
return $this->redis->hMget($key,$field);
}

/**
* 为hash表设这累加,可以负数
* @param string $key
* @param int $field
* @param string $value
* @return bool
*/
public function hIncrBy($key,$field,$value)
{
$value=intval($value);
return $this->redis->hIncrBy($key,$field,$value);
}

/**
* 返回所有hash表的所有字段
* @param string $key
* @return array|bool
*/
public function hKeys($key)
{
return $this->redis->hKeys($key);
}

/**
* 返回所有hash表的字段值,为一个索引数组
* @param string $key
* @return array|bool
*/
public function hVals($key)
{
return $this->redis->hVals($key);
}

/**
* 返回所有hash表的字段值,为一个关联数组
* @param string $key
* @return array|bool
*/
public function hGetAll($key)
{
return $this->redis->hGetAll($key);
}

/*********************有序集合操作*********************/

/**
* 给当前集合添加一个元素
* 如果value已经存在,会更新order的值。
* @param string $key
* @param string $order 序号
* @param string $value 值
* @return bool
*/
public function zAdd($key,$order,$value)
{
return $this->redis->zAdd($key,$order,$value);
}

/**
* 给$value成员的order值,增加$num,可以为负数
* @param string $key
* @param string $num 序号
* @param string $value 值
* @return 返回新的order
*/
public function zinCry($key,$num,$value)
{
return $this->redis->zinCry($key,$num,$value);
}

/**
* 删除值为value的元素
* @param string $key
* @param stirng $value
* @return bool
*/
public function zRem($key,$value)
{
return $this->redis->zRem($key,$value);
}

/**
* 集合以order递增排列后,0表示第一个元素,-1表示最后一个元素
* @param string $key
* @param int $start
* @param int $end
* @return array|bool
*/
public function zRange($key,$start,$end)
{
return $this->redis->zRange($key,$start,$end);
}

/**
* 集合以order递减排列后,0表示第一个元素,-1表示最后一个元素
* @param string $key
* @param int $start
* @param int $end
* @return array|bool
*/
public function zRevRange($key,$start,$end)
{
return $this->redis->zRevRange($key,$start,$end);
}

/**
* 集合以order递增排列后,返回指定order之间的元素。
* min和max可以是-inf和+inf 表示最大值,最小值
* @param string $key
* @param int $start
* @param int $end
* @package array $option 参数
* withscores=>true,表示数组下标为Order值,默认返回索引数组
* limit=>array(0,1) 表示从0开始,取一条记录。
* @return array|bool
*/
public function zRangeByScore($key,$start='-inf',$end="+inf",$option=array())
{
return $this->redis->zRangeByScore($key,$start,$end,$option);
}

/**
* 集合以order递减排列后,返回指定order之间的元素。
* min和max可以是-inf和+inf 表示最大值,最小值
* @param string $key
* @param int $start
* @param int $end
* @package array $option 参数
* withscores=>true,表示数组下标为Order值,默认返回索引数组
* limit=>array(0,1) 表示从0开始,取一条记录。
* @return array|bool
*/
public function zRevRangeByScore($key,$start='-inf',$end="+inf",$option=array())
{
return $this->redis->zRevRangeByScore($key,$start,$end,$option);
}

/**
* 返回order值在start end之间的数量
* @param unknown $key
* @param unknown $start
* @param unknown $end
*/
public function zCount($key,$start,$end)
{
return $this->redis->zCount($key,$start,$end);
}

/**
* 返回值为value的order值
* @param unknown $key
* @param unknown $value
*/
public function zScore($key,$value)
{
return $this->redis->zScore($key,$value);
}

/**
* 返回集合以score递增加排序后,指定成员的排序号,从0开始。
* @param unknown $key
* @param unknown $value
*/
public function zRank($key,$value)
{
return $this->redis->zRank($key,$value);
}

/**
* 返回集合以score递增加排序后,指定成员的排序号,从0开始。
* @param unknown $key
* @param unknown $value
*/
public function zRevRank($key,$value)
{
return $this->redis->zRevRank($key,$value);
}

/**
* 删除集合中,score值在start end之间的元素 包括start end
* min和max可以是-inf和+inf 表示最大值,最小值
* @param unknown $key
* @param unknown $start
* @param unknown $end
* @return 删除成员的数量。
*/
public function zRemRangeByScore($key,$start,$end)
{
return $this->redis->zRemRangeByScore($key,$start,$end);
}

/**
* 返回集合元素个数。
* @param unknown $key
*/
public function zCard($key)
{
return $this->redis->zCard($key);
}
/*********************队列操作命令************************/

/**
* 在队列尾部插入一个元素
* @param unknown $key
* @param unknown $value
* 返回队列长度
*/
public function rPush($key,$value)
{
return $this->redis->rPush($key,$value);
}

/**
* 在队列尾部插入一个元素 如果key不存在,什么也不做
* @param unknown $key
* @param unknown $value
* 返回队列长度
*/
public function rPushx($key,$value)
{
return $this->redis->rPushx($key,$value);
}

/**
* 在队列头部插入一个元素
* @param unknown $key
* @param unknown $value
* 返回队列长度
*/
public function lPush($key,$value)
{
return $this->redis->lPush($key,$value);
}

/**
* 在队列头插入一个元素 如果key不存在,什么也不做
* @param unknown $key
* @param unknown $value
* 返回队列长度
*/
public function lPushx($key,$value)
{
return $this->redis->lPushx($key,$value);
}

/**
* 返回队列长度
* @param unknown $key
*/
public function lLen($key)
{
return $this->redis->lLen($key);
}

/**
* 返回队列指定区间的元素
* @param unknown $key
* @param unknown $start
* @param unknown $end
*/
public function lRange($key,$start,$end)
{
return $this->redis->lrange($key,$start,$end);
}

/**
* 返回队列中指定索引的元素
* @param unknown $key
* @param unknown $index
*/
public function lIndex($key,$index)
{
return $this->redis->lIndex($key,$index);
}

/**
* 设定队列中指定index的值。
* @param unknown $key
* @param unknown $index
* @param unknown $value
*/
public function lSet($key,$index,$value)
{
return $this->redis->lSet($key,$index,$value);
}

/**
* 删除值为vaule的count个元素
* PHP-REDIS扩展的数据顺序与命令的顺序不太一样,不知道是不是bug
* count>0 从尾部开始
* >0 从头部开始
* =0 删除全部
* @param unknown $key
* @param unknown $count
* @param unknown $value
*/
public function lRem($key,$count,$value)
{
return $this->redis->lRem($key,$value,$count);
}

/**
* 删除并返回队列中的头元素。
* @param unknown $key
*/
public function lPop($key)
{
return $this->redis->lPop($key);
}

/**
* 删除并返回队列中的尾元素
* @param unknown $key
*/
public function rPop($key)
{
return $this->redis->rPop($key);
}

/*************redis字符串操作命令*****************/

/**
* 设置一个key
* @param unknown $key
* @param unknown $value
*/
public function set($key,$value)
{
return $this->redis->set($key,$value);
}

/**
* 得到一个key
* @param unknown $key
*/
public function get($key)
{
return $this->redis->get($key);
}

/**
* 设置一个有过期时间的key
* @param unknown $key
* @param unknown $expire
* @param unknown $value
*/
public function setex($key,$expire,$value)
{
return $this->redis->setex($key,$expire,$value);
}


/**
* 设置一个key,如果key存在,不做任何操作.
* @param unknown $key
* @param unknown $value
*/
public function setnx($key,$value)
{
return $this->redis->setnx($key,$value);
}

/**
* 批量设置key
* @param unknown $arr
*/
public function mset($arr)
{
return $this->redis->mset($arr);
}

/*************redis 无序集合操作命令*****************/

/**
* 返回集合中所有元素
* @param unknown $key
*/
public function sMembers($key)
{
return $this->redis->sMembers($key);
}

/**
* 求2个集合的差集
* @param unknown $key1
* @param unknown $key2
*/
public function sDiff($key1,$key2)
{
return $this->redis->sDiff($key1,$key2);
}

/**
* 添加集合。由于版本问题,扩展不支持批量添加。这里做了封装
* @param unknown $key
* @param string|array $value
*/
public function sAdd($key,$value)
{
if(!is_array($value))
$arr=array($value);
else
$arr=$value;
foreach($arr as $row)
$this->redis->sAdd($key,$row);
}

/**
* 返回无序集合的元素个数
* @param unknown $key
*/
public function scard($key)
{
return $this->redis->scard($key);
}

/**
* 从集合中删除一个元素
* @param unknown $key
* @param unknown $value
*/
public function srem($key,$value)
{
return $this->redis->srem($key,$value);
}

/*************redis管理操作命令*****************/

/**
* 选择数据库
* @param int $dbId 数据库ID号
* @return bool
*/
public function select($dbId)
{
$this->dbId=$dbId;
return $this->redis->select($dbId);
}

/**
* 清空当前数据库
* @return bool
*/
public function flushDB()
{
return $this->redis->flushDB();
}

/**
* 返回当前库状态
* @return array
*/
public function info()
{
return $this->redis->info();
}

/**
* 同步保存数据到磁盘
*/
public function save()
{
return $this->redis->save();
}

/**
* 异步保存数据到磁盘
*/
public function bgSave()
{
return $this->redis->bgSave();
}

/**
* 返回最后保存到磁盘的时间
*/
public function lastSave()
{
return $this->redis->lastSave();
}

/**
* 返回key,支持*多个字符,?一个字符
* 只有* 表示全部
* @param string $key
* @return array
*/
public function keys($key)
{
return $this->redis->keys($key);
}

/**
* 删除指定key
* @param unknown $key
*/
public function del($key)
{
return $this->redis->del($key);
}

/**
* 判断一个key值是不是存在
* @param unknown $key
*/
public function exists($key)
{
return $this->redis->exists($key);
}

/**
* 为一个key设定过期时间 单位为秒
* @param unknown $key
* @param unknown $expire
*/
public function expire($key,$expire)
{
return $this->redis->expire($key,$expire);
}

/**
* 返回一个key还有多久过期,单位秒
* @param unknown $key
*/
public function ttl($key)
{
return $this->redis->ttl($key);
}

/**
* 设定一个key什么时候过期,time为一个时间戳
* @param unknown $key
* @param unknown $time
*/
public function exprieAt($key,$time)
{
return $this->redis->expireAt($key,$time);
}

/**
* 关闭服务器链接
*/
public function close()
{
return $this->redis->close();
}

/**
* 关闭所有连接
*/
public static function closeAll()
{
foreach(static::$_instance as $o)
{
if($o instanceof self)
$o->close();
}
}

/** 这里不关闭连接,因为session写入会在所有对象销毁之后。
*public function __destruct()
*{
*return $this->redis->close();
*}
**/
/**
* 返回当前数据库key数量
*/
public function dbSize()
{
return $this->redis->dbSize();
}

/**
* 返回一个随机key
*/
public function randomKey()
{
return $this->redis->randomKey();
}

/**
* 得到当前数据库ID
* @return int
*/
public function getDbId()
{
return $this->dbId;
}

/**
* 返回当前密码
*/
public function getAuth()
{
return $this->auth;
}

public function getHost()
{
return $this->host;
}

public function getPort()
{
return $this->port;
}

public function getConnInfo()
{
return array(
'host'=>$this->host,
'port'=>$this->port,
'auth'=>$this->auth
);
}
/*********************事务的相关方法************************/

/**
* 监控key,就是一个或多个key添加一个乐观锁
* 在此期间如果key的值如果发生的改变,刚不能为key设定值
* 可以重新取得Key的值。
* @param unknown $key
*/
public function watch($key)
{
return $this->redis->watch($key);
}

/**
* 取消当前链接对所有key的watch
* EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了
*/
public function unwatch()
{
return $this->redis->unwatch();
}

/**
* 开启一个事务
* 事务的调用有两种模式Redis::MULTI和Redis::PIPELINE,
* 默认是Redis::MULTI模式,
* Redis::PIPELINE管道模式速度更快,但没有任何保证原子性有可能造成数据的丢失
*/
public function multi($type=Redis::MULTI)
{
return $this->redis->multi($type);
}

/**
* 执行一个事务
* 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
*/
public function exec()
{
return $this->redis->exec();
}

/**
* 回滚一个事务
*/
public function discard()
{
return $this->redis->discard();
}

/**
* 测试当前链接是不是已经失效
* 没有失效返回+PONG
* 失效返回false
*/
public function ping()
{
return $this->redis->ping();
}

public function auth($auth)
{
return $this->redis->auth($auth);
}
/*********************自定义的方法,用于简化操作************************/

/**
* 得到一组的ID号
* @param unknown $prefix
* @param unknown $ids
*/
public function hashAll($prefix,$ids)
{
if($ids==false)
return false;
if(is_string($ids))
$ids=explode(',', $ids);
$arr=array();
foreach($ids as $id)
{
$key=$prefix.'.'.$id;
$res=$this->hGetAll($key);
if($res!=false)
$arr[]=$res;
}

return $arr;
}

/**
* 生成一条消息,放在redis数据库中。使用0号库。
* @param string|array $msg
*/
public function pushMessage($lkey,$msg)
{
if(is_array($msg)){
$msg = json_encode($msg);
}
$key = md5($msg);

//如果消息已经存在,删除旧消息,已当前消息为准
//echo $n=$this->lRem($lkey, 0, $key)."
";
//重新设置新消息
$this->lPush($lkey, $key);
$this->setex($key, 3600, $msg);
return $key;
}


/**
* 得到条批量删除key的命令
* @param unknown $keys
* @param unknown $dbId
*/
public function delKeys($keys,$dbId)
{
$redisInfo=$this->getConnInfo();
$cmdArr=array(
'redis-cli',
'-a',
$redisInfo['auth'],
'-h',
$redisInfo['host'],
'-p',
$redisInfo['port'],
'-n',
$dbId,
);
$redisStr=implode(' ', $cmdArr);
$cmd="{$redisStr} KEYS "{$keys}" | xargs {$redisStr} del";
return $cmd;
}
}

有好的建议,请在下方输入你的评论。

欢迎访问个人博客
guanchao.site

欢迎访问小程序:

在这里插入图片描述

本文转载自: 掘金

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

每次新增页面复制粘贴?100多行源码的 element-ui

发表于 2021-11-17
  1. 前言

大家好,我是若川。最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,或者关注我的公众号若川视野,回复“源码”参与。已进行三个月,大家一起交流学习,共同进步,很多人都表示收获颇丰。

想学源码,极力推荐之前我写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite等10余篇源码文章。

本文仓库 element-analysis,求个star^_^

最近组织了源码共读活动,大家一起学习源码,每周学习200行左右的源码,已进行到13期。于是搜寻各种值得我们学习,且代码行数不多的源码。

其中 element-ui 新建组件的源码 仅 100多行,非常值得我们学习。

可以通过 github1s.com 在线 VSCode 打开:github1s.com/ElemeFE/ele…

阅读本文,你将学到:

1
2
3
4
js复制代码1. 学会调试学习源码
2. element-ui 如何初始化新的组件
3. 可以学以致用应用到自己开发的项目中,比如新增页面等
4. 等等
  1. 环境准备

2.1 克隆

1
2
3
4
5
6
7
8
9
bash复制代码# 推荐克隆我的项目,保证与文章同步
git clone https://github.com/lxchuan12/element-analysis.git
# npm i -g yarn
cd element-analysis/element && npm run dev

# 或者克隆官方项目
git clone https://github.com/ElemeFE/element.git
# npm i -g yarn
cd element && npm run dev

2.2 看开源项目的 README 和贡献文档等

看开源项目,我们一般先看README,README.md 中一般有贡献指南。

开发环境搭建

首先你需要 Node.js 4+,yarn 和 npm 3+。注意:我们使用 yarn 进行依赖版本的锁定,所以请不要使用 npm install 安装依赖。

1
2
3
4
sh复制代码git clone git@github.com:ElemeFE/element.git
npm run dev

# open http://localhost:8085

package.json

1
2
3
4
5
6
7
js复制代码{
"script": {
"bootstrap": "yarn || npm i",
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
"dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
},
}

在 npm run dev 时是先执行了 npm run bootstrap => yarn || npm i 命令,安装好了依赖。

npm run build:file 应该留着下一篇文章讲述。

组件开发规范

通过 make new 创建组件目录结构,包含测试代码、入口文件、文档
如果包含父子组件,需要更改目录结构,参考 Button
组件内如果依赖了其他组件,需要在当前组件内引入,参考 Select

make 命令的配置对应根目录 Makefile。

1
2
3
sh复制代码# element/Makefile
new:
node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS))

通过查看 Makefile 文件我们知道了make new命令对应的是: node build/bin.new.js。

接着我们来调试源码。

2.3 调试源码

在最新版的 VSCode 中,auto attach 功能,默认支持智能调试,如果发现不支持,可以通过快捷键 ctrl + shift + p 查看是否启用。

ctrl + 快捷键打开终端。输入如下命令,即可调试build/bin/new.js`。

1
2
3
4
sh复制代码make new ruochuan 若川组件
# Ubuntu 和 Mac 支持 make 命令
# 不支持可以用 node
node build/bin/new.js ruochuan 若川组件

调试截图

更多调试细节可以看我的这篇文章:新手向:前端程序员必学基本技能——调试JS代码

接着我们按调试来看主流程。

  1. 主流程

我看完 build/bin/new.js 源码画了一张流程图。毕竟俗话说得好,一图胜千言。

流程图

同时执行完命令后也新增和修改了若干文件,git diff 如下图所示。

所有修改的文件

接着我们来看 build/bin/new.js 文件。

3.1 文件开头判断

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码'use strict';

console.log();
process.on('exit', () => {
console.log();
});

// 第一个参数没传递报错,退出进程
if (!process.argv[2]) {
console.error('[组件名]必填 - Please enter new component name');
process.exit(1);
}

关于 process 对象可以查看 阮一峰老师 process 对象

process.argv 属性返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是 node,第二个成员是脚本文件名,其余成员是脚本文件的参数。

接着我们来看,引用的依赖等。

3.2 引用依赖等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码// 路径模块
const path = require('path');
// 文件模块
const fs = require('fs');
// 保存文件
const fileSave = require('file-save');
// 转驼峰
const uppercamelcase = require('uppercamelcase');
// 第一个参数 组件名
const componentname = process.argv[2];
// 第二个参数 组件中文名
const chineseName = process.argv[3] || componentname;
// 转驼峰
const ComponentName = uppercamelcase(componentname);
// package 路径
const PackagePath = path.resolve(__dirname, '../../packages', componentname);
// const Files = [];

其中 file-save 依赖,顾名思义,且非常关键。我们可以在 node_module/file-save 查看一些信息。
也可以在 https://npmjs.com 搜索其信息。

接着,我们来看文件模板。定义了若干文件模板,方便写入到项目中。

3.3 文件模板 Files

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
js复制代码const Files = [
{
filename: 'index.js',
content: `import ${ComponentName} from './src/main';

/* istanbul ignore next */
${ComponentName}.install = function(Vue) {
Vue.component(${ComponentName}.name, ${ComponentName});
};

export default ${ComponentName};`
},
{
filename: 'src/main.vue',
content: `<template>
<div class="el-${componentname}"></div>
</template>

<script>
export default {
name: 'El${ComponentName}'
};
</script>`
},
// 省略其他
];

接着我们继续看添加对应的路径到组件 json 配置中。

3.4 把 componentname 添加到 components.json

1
2
3
4
5
6
7
8
9
10
js复制代码// 添加到 components.json
const componentsFile = require('../../components.json');
if (componentsFile[componentname]) {
console.error(`${componentname} 已存在.`);
process.exit(1);
}
componentsFile[componentname] = `./packages/${componentname}/index.js`;
fileSave(path.join(__dirname, '../../components.json'))
.write(JSON.stringify(componentsFile, null, ' '), 'utf8')
.end('\n');

添加到 components.json

3.5 把 componentname.scss 添加到 index.scss

1
2
3
4
5
6
js复制代码// 添加到 index.scss
const sassPath = path.join(__dirname, '../../packages/theme-chalk/src/index.scss');
const sassImportText = `${fs.readFileSync(sassPath)}@import "./${componentname}.scss";`;
fileSave(sassPath)
.write(sassImportText, 'utf8')
.end('\n');

3.6 把 componentname.d.ts 添加到 element-ui.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码// 添加到 element-ui.d.ts
const elementTsPath = path.join(__dirname, '../../types/element-ui.d.ts');

let elementTsText = `${fs.readFileSync(elementTsPath)}
/** ${ComponentName} Component */
export class ${ComponentName} extends El${ComponentName} {}`;

const index = elementTsText.indexOf('export') - 1;
const importString = `import { El${ComponentName} } from './${componentname}'`;

elementTsText = elementTsText.slice(0, index) + importString + '\n' + elementTsText.slice(index);

fileSave(elementTsPath)
.write(elementTsText, 'utf8')
.end('\n');

3.7 创建 package

1
2
3
4
5
6
7
js复制代码// const PackagePath = path.resolve(__dirname, '../../packages', componentname);
// 创建 package
Files.forEach(file => {
fileSave(path.join(PackagePath, file.filename))
.write(file.content, 'utf8')
.end('\n');
});

创建的文件

3.8 把新增的组件添加到 nav.config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码const navConfigFile = require('../../examples/nav.config.json');

Object.keys(navConfigFile).forEach(lang => {
let groups = navConfigFile[lang][4].groups;
groups[groups.length - 1].list.push({
path: `/${componentname}`,
title: lang === 'zh-CN' && componentname !== chineseName
? `${ComponentName} ${chineseName}`
: ComponentName
});
});

fileSave(path.join(__dirname, '../../examples/nav.config.json'))
.write(JSON.stringify(navConfigFile, null, ' '), 'utf8')
.end('\n');

console.log('DONE!');

nav

nav.config.json 的修改,新增的组件显示在导航这里。其中有四次修改是对应四种语言。

nav 导航网站显示

  1. 总结

再次放出开头的流程图。

流程图

通过看 element-ui 新建组件的源码 流程,我们学到了 file-save 这么方便的写入文件的库等。

同时给我们启发:公司项目新建页面时,或者组件库新增组件时,是不是可以类似做到的,一条命令省去一些繁杂重复的操作。

建议读者克隆我的仓库动手实践调试源码学习。

后续也可以查看 file-save 源码实现等。

最后可以持续关注我@若川。欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,大家一起学习源码,共同进步。


关于 && 交流群

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

SpringCloud实战三十六,如何优雅的使用Feign?

发表于 2021-11-17

在SpringCloud架构体系中,微服务间的通信是基于Feign调用。而在实际使用Feign的过程中我们大概率会面临下面几个问题:

  • Feign客户端放在消费端还是独立一个api层?
  • Feign调用的接口如何要不要进行包装?
  • Feign如何抓取业务生产端的业务异常?

这篇文章我们就来一起探讨一下这几个问题,希望看完能对你有所帮助。

首先我们先看看Feign的调用方式如何抉择?

Feign的调用方式如何选择?

总体来说,Feign的调用方式分为两大类:

在生产端API中声明Feign客户端


如上,消费端服务直接依赖生产端提供的API包,然后通过@Autowired注解注入,就可以直接调用生产者提供的接口。

这样做的 好处 是:简单方便,消费端直接使用生产者提供的Feign接口即可。

这样做的 坏处 也很明显:消费端获取到的接口是生产者提供给所有服务的接口列表,当某一生产者接口很多时就会很混乱;而且熔断降级类也在生产端,消费端在调用时由于包路径可能与生产者不一样,必须要通过@SpringBootApplication(scanBasePackages = {"com.javadaily.feign"})扫描Feign的路径,当消费端需要引入很多生产者Feign时那就需要扫描很多个接口路径。

此调用方式在之前两篇文章中有详细说明,感兴趣的可以通过下方链接直达:

SpringCloud Alibaba微服务实战三 - 服务调用

SpringCloud Alibaba微服务实战二十 - 集成Feign的降级熔断

在消费端声明Feign客户端


还是需要独立一个公共的API接口层,生产端消费端都需要引入此jar包,同时在消费端按需编写Feign客户端及熔断类。

这样做的 好处 是:客户端可以按需编写自己需要的接口,熔断降级都由消费者控制;不需要在启动类上加入额外的扫描注解scanBasePackages。

这样做的 坏处 是:消费端代码冗余,每个消费者都需要编写Feign客户端;服务间耦合比较紧,修改一处接口三处都要修改。

小结

那么问题来了:既然有两种调用方式,那那种才更合理呢?

我这里建议的是优先使用第二种方式,由客户端自己定制Feign客户端。

从职责来说只有消费端才能明确知道自己要调用哪个服务提供方,需要调用哪些接口。如果直接把@FeignClient写在服务提供方的API上,消费端就很难按需定制,而熔断处理逻辑也应该是由消费端自己定制熔断逻辑。虽然会导致代码冗余,但是职责很清晰,而且可以避免扫描不到接口路径的问题。

当然这里只是个人建议,如果你觉得我说的不对,你可以按照你自己的想法来。

接下来我们再来看看Feign接口要不要封装的问题。

Feign接口要不要包装?

现状分析

在前后端分离项目中,后端给前端返回接口数据时一般会统一返回格式,此时我们的Controller大概会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RestController
@Log4j2
@Api(tags = "account接口")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AccountController implements AccountApi {

private final AccountService accountService;

...
public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){
AccountDTO accountDTO = accountService.selectByCode(accountCode);
return ResultData.success(accountDTO);
}
...
}

而Feign的接口定义需要跟实现类保持一致,那么现在order-service获取订单详情时也要返回用户信息,此时我们通过feign调用account-service的getByCode()接口会这样写:

1
2
3
4
5
6
7
8
9
10
java复制代码/**
* 获取Order详情
* @param orderNo order编号
* @return ResultData<OrderDTO>
*/
@GetMapping("/order/{orderNo}")
public ResultData<OrderDTO> getById(@PathVariable("orderNo") String orderNo){
OrderDTO orderDTO = orderService.selectByNo(orderNo);
return ResultData.success(orderDTO);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public OrderDTO selectByNo(String orderNo) {
OrderDTO orderDTO = new OrderDTO();

//1. 查询订单基础信息
Order order = orderMapper.selectByNo(orderNo);
BeanUtils.copyProperties(order,orderDTO);

//2. 获取用户信息
ResultData<AccountDTO> accountResult = accountClient.getByCode("javadaily");
if(accountResult.isSuccess()){
orderDTO.setAccountDTO(accountResult.getData());
}

return orderDTO;
}

这里要先获取到ResultData包装类,再通过判断返回结果解成具体的AccountDTO对象,很明显这段代码有两个问题:

  1. 每个Controller接口都需要手动使用ResultData.success对结果进行包装,Repeat Yourself !
  2. Feign调用时又需要从包装类解装成需要的实体对象,Repeat Yourself !

如果有很多这样的接口调用,那…

image-20210716084136689

优化包装

这样丑陋的代码我们当然需要进行优化,优化的目标也很明确:当我们通过Feign调用时,直接获取到实体对象,不需要额外的解装。而前端通过网关直接调用时,返回统一的包装体。

这里我们可以借助ResponseBodyAdvice来实现,通过对Controller返回体进行增强,如果识别到是Feign的调用就直接返回对象,否则给我们加上统一包装结构。

至于为什么前后端需要统一返回格式以及如何实现,在我的老鸟系列文章 SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的! 有详细讲述,感兴趣的可以移步。

现在问题是:如何识别出是Feign的调用还是网关直接调用呢?

这又有两种方法,基于自定义注解实现和基于Feign拦截器实现。

基于自定义注解实现

自定义一个注解,比如Inner,给Feign的接口标注上此注解,这样在使用ResponseBodyAdvice匹配时可以通过此注解进行匹配。

不过这种方法有个弊端,就是前端和feign没法公用,如一个接口user/get/{id}既可以通过feign调用也可以通过网关直接调用,采用这种方法就需要写2个不同路径的接口。

基于Feign拦截器实现

对于Feign的调用,在Feign拦截器上加上特殊标识,在转换对象时如果发现是feign调用就直接返回对象。

具体实现过程

这里我们使用第二种方法来实现(第一种方法也很简单,大家可自行尝试)

  1. 在feign拦截器中给feign请求添加特定请求头T_REQUEST_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
java复制代码/**
* 给Feign设置请求头
*/
@Bean
public RequestInterceptor requestInterceptor(){
return requestTemplate -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(null != attributes){
HttpServletRequest request = attributes.getRequest();
Map<String, String> headers = getRequestHeaders(request);

// 传递所有请求头,防止部分丢失
//此处也可以只传递认证的header
//requestTemplate.header("Authorization", request.getHeader("Authorization"));
for (Map.Entry<String, String> entry : headers.entrySet()) {
requestTemplate.header(entry.getKey(), entry.getValue());
}

// 微服务之间传递的唯一标识,区分大小写所以通过httpServletRequest获取
if (request.getHeader(T_REQUEST_ID)==null) {
String sid = String.valueOf(UUID.randomUUID());
requestTemplate.header(T_REQUEST_ID, sid);
}

}
};
}
  1. 自定义BaseResponseAdvice并实现ResponseBodyAdvice
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
java复制代码@RestControllerAdvice(basePackages = "com.javadaily")
@Slf4j
public class BaseResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;

@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

//Feign请求时通过拦截器设置请求头,如果是Feign请求则直接返回实体对象
boolean isFeign = serverHttpRequest.getHeaders().containsKey(OpenFeignConfig.T_REQUEST_ID);

if(isFeign){
return object;
}

if(object instanceof String){
return objectMapper.writeValueAsString(ResultData.success(object));
}

if(object instanceof ResultData){
return object;
}

return ResultData.success(object);
}

}

如果为Feign请求,则不做转换,否则通过ResultData进行包装。

  1. 修改后端接口返回对象
1
2
3
4
5
6
java复制代码@ApiOperation("select接口")
@GetMapping("/account/getByCode/{accountCode}")
@ResponseBody
public AccountDTO getByCode(@PathVariable(value = "accountCode") String accountCode){
return accountService.selectByCode(accountCode);
}

不需要在接口上返回封装体ResultData,经由ResponseBodyAdvice实现自动增强。

  1. 修改feign调用逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    @Override
public OrderDTO selectByNo(String orderNo) {
OrderDTO orderDTO = new OrderDTO();

//1. 查询订单基础信息
Order order = orderMapper.selectByNo(orderNo);
BeanUtils.copyProperties(order,orderDTO);
//2. 远程获取用户信息
AccountDTO accountResult = accountClient.getByCode(order.getAccountCode());
orderDTO.setAccountDTO(accountResult);

return orderDTO;
}

经过上面四个步骤,在正常情况下 达到了我们优化目标,通过Feign调用直接返回实体对象,通过网关调用返回统一包装体。看上去很完美,但是实际很糟糕,这又导致了第三个问题,Feign如何处理异常?

Feign异常处理

现状分析

生产者对于提供的接口方法会进行业务规则校验,对于不符合业务规则的调用请求会抛出业务异常BizException,而正常情况下项目上会有个全局异常处理器,他会捕获业务异常BizException,并将其封装成统一包装体返回给调用方,现在让我们来模拟这种业务场景:

  1. 生产者抛出业务异常
1
2
3
4
5
6
7
8
9
java复制代码public AccountDTO selectByCode(String accountCode) {
if("javadaily".equals(accountCode)){
throw new BizException(accountCode + "用户不存在");
}
AccountDTO accountDTO = new AccountDTO();
Account account = accountMapper.selectByCode(accountCode);
BeanUtils.copyProperties(account,accountDTO);
return accountDTO;
}

当用户名为 “javadaily” 的时候直接抛出业务异常BizException。

  1. 全局异常拦截器捕获业务异常
1
2
3
4
5
6
7
8
9
10
java复制代码/**
* 自定义业务异常处理
* @param e the e
* @return ResultData
*/
@ExceptionHandler(BaseException.class)
public ResultData<String> exception(BaseException e) {
log.error("业务异常 ex={}", e.getMessage(), e);
return ResultData.fail(e.getErrorCode(),e.getMessage());
}

捕获BaseException,BizException属于BaseException的子类,同样会被捕获。

  1. 调用方直接模拟异常数据调用
1
2
3
4
5
6
7
8
9
10
11
java复制代码public OrderDTO selectByNo(String orderNo) {
OrderDTO orderDTO = new OrderDTO();
//1. 查询订单基础信息
Order order = orderMapper.selectByNo(orderNo);
BeanUtils.copyProperties(order,orderDTO);

//2. 远程获取用户信息
AccountDTO accountResult = accountClient.getByCode("javadaily");
orderDTO.setAccountDTO(accountResult);
return orderDTO;
}

调用getByCode()方法时传递 “javadaily” ,触发生产者的业务异常规则。

Feign捕获不到异常

此时我们调用selectByNo()方法,会发现调用方捕获不到异常,accountDTO全部被设置成为null,如下:

返回结果

将Feign的日志级别设置为FULL查看返回结果:

image-20211102113222395

通过日志可以看到Feign其实获取到了全局异常处理器转换后的统一对象ResultData,并且响应码为200,正常响应。而消费者接受对象为AccountDTO,属性无法转换,全部当作NULL值处理。

很显然,这不符合我们正常业务逻辑,我们应该要直接返回生产者抛出的异常,那如何处理呢?

很简单,我们只需要给全局异常拦截器中业务异常设置一个非200的响应码即可,如:

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 自定义业务异常处理。
* @param e the e
* @return ResultData
*/
@ExceptionHandler(BaseException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(BaseException e) {
log.error("业务异常 ex={}", e.getMessage(), e);
return ResultData.fail(e.getErrorCode(),e.getMessage());
}

这样消费者就可以正常捕获到生产者抛出的业务异常,如下图所示:

image-20211102143348001

异常被额外封装

虽然能获取到异常,但是Feign捕获到异常后又在业务异常的基础上再进行了一次封装。

原因是当feign调用结果为非200的响应码时就触发了Feign的异常解析,Feign的异常解析器会将其包装成FeignException,即在我们业务异常的基础上再包装一次。

可以在feign.codec.ErrorDecoder#decode()方法上打上断点观察执行结果,如下:

image-20211102110247523

很显然,这个包装后的异常我们并不需要,我们应该直接将捕获到的生产者的业务异常直接抛给前端,那这又该如何解决呢?

很简单,我们只需要重写Feign的异常解析器,重新实现decode逻辑,返回正常的BizException即可,而后全局异常拦截器又会捕获BizException!(感觉有点无限套娃的感觉)

代码如下:

  1. 重写Feign异常解析器
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
java复制代码/**
* 解决Feign的异常包装,统一返回结果
* @author 公众号:JAVA日知录
*/
@Slf4j
public class OpenFeignErrorDecoder implements ErrorDecoder {
/**
* Feign异常解析
* @param methodKey 方法名
* @param response 响应体
* @return BizException
*/
@Override
public Exception decode(String methodKey, Response response) {
log.error("feign client error,response is {}:",response);
try {
//获取数据
String errorContent = IOUtils.toString(response.body().asInputStream());
String body = Util.toString(response.body().asReader(Charset.defaultCharset()));

ResultData<?> resultData = JSON.parseObject(body,ResultData.class);
if(!resultData.isSuccess()){
return new BizException(resultData.getStatus(),resultData.getMessage());
}

} catch (IOException e) {
e.printStackTrace();
}

return new BizException("Feign client 调用异常");
}
}
  1. 在Feign配置文件中注入自定义的异常解码器
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig {
/**
* 自定义异常解码器
* @return OpenFeignErrorDecoder
*/
@Bean
public ErrorDecoder errorDecoder(){
return new OpenFeignErrorDecoder();
}
}
  1. 再次调用,符合预期。

image-20211102150719270

此时通过自定义Feign的异常解码器,直接抛出生产者的业务异常信息,完成目标。

总结

本文对Feign在使用过程中会遇到的问题做了个小小的总结,也提出了自己可能不太成熟的解决方案。当然,由于本人水平有限,提出的解决方案不一定是最好的,如果你有更好的解决方案,还请留言告诉我,谢谢!

此文为SpringCloud实战系列的第三十六篇,如果要看其他文章可以关注公号 java日知录 哟

本文转载自: 掘金

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

Golang实践录:使用gin框架实现转发功能:一些负载均衡

发表于 2021-11-17

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

近段时间需要实现一个转发 post 请求到指定后端服务的小工具,由于一直想学习 gin 框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究一些负载均衡算法的实现。

概述

本文实现的负载均衡纯粹是为了笔者的一个念想,并不具有实际指导意义。

本文假定有一个后端服务 URL 的数组,因此,在实现上,仅是输出数组的索引值。权重数组每个索引值对应一个后端服务。比如”1 3 3”,表示有3台服务器,第1台权重为1,第2、3台权重均为3。

算法实现及测试结果

负载均衡有很多类型,如:随机、加权随机;简单轮询、加权轮询、平滑加权轮询,等。本文仅实现几种轮询算法,并且按请求次序递增,不使用随机数。

简单轮询算法

算法描述:

按请求先后轮询服务器。接收到第一次请求,转发到第1台服务器,第二次请求,转发到第2台服务器,依次类推,如果轮询到最后一台服务器,再转发第一台。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码​
var lb_index int = 0
​
// 简单轮询
func getOneServerUrlForLB_RR(exTime string) (url string) {
​
  url = ""
  count := conf.TotalPort //len(conf.BackInfo)
​
  if lb_index >= count {
      lb_index = 0
  }
  fmt.Println("indx: ", lb_index, count)
  url = fmt.Sprintf("http://127.0.0.1:%d", conf.BackPorts[lb_index])
  fmt.Println("got url: ", url)
​
  lb_index += 1
​
  return
}

加权轮询算法

一个权重的示意:

3台机器(分别为A、B、C),权重分别为5,3,2。排列(注:按设置的权重先后列出):

1
2
3
4
css复制代码   5       3    2
1       5     8   10
| ----- | --- | -- |
  A       B     C

某一区间大,表示该服务器的权重大。 设一数,其值在1~10之间,即范围在权重总和之内,依次与5、3、2对比,如小则在该区间,如大,则减去前一数,再比对。看落到哪个区域。 举例: 设该数为3,与5对比,小,则落到第1区间,即选择服务器 A。 设该数为7,与5对比,大,取下一区间,即减去5,得2,与3对比,小,则落到第2区间,即选择服务器 B。 设该数为9,与5对比,大,减去5得4,与3对比,大,减去3得1,与2对比,小,第3区间,即选择服务器 C。

注意,我们关注的是某个区间出现的次数,并不关注是哪一个索引。以轮询 10 次为例,只要保证A、B、C服务器分别访问了5、3、2次即可。最简单的算法,就是将这次 10 次依次分配到A、B、C服务器。

代码:

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
go复制代码/*
测试用,假定有3组IP,每组分配好权重,根据请求顺序,按权重分配
可以认为,IP总数为权重总值,只是部分重复了,所以请求循环的次数就是权重总值
*/
// 因为测试,所以直接赋值,测试的总数不超过 conf.TotalPort
// conf.Weights 示例:[3]int{2, 5, 3}, 数组索引0表示第1个后端,1表示第2个后端
​
func getIndex_WRR(offset int, totalWeight int) (index int) {
​
   // 如果不指定,使用随机
   if offset == -1 {
       rand.Seed(time.Now().UnixNano())
       offset = rand.Int() % totalWeight
  }
   // fmt.Println("total: ", totalWeight, offset)
​
   // 注:这里关注的是某个索引值的次数,与顺序无关
   for index, w := range conf.Weights {
       if offset < w {
           return index
      }
       offset -= w
  }
​
   return
}
​
func getOneServerUrlForLB_WRR(exTime string) (url string) {
​
   url = ""
   count := conf.TotalPort
​
   totalWeight := 0
   for _, w := range conf.Weights {
       totalWeight += w
  }
​
   count = totalWeight // 注:按权重总数轮询
   if lb_index >= count {
       lb_index = 0
  }
   new_lb_index := getIndex_WRR(lb_index, totalWeight)
​
   if lb_index == 100 {
       fmt.Println("indx: ", lb_index, new_lb_index, count)
  }
   fmt.Printf("%d ", new_lb_index)
​
   url = fmt.Sprintf("http://127.0.0.1:%d", conf.BackPorts[new_lb_index])
   fmt.Println("got url: ", url)
​
   lb_index += 1
​
   return
}

实验结果:

1
2
3
4
5
6
7
8
bash复制代码./httpforward.exe -w "5 3 2"
0 0 0 0 0 1 1 1 2 2 0 0 0 0 0 1 1 1 2 2
​
./httpforward.exe -w "1 3 3"
0 1 1 1 2 2 2 0 1 1 1 2 2 2
​
./httpforward.exe -w "5 3 1"
0 0 0 0 0 1 1 1 2 0 0 0 0 0 1 1 1 2

为了数据可靠,轮询了2遍,下同。

平滑加权轮询算法

上述算法并不考虑服务器处理在效率,比如前面5次均在A服务器,其它服务器均为空闲状态,由此引出平滑加权轮询算法。笔者暂未参透该算法的证明过程,因此本文不涉及。

设置如下变量: 总权重值:totalWeight,其值固定。 固定权重数组:Weights,其值固定。 当前权重数组 CurrentWeights,其值可变。 当前权重数组最大值 maxWeight,CurrentWeights数组的最大值。

算法步骤: 0、某次请求到来。 1、判断当前权重数组(对应下表当前权重1)值是否全为0,如是,则用固定权重初始化之。 2、查获当前权重数组,获取最大值,对应的索引,即为需要返回的服务器。 3、将第2步的索引对应的值,减去权重总和,其它索引的值保持不变。(对应下表当前权重2) 4、 将第3步得到的当前权重数组的每个值,加上对应的固定权重值。 5、回到第1步,重复。

假定有3台服务器A、B、C,其权重依次为 5、1、1,权重总和为7。演算过程如下:

初始时,当前权重为[0,0,0],则用固定权重[5,1,1]初始化之。 此时,最大值为5,索引为0,返回服务器A。 将[5,1,1]的索引0值减去权重总和7,得到[-2,1,1]。 将[-2,1,1]加上[5,1,1],得到新的当前权重[3,2,2]。 此时,最大值为3,索引为0,返回服务器A。 将[3,2,2]的索引0值减去权重总和7,得到[-4,2,2]。 将[-4,2,2]加上[5,1,1],得到新的当前权重[1,3,3]。 (下略)

总体过程如下表:

请求次数 当前权重1 返回服务器 当前权重2
0 [0, 0,0] -- 初始化为[5, 1, 1]
1 [5, 1, 1] A [-2, 1, 1]
2 [3, 2, 2] A [-4, 2, 2]
3 [1, 3, 3] B [1, -4, 3]
4 [6, -3, 4] A [-1, -3, 4]
5 [4, -2, 5] C [4, -2, -2]
6 [9, -1, -1] A [2, -1, -1]
7 [7, 0, 0] A [0, 0, 0]
8 [5, 1, 1] A [-2, 1, 1]

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
go复制代码// 平滑加权
func getIndex_SWRR(offset int, totalWeight int) (index int) {
​
   initflag := 0
​
   // 0.判断是否全为0
   for _, w := range conf.CurrentWeights {
       if w == 0 {
           initflag++
      }
  }
   // 全为0,则需要初始化
   if initflag == len(conf.CurrentWeights) {
       for idx, w := range conf.Weights {
           conf.CurrentWeights[idx] = w
      }
  }
​
   // 1. 查询当前权重表最大者,并取该索引,即为所需结果
   maxWeight := 0
   for idx, w := range conf.CurrentWeights {
       if w > maxWeight {
           maxWeight = w
           index = idx
      }
  }
   // 2. 最大的那个权重,其值减去总权重
   conf.CurrentWeights[index] = maxWeight - totalWeight
   // fmt.Println("current222: ", conf.CurrentWeights)
​
   // 3. 新的当前权重,所对应的值加上初始权重
   // 为下次循环计算打下基础
   for idx, w := range conf.Weights {
       conf.CurrentWeights[idx] += w
  }
   return
}
​
func getOneServerUrlForLB_SWRR(exTime string) (url string) {
   url = ""
​
   totalWeight := 0
​
   for _, w := range conf.Weights {
       totalWeight += w
  }
​
   // 注:按权重总数轮询
   if lb_index >= totalWeight {
       lb_index = 0
  }
   new_lb_index := getIndex_SWRR(lb_index, totalWeight)
​
   if lb_index == 100 {
       fmt.Println("indx1: ", lb_index, new_lb_index, totalWeight)
  }
   fmt.Printf("%d ", new_lb_index)
   url = fmt.Sprintf("http://127.0.0.1:%d", conf.BackPorts[new_lb_index])
   //fmt.Println("got url: ", url)
​
   lb_index += 1
​
   return
}

实验结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码./httpforward.exe -w "5 1 1"
0 0 1 0 2 0 0 0 0 1 0 2 0 0
​
./httpforward.exe -w "1 3 3"
1 2 0 1 2 1 2 1 2 0 1 2 1 2
​
./httpforward.exe -w "5 3 1"
0 1 0 2 0 1 0 1 0 0 1 0 2 0 1 0 1 0
​
./httpforward.exe -w "2 5 3"
1 2 0 1 1 2 1 0 2 1 1 2 0 1 1 2 1 0 2 1

从结果上看,权重保持着比例,但响应的服务器分布较平衡,即不会出现权重大的服务器集中处理请求的情况。某次测试 nginx 的负载均衡,似乎默认便是此种方法,但算法实现不同,测试发现,在保持权重比例情况下,不同的服务器出现的顺序不同,未深究原因。

小结

本文主要根据网络的相关资料整理并用 golang 代码实现负载均衡部分算法。

参考

www.cnblogs.com/wsw-seu/p/1…

juejin.cn/post/684490…

本文转载自: 掘金

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

1…315316317…956

开发者博客

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