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

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


  • 首页

  • 归档

  • 搜索

Python flask框架浅析 前言 1 flask 框

发表于 2021-11-02

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

前言

Python 面向对象的高级编程语言,以其语法简单、免费开源、免编译扩展性高,同时也可以嵌入到C/C++程序和丰富的第三方库,Python运用到大数据分析、人工智能、web后端等应用场景上。

Python 目前主要流行的web框架:flask、Django、Tornado

image.png

补充一下,我们前面学习的库都是叫模块,那么框架与库的区别?

框架(framework)跟库的功能类似,但是框架在某一领域上功能更加全面。使用框架,会减少开发者重复造轮子,直接调用其类或者函数就可以实现需求的功能。

那么,我们本期来学习Python提供的 web 框架之一-flask框架相关方法的学习,Let’s go~

  1. flask 框架概述

flask 框架是一款基于 WSGI 的轻量级的web框架。flask 犹如耳熟能详的“麻雀虽小,五脏俱全”,因此flask具有简单可拓展的特点。

flask 框架优势:

  • 基于WSGI应用程序,必须使用显式实例化
  • 使用Werkzeug路由系统进行自动排序路由
  • 使用Jinja2模板引擎,快速方便使用模板
  • 使用线程局部变量,实现快速访问weby应用程序
  • 支持异步等待和ASCI(async-first)
  • 衔接单元测试,开发人员快速进行测试检查
  • 自带开发服务器,无需借助其他第三方网络服务

flask 框架获取

  • 使用pip进行下载flask
1
python复制代码pip install flask
  • 🔔 flask 包下载之后,会自动下载依赖库
+ Werkzeug 库:实现WSGI,前端与服务端的Python接口
+ Jinjia 库:模板语言,显示web页面
+ MarkupSafe 库:附带Jinjia,用于转义渲染模板不受信任的输入
+ itsDangerous:保护flask会话的cookie
+ Click库:用于编写命令行框架

  • 📢 flask 可选的依赖库,按实际需要进行下载
+ Blinker 库:提供对Singals的支持
+ Python-dotenv 库:运行命令时启动dotenv的环境变量来支持flask
+ Watchdog 为flask服务器提供快速的加载器

flask 框架使用

在代码中,使用from…import 导入flask包

1
python复制代码from flask import flask

长按 Ctrl,我们可以查看Flask源代码介绍

  1. flask demo步骤

在flask web 框架中,我们来看看要搭建一个DEMO的网站需要做哪些步骤:

  • 初始化:导入Flask库,定义Flask实例化对象

+ Flask实例化时,需要传入\_\_name\_\_:目的是接收包或者模块的名字作为参数
+ 让flask.helpers.get\_root\_path以便获得静态文件和模板文件的目录
1
2
python复制代码from flask import Flask
APP = Flask(__name__)
  • 定义一个应用方法,并且route装饰器装饰

+ 调用route()方法装饰创建的应用方法:目的是告诉flask怎么访问该函数
1
2
3
python复制代码@APP.route("/")
def hello():
return ("<h1>welcome juejin</h1>")
  • 在main中,flask实例化对象调用run()方法,进行运行

+ 如果在调试阶段,我们需要设置debug=True
1
2
3
python复制代码if __name__ == "__main__":

APP.run(debug=True)
  • 我们可以看到,后台运行日志

image.png

  • 我们访问日志中生成的链接🔗,可以在网页上看到解析的文字

  1. flask 基础功能

flask 上述实现的简易web程序,主要使用到啦flask模块路由功能

  • 路由功能

+ ### route()是装饰器,将URL和函数绑定


    - rule:绑定访问该函数的url规则
    - option:要转发给Rule对象的参数列表
1
python复制代码@app.route(url,optiion)
+ ### run()是服务器上运行应用程序


    - host:监听的主机名
    - port:主机端口号
    - debug:提供调试信息
    - options:要转发给底层Werkzeug服务器
  • 模版提供

flask 框架是基于Jinja2模板引擎实现的

+ 在项目中创建一个子目录templates,然后在其目录下创建demo.html
+ 在flask文件中,应用方法中需要调用



1
python复制代码render_template('demo.html', name=name)

总结

本期,我们对flask 微web框架优势特点、demo web应用程序程序步骤以及对flask模块中路由和模版两个功能进行基本了解。

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

Spring Security专栏(自定义用户认证)

发表于 2021-11-02

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

写在前面

通过前几篇的原理剖析,我们这一篇来一篇很干的理论实战,那就是如何自定义用户认证,

实现自定义用户认证的过程通常涉及两大部分内容,

一方面需要使用 User 和 Authority 对象来完成定制化的用户管理

另一方面需要把这个定制化的用户管理嵌入整个用户认证流程中。下面我们分别详细分析。

实现用户管理

我们知道在 Spring Security 中,代表用户信息的就是 UserDetails 接口。我们在前几篇文章中也介绍过 UserDetails 接口的具体定义。如果你想实现自定义的用户信息,扩展这个接口即可。实现方式如下所示:

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
java复制代码public class CustomUserDetails implements UserDetails {
 
    private final User user;
 
    public CustomUserDetails(User user) {
        this.user = user;
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities().stream()
                   .map(a -> new SimpleGrantedAuthority(a.getName()))
                   .collect(Collectors.toList());
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getUsername();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
    public final User getUser() {
        return user;
    }
}

上述 CustomUserDetails 类实现了 UserDetails 接口中约定的所有需要实现的方法。请注意,这里的 getAuthorities() 方法中,我们将 User 对象中的 Authority 列表转换为了 Spring Security 中代表用户权限的SimpleGrantedAuthority 列表。

当然,所有的自定义用户信息和权限信息都是维护在数据库中的,所以为了获取这些信息,我们需要创建数据访问层组件,这个组件就是 UserRepository,定义如下:

1
2
3
4
java复制代码public interface UserRepository extends JpaRepository<User, Integer> {
 
    Optional<User> findUserByUsername(String username);
}

这里只是简单扩展了 Spring Data JPA 中的 JpaRepository 接口,并使用方法名衍生查询机制定义了根据用户名获取用户信息的 findUserByUsername 方法。

现在,我们已经能够在数据库中维护自定义用户信息,也能够根据这些用户信息获取到 UserDetails 对象,那么接下来要做的事情就是扩展 UserDetailsService。自定义 CustomUserDetailsService 实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Service
public class CustomUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public CustomUserDetails loadUserByUsername(String username) {
        Supplier<UsernameNotFoundException> s =
                () -> new UsernameNotFoundException("Username" + username + "is invalid!");
 
        User u = userRepository.findUserByUsername(username).orElseThrow(s);
 
        return new CustomUserDetails(u);
    }
}

这里我们通过 UserRepository 查询数据库来获取 CustomUserDetails 信息,如果传入的用户名没有对应的 CustomUserDetails 则会抛出异常。

实现认证流程

我们再次回顾 AuthenticationProvider 的接口定义,如下所示:

1
2
3
4
5
6
7
8
9
java复制代码public interface AuthenticationProvider {
 
    //执行认证,返回认证结果
    Authentication authenticate(Authentication authentication)
             throws AuthenticationException;
 
    //判断是否支持当前的认证对象
    boolean supports(Class<?> authentication);
}

实现自定义认证流程要做的也是实现 AuthenticationProvider 中的这两个方法,而认证过程势必要借助于前面介绍的 CustomUserDetailsService。

我们先来看一下 AuthenticationProvider 接口的实现类 AuthenticationProviderService,如下所示:

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
java复制代码@Service
public class AuthenticationProviderService implements AuthenticationProvider {
 
    @Autowired
    private CustomUserDetailsService userDetailsService;
 
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
 
    @Autowired
    private SCryptPasswordEncoder sCryptPasswordEncoder;
 
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
 
        //根据用户名从数据库中获取 CustomUserDetails
        CustomUserDetails user = userDetailsService.loadUserByUsername(username);
 
        //根据所配置的密码加密算法分别验证用户密码
        switch (user.getUser().getPasswordEncoderType()) {
            case BCRYPT:
                return checkPassword(user, password, bCryptPasswordEncoder);
            case SCRYPT:
                return checkPassword(user, password, sCryptPasswordEncoder);
        }
 
        throw new  BadCredentialsException("Bad credentials");
    }
 
    @Override
    public boolean supports(Class<?> aClass) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
    }
 
    private Authentication checkPassword(CustomUserDetails user, String rawPassword, PasswordEncoder encoder) {
        if (encoder.matches(rawPassword, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
        } else {
            throw new BadCredentialsException("Bad credentials");
        }
    }
}

1、AuthenticationProviderService 类虽然看起来比较长,但代码基本都是自解释的。我们首先通过 CustomUserDetailsService 从数据库中获取用户信息并构造成 CustomUserDetails 对象。然后,根据指定的密码加密器对用户密码进行验证。

2、如果验证通过则构建一个UsernamePasswordAuthenticationToken 对象并返回,反之直接抛出 BadCredentialsException 异常。而在 supports() 方法中指定的就是这个目标 UsernamePasswordAuthenticationToken 对象。

好了。今天就学到这里,我们下期学一下安全配置,一点一点学,每次看太多可能也消化不了,最后多说一句。欢迎大家点击我头像查看Security专栏(设计模式专栏已完结),目前正在进行的并发队列专题和Security的专题

弦外之音

感谢你的阅读,如果你感觉学到了东西,麻烦您点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

聊聊不一样的策略模式(值得收藏)

copy对象,这个操作有点骚!

本文转载自: 掘金

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

LDAP 是个啥你知道吗?

发表于 2021-11-02

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

最近业务上需要和 LDAP 对接,在想 LDAP 是个啥?

赶紧查查资料,补齐一下我的知识盲区

LDAP 是什么?

LDAP 就是一个 轻量目录访问协议, 全称是 (Lightweight Directory Access Protocol),是基于X.500标准的轻量级目录访问协议

我们就可以理解成,他就是一个目录数据库,读的性能超级高,写性能不如人意,所以我们基本上都是查询它居多

LDAP 的产品有哪些?

  • OpenLDAP
  • MiscroSoft 的Active Directory
  • IBM Security Directory
  • sun 公司的 ldap
  • 其他的 ldap

相信只要熟悉了 LDAP 的基本协议,对接上述其他公司的产品应该都是信手拈来了

LDAP 必须知道的基本概念?

上面我们知道基于 LDAP 协议做出来的产品有很多,我们以 Active Directory 为例 统一来看看 LDAP 我们需要知道哪些基本概念吧

Active Directory 见名知意,就是活动目录的意思

其中的重要概念有:

DC Domain Controller

就是域控制器,是非常核心的

其实他就是一台 AD 里的服务器,安装了活动目录的 PC 而已,所有域用户的信息都会存在这个 Domain Controller 里面,他就是整个活动目录的服务器

当你本地的 PC 装上 Active Directory 之后,那么就没有本地用户了,就会全部自动变成域用户

成员计算机

跟域控制器建立了信任并加入到域里面的计算机 , 每台计算机加入到域的时候会生成信任密钥,类似于我们服务端常常使用 token 一样

并且每隔一段时间,访问其他成员的计算机是需要通过 Domain Controller 来拿到 token 来实现的,这样才能域计算机之间互相访问

例如,我是一个 Domain Controller , 我给计算机 A 发了一个 token, 那么我就会把这个计算机 A 加入到我的域当中来

SID Security id

用户和计算机都有一个 sid ,这是一个唯一标识

DNS 服务

搭建活动目录的时候还必须要设置一个 DNS 服务,因为我们访问活动目录的时候,一般是不会使用 ip 去访问的,而是使用 域名去访问

这个是需要在本地配置 DNS 的,一般配置的 DNS 服务 会和 活动目录的服务器放在一起

我们可以来看看我自己搭建的一个 ldap 服务器的例子:

DN Distinguished Name 分辨名

是一个唯一的标识,可以通过这个名字来找到一条记录,标识从树的根部到这个节点的路径,DN 也标识这个节点在树中的详细位置

例如这个是简单搭建的 ldap 服务器,图中的 ou=people, 我们可以看到 最上方有一个 DN 内容是 ou=people,dc=ldapdomain,dc=com 这就是一个 DN

DN 就类似于数据库的主键,标识一个唯一的项,这是不能重复的

RDN Reletive Distinguished Name

我们看到的 ou=people,dc=ldapdomain,dc=com 包含了 3 个 RDN,里面的每一个键值对是一个 RDN

DC domain component

上述 dc=ldapdomain,dc=com 这里的 dc ,可不是域名控制器的意思,而是域名组成的意思

例如这上面的一句话,就表示 dc=ldapdomain,dc=com 组成 ldapdomain.com

C country

代表国家

O Organization

代表组织,代表公司

OU Organization Unit

代表组织单元,代表部门

例如我这里的 ou=people,ou=group ,我们也可以理解 ou 代表分公司

这个 OU 下面是可以包含很多内容的,例如 计算机,用户,打印机,安全策略,其他组织等等

CN Common Name

表示一条记录的名字,图中 uid 也是表示一条记录的名字,他们是可以共存的

objectClass 对象类

objectClass 会预定义一些类,这个类上面还可以规定一些规则,例如定义一些标签字段等等

DIT Directory Information Tree

image-20211102232326544

目录信息树的意思,看我们自己的服务器,上述截图的位置就是一个 目录信息树

可视化的 LDAP 客户端

目前我开始使用的 2 款 LDAP可视化工具是:

  • LdapAdmin
  • LdapBrowser

我们一般使用 ldap 协议做对接,用的最多的就是 ldapsearch 命令,更多的是查,查的越多,就越能体现 ldap 的高性能

慢慢的摸索,把它吃掉

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

最全的Git基本操作命令,看完它,就没有啥能难倒你了 获取G

发表于 2021-11-02

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

本篇文章将涵盖你在使用 Git 完成各种工作中将要使用的各种基本命令。 在学习完本篇文章后,你能够配置并初始化一个仓库(repository)、开始或停止跟踪(track)文件、暂存(stage)或提交(commit)更改,如何浏览你的项目的历史版本以及不同提交(commits)间的差异,操作远程分支等。

获取Git仓库

初始化仓库

新建一个目录,然后我们进入该文件目录输入如下命令:

1
csharp复制代码git init

该命令会在这个目录下创建一个.git的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件。

克隆现有的仓库

假如你在github中创建了一个仓库或者你看中了一个开源项目想贡献一份自己的力量,就要把这个仓库拉下来,就需要进行clone操作

例如我在github中有一个仓库
image-20211102220952270

我现在把它clone下来

1
2
3
4
5
6
scss复制代码lirui@liruideMacBook-Pro IdeaProjects % git clone git@github.com:lrlr/git_learning.git
Cloning into 'git_learning'...
remote: Enumerating objects: 26, done.
remote: Total 26 (delta 0), reused 0 (delta 0), pack-reused 26
Receiving objects: 100% (26/26), done.
Resolving deltas: 100% (2/2), done.

Git基本操作命令

查看当前文件状态

1
lua复制代码git status

如果要查看目前文件处于什么状态,就需要用到此命令,通过这个命令可以查到文件是已更新,还是已暂存或者是未暂存等状态

1
2
3
4
5
6
7
8
9
10
11
12
vbnet复制代码​
 
 //示例
 lirui@liruideMacBook-Pro git_learning % git status
On branch master
Your branch is up to date with 'origin/master'.
​
Untracked files:
(use "git add <file>..." to include in what will be committed)
      .idea/
​
nothing added to commit but untracked files present (use "git add" to track)

通过这个示例可以看到我有一个文件还处于Untracked就是未暂存状态。

还有一个命令可以查到更为细致的文件状态展示,叫状态简览命令

1
2
3
4
bash复制代码lirui@liruideMacBook-Pro git_learning % git status -s
AM .idea/workspace.xml
M README.md
​

每个文件前面都会有对应的字母,这些字母的含义是什么呢?

新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记。 你可能注意到了 M 有两个可以出现的位置,出现在右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件被修改了并放入了暂存区。 例如,上面的状态报告显示: .idea/workspace.xml 文件在工作区被修改了但还没暂存,README.md 文件被修改了并将修改后的文件放入了暂存区。

跟踪新文件

1
csharp复制代码git add

如果需要将一个新增的文件或者修改的文件加入到暂存状态,就需要使用跟踪命令

1
2
3
4
5
csharp复制代码//将本地所有的文件都加入到暂存区
git add .
​
// 只将指定的文件加入到暂存区
git add 具体的文件名

查看已暂存和未暂存之间的修改

1
复制代码git diff

如果你想知道你对一个文件修改了哪些东西,就可以通过git diff命令来查看,例如我的README.md是已暂存的文件,然后我在里面新加了一句话,不暂存,我们来看下git diff 命令会显示什么

1
2
3
4
5
6
7
8
9
diff复制代码lirui@liruideMacBook-Pro git_learning % git diff
diff --git a/README.md b/README.md
index e77d1d2..eb87118 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
# git_learning
+这是我新加的一句话
\ No newline at end of file

这里可以看到就会把我修改的内容显示出来,注意,git diff命令只会展示未暂存的修改内容,如果你修改后暂存了就不会再显示你的修改

1
2
3
sql复制代码lirui@liruideMacBook-Pro git_learning % git add .
lirui@liruideMacBook-Pro git_learning % git diff
lirui@liruideMacBook-Pro git_learning %

提交更新

1
sql复制代码git commit

通过commit命令我们就可以将所有暂存的数据提交到本地数据库,例如如下命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ruby复制代码lirui@liruideMacBook-Pro git_learning % git commit
提交拉
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#   (use "git push" to publish your local commits)
#
# Changes to be committed:
#       modified:   .idea/workspace.xml
#       modified:   README.md
:wq
​

这个命令需要你自行填写提交备注,保存后才能正式提交成功,这样操作其实很繁琐,我们还有一个简便的提交命令可以在提交命令当前行输入命令

1
2
3
4
5
6
7
sql复制代码git commit -m "备注内容"
​
//示例
lirui@liruideMacBook-Pro git_learning % git commit -m '提交'
[master dc733e0] 提交
2 files changed, 3 insertions(+), 3 deletions(-)
lirui@liruideMacBook-Pro git_learning %

跳过使用暂存区域

提交的数据必须是已经暂存的数据,每次进行提交操作还需要执行一下暂存命令,如何跳过暂存进行直接提交呢?

只需要给 git commit 加上 -a选项即可,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
perl复制代码lirui@liruideMacBook-Pro git_learning % git status
On branch master
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
​
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
      modified:   README.md
​
no changes added to commit (use "git add" and/or "git commit -a")
lirui@liruideMacBook-Pro git_learning %
​

这里可以看到我对README.md做了修改但是是未暂存的状态,这时候我们直接操作git commit -a看下是否能提交成功

1
2
3
4
sql复制代码lirui@liruideMacBook-Pro git_learning % git commit -a -m '跳过暂存'
[master 995917f] 跳过暂存
2 files changed, 2 insertions(+), 2 deletions(-)
lirui@liruideMacBook-Pro git_learning %

直接提交成功,这样就省去了我们多个操作命令,一个命令就可以完成提交

移除某个文件

1
bash复制代码git rm

如果想从Git中移除某个文件,可以使用git rm命令完成,这个命令不仅会将该文件从暂存区域移除,还会从工作目录中删除指定的文件。

1
2
3
4
5
6
bash复制代码lirui@liruideMacBook-Pro git_learning % ls
README.md
lirui@liruideMacBook-Pro git_learning % git rm README.md
rm 'README.md'
lirui@liruideMacBook-Pro git_learning % ls
lirui@liruideMacBook-Pro git_learning %

这里我移除了REDME.md文件,这个文件在我的目录中也同时被移除了

还有一种场景是我只想在Git仓库中删除文件,但希望在工作目录中保留,那么我们可以使用git rm –cached

1
2
3
4
5
6
7
8
9
10
11
12
13
vbnet复制代码//示例代码
lirui@liruideMacBook-Pro git_learning % git rm --cached README.md
rm 'README.md'
lirui@liruideMacBook-Pro git_learning % ls
README.md
lirui@liruideMacBook-Pro git_learning % git status
On branch master
Your branch is up to date with 'origin/master'.
​
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
      deleted:   README.md
​

通过示例可以看到,README.md已经在Git中删除,但是在工作目录中还是完美保留的

查看提交历史

1
bash复制代码git log

如果你想查看你的提交历史,实现这个查看最简单的命令就是git log

image-20211102231540603

执行该命令就会列出你的所有提交记录,我的提交记录比较长,就截图展示了部分历史。

git log 有许多选项可以帮助你搜寻你所要找的提交, 接下来我们介绍些最常用的。

一个常用的选项是 -p,用来显示每次提交的内容差异。 你也可以加上 -2 来仅显示最近两次提交w q

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码lirui@liruideMacBook-Pro git_learning % git log -p -2
commit 028c66e79f8b5bf6c6176fc429bd7921561418bb (HEAD -> master, origin/master, origin/HEAD)
Author: lirui <630240701@qq.com>
Date:   Tue Nov 2 23:08:06 2021 +0800
​
  ss
​
diff --git a/README.md b/README.md
index 22ded55..3f151fa 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-:
\ No newline at end of file
+sssss
\ No newline at end of file
​
commit f41cf3a5a08507beec5792f91c0f45fa709e14db
Author: lirui <630240701@qq.com>
Date:   Tue Nov 2 23:06:06 2021 +0800
​

git log命令除了显示基本信息,还会显示一些commit 信息,如果你只想看到每次提交的简略统计信息,那可以使用 –stat选项

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码lirui@liruideMacBook-Pro git_learning % git log --stat
commit 028c66e79f8b5bf6c6176fc429bd7921561418bb (HEAD -> master, origin/master, origin/HEAD)
Author: lirui <630240701@qq.com>
Date:   Tue Nov 2 23:08:06 2021 +0800
​
  ss
​
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
​
commit f41cf3a5a08507beec5792f91c0f45fa709e14db
Author: lirui <630240701@qq.com>
Date:   Tue Nov 2 23:06:06 2021 +0800

git log命令还支持许多其他选项,我就不一一列举了,我这里就展示一些供大家参考:image-20211102232203219

撤销操作

1
sql复制代码git commit --amend

有时候我们可能提交完之后发现自己的提交信息写错了,或者发现漏了几个文件没提交,就可以使用–admend命令来重新提交。

1
2
3
4
5
6
7
8
sql复制代码lirui@liruideMacBook-Pro git_learning % git commit -m '提交'
[master 97cdd20] 提交
2 files changed, 10 insertions(+), 4 deletions(-)
lirui@liruideMacBook-Pro git_learning % git commit -m '修改描述' --amend
[master 7a53706] 修改描述
Date: Tue Nov 2 23:58:32 2021 +0800
2 files changed, 10 insertions(+), 4 deletions(-)
lirui@liruideMacBook-Pro git_learning %

最终你只会有一个提交 - 第二次提交将代替第一次提交的结果。

取消暂存的文件

1
perl复制代码git reset HEAD

当你有两个文件都还没有暂存,这时你只想暂存一个文件,结果不小心执行了git add .,这时需要取消某个文件的暂存状态,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
perl复制代码# 两个文件均已暂存
lirui@liruideMacBook-Pro git_learning % git status
Changes to be committed:
      modified:   README.md
      modified:   test.md
​
# 取消test文件的暂存
lirui@liruideMacBook-Pro git_learning % git reset HEAD test.md
Unstaged changes after reset:
M       test.md
​
# 查看状态,test文件变成了未暂存状态
lirui@liruideMacBook-Pro git_learning % git status            
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
      modified:   test.md
​

查看远程仓库

1
复制代码git remote

如果想查看你已经配置的远程仓库服务器,可以运行 git remote 命令。它会列出你指定的远程服务器的简写,如果你想看到这个简写对应的远程服务器的url,可以指定选项 -v。

1
2
3
4
5
scss复制代码lirui@liruideMacBook-Pro git_learning % git remote
origin
lirui@liruideMacBook-Pro git_learning % git remote -v
origin  git@github.com:lrlr/git_learning.git (fetch)
origin  git@github.com:lrlr/git_learning.git (push)

添加远程仓库

运行 git remote add <shortname> <url> 添加一个新的远程 Git 仓库

1
2
3
4
5
6
7
scss复制代码lirui@liruideMacBook-Pro git_learning % git remote add mg git@github.com:lrlr/git_learning.git
lirui@liruideMacBook-Pro git_learning % git remote -v
mg      git@github.com:lrlr/git_learning.git (fetch)
mg      git@github.com:lrlr/git_learning.git (push)
origin  git@github.com:lrlr/git_learning.git (fetch)
origin  git@github.com:lrlr/git_learning.git (push)
​

从远程仓库拉取

1
css复制代码git fetch [remote-name]

这个命令会访问远程仓库,从中拉取所有你还没有的数据。

注意它并不会自动合并或修改你当前的工作,当准备好时你必须手动将其合并入你的工作。

如何你想拉取后自动合并远程分支到当前分支,可以使用git pull

推送到远程分支

1
css复制代码git push [remote-name] [branch-name]

例如你想讲本地master分支推送远程分支origin上,可以进行如下操作

1
2
3
4
5
6
7
8
9
10
css复制代码lirui@liruideMacBook-Pro git_learning % git push origin master
Enumerating objects: 17, done.
Counting objects: 100% (17/17), done.
Delta compression using up to 8 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (13/13), 1.25 KiB | 1.25 MiB/s, done.
Total 13 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:lrlr/git_learning.git
  028c66e..1be830c master -> master

现在,你可以完成所有基本的 Git 本地操作-创建或者克隆一个仓库、做更改、暂存并提交这些更改、浏览你的仓库从创建到现在的所有更改的历史、撤销、取消暂存、查看、拉取、推送远程分支。 下一篇文章我将介绍Git最有亮点的功能,Git分支,还没有关注我的小伙伴赶紧关注一波吧,带你畅游Git的世界。

本文转载自: 掘金

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

Python 批量爬取猫咪图片实现千图成像 前言 爬取猫咪图

发表于 2021-11-02

一起用代码吸猫!本文正在参与【喵星人征文活动】

前言

使用 Python 爬取猫咪图片,并为猫咪🐱制作千图成像!

爬取猫咪图片

本文使用的 Python 版本是 3.10.0 版本,可直接在官网下载:www.python.org 。

Pythonn 的安装配置过程在此不做详细介绍,网上随意搜都是教程!

1、爬取绘艺素材网站

爬取网站:猫咪图片

首先安装必须的库:

1
2
3
4
bash复制代码pip install BeautifulSoup4
pip install requests
pip install urllib3
pip install lxml

爬取图片代码:

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
python复制代码from bs4 import BeautifulSoup
import requests
import urllib.request
import os

# 第一页猫咪图片网址
url = 'https://www.huiyi8.com/tupian/tag-%E7%8C%AB%E5%92%AA/1.html'
# 图片保存路径,这里 r 表示不转义
path = r"/Users/lpc/Downloads/cats/"
# 判断目录是否存在,存在则跳过,不存在则创建
if os.path.exists(path):
pass
else:
os.mkdir(path)


# 获得所有猫咪图片网页地址
def allpage():
all_url = []
# 循环翻页次数 20 次
for i in range(1, 20):
# 替换翻页的页数,这里的 [-6] 是指网页地址倒数第 6 位
each_url = url.replace(url[-6], str(i))
# 将所有获取的 url 加入 all_url 数组
all_url.append(each_url)
# 返回所有获取到的地址
return all_url


# 主函数入口
if __name__ == '__main__':
# 调用 allpage 函数获取所有网页地址
img_url = allpage()
for url in img_url:
# 获得网页源代码
requ = requests.get(url)
req = requ.text.encode(requ.encoding).decode()
html = BeautifulSoup(req, 'lxml')
# 添加一个 url 数组
img_urls = []
# 获取 html 中所有 img 标签的内容
for img in html.find_all('img'):
# 筛选匹配 src 标签内容以 http 开头,以 jpg 结束
if img["src"].startswith('http') and img["src"].endswith("jpg"):
# 将符合条件的 img 标签加入 img_urls 数组
img_urls.append(img)
# 循环数组中所有 src
for k in img_urls:
# 获取图片 url
img = k.get('src')
# 获取图片名称,强制类型转换很重要
name = str(k.get('alt'))
type(name)
# 给图片命名
file_name = path + name + '.jpg'
# 通过图片 url 和图片名称下载猫咪图片
with open(file_name, "wb") as f, requests.get(img) as res:
f.write(res.content)
# 打印爬取的图片
print(img, file_name)

📢 注意: 以上代码无法直接复制运行,需要修改下载图片路径:/Users/lpc/Downloads/cats,请修改为读者本地的保存路径!

爬取成功:

共爬取 346 张猫咪图片!

2、爬取 ZOL 网站

爬取 ZOL 网址:萌猫

爬取代码:

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
python复制代码import requests
import time
import os
from lxml import etree

# 请求的路径
url = 'https://desk.zol.com.cn/dongwu/mengmao/1.html'
# 图片保存路径,这里 r 表示不转义
path = r"/Users/lpc/Downloads/ZOL/"
# 这里是你要保存的路径位置 前面的r 表示这段不转义
if os.path.exists(path): # 判断目录是否存在,存在则跳过,不存在则创建
pass
else:
os.mkdir(path)
# 请求头
headers = {"Referer": "Referer: http://desk.zol.com.cn/dongman/1920x1080/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", }

headers2 = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36 SE 2.X MetaSr 1.0", }


def allpage(): # 获得所有网页
all_url = []
for i in range(1, 4): # 循环翻页次数
each_url = url.replace(url[-6], str(i)) # 替换
all_url.append(each_url)
return all_url # 返回地址列表


# TODO 获取到Html页面进行解析
if __name__ == '__main__':
img_url = allpage() # 调用函数
for url in img_url:
# 发送请求
resq = requests.get(url, headers=headers)
# 显示请求是否成功
print(resq)
# 解析请求后获得的页面
html = etree.HTML(resq.text)
# 获取a标签下进入高清图页面的url
hrefs = html.xpath('.//a[@class="pic"]/@href')
# TODO 进入更深一层获取图片 高清图片
for i in range(1, len(hrefs)):
# 请求
resqt = requests.get("https://desk.zol.com.cn" + hrefs[i], headers=headers)
# 解析
htmlt = etree.HTML(resqt.text)
srct = htmlt.xpath('.//img[@id="bigImg"]/@src')
# 截图片名称
imgname = srct[0].split('/')[-1]
# 根据url获取图片
img = requests.get(srct[0], headers=headers2)
# 执行写入图片到文件
with open(path + imgname, "ab") as file:
file.write(img.content)
# 打印爬取的图片
print(img, imgname)

爬取成功:

共爬取 81 张猫咪图片!

3、爬取百度图片网站

爬取百度网站:百度猫咪图片

1、爬取图片代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
python复制代码import requests
import os
from lxml import etree
path = r"/Users/lpc/Downloads/baidu1/"
# 判断目录是否存在,存在则跳过,不存在则创建
if os.path.exists(path):
pass
else:
os.mkdir(path)

page = input('请输入要爬取多少页:')
page = int(page) + 1
header = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
}
n = 0
pn = 1
# pn是从第几张图片获取 百度图片下滑时默认一次性显示30张
for m in range(1, page):
url = 'https://image.baidu.com/search/acjson?'

param = {
'tn': 'resultjson_com',
'logid': '7680290037940858296',
'ipn': 'rj',
'ct': '201326592',
'is': '',
'fp': 'result',
'queryWord': '猫咪',
'cl': '2',
'lm': '-1',
'ie': 'utf-8',
'oe': 'utf-8',
'adpicid': '',
'st': '-1',
'z': '',
'ic': '0',
'hd': '1',
'latest': '',
'copyright': '',
'word': '猫咪',
's': '',
'se': '',
'tab': '',
'width': '',
'height': '',
'face': '0',
'istype': '2',
'qc': '',
'nc': '1',
'fr': '',
'expermode': '',
'nojc': '',
'acjsonfr': 'click',
'pn': pn, # 从第几张图片开始
'rn': '30',
'gsm': '3c',
'1635752428843=': '',
}
page_text = requests.get(url=url, headers=header, params=param)
page_text.encoding = 'utf-8'
page_text = page_text.json()
print(page_text)
# 先取出所有链接所在的字典,并将其存储在一个列表当中
info_list = page_text['data']
# 由于利用此方式取出的字典最后一个为空,所以删除列表中最后一个元素
del info_list[-1]
# 定义一个存储图片地址的列表
img_path_list = []
for i in info_list:
img_path_list.append(i['thumbURL'])
# 再将所有的图片地址取出,进行下载
# n将作为图片的名字
for img_path in img_path_list:
img_data = requests.get(url=img_path, headers=header).content
img_path = path + str(n) + '.jpg'
with open(img_path, 'wb') as fp:
fp.write(img_data)
n = n + 1

pn += 29

2、爬取代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
python复制代码# -*- coding:utf-8 -*-
import requests
import re, time, datetime
import os
import random
import urllib.parse
from PIL import Image # 导入一个模块

imgDir = r"/Volumes/DBA/python/img/"
# 设置headers 为了防止反扒,设置多个headers
# chrome,firefox,Edge
headers = [
{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Connection': 'keep-alive'
},
{
"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Connection': 'keep-alive'
},
{
"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19041',
'Accept-Language': 'zh-CN',
'Connection': 'keep-alive'
}
]

picList = [] # 存储图片的空 List

keyword = input("请输入搜索的关键词:")
kw = urllib.parse.quote(keyword) # 转码


# 获取 1000 张百度搜索出来的缩略图 list
def getPicList(kw, n):
global picList
weburl = r"https://image.baidu.com/search/acjson?tn=resultjson_com&logid=11601692320226504094&ipn=rj&ct=201326592&is=&fp=result&queryWord={kw}&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=&z=&ic=&hd=&latest=&copyright=&word={kw}&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=1&fr=&expermode=&force=&cg=girl&pn={n}&rn=30&gsm=1e&1611751343367=".format(
kw=kw, n=n * 30)
req = requests.get(url=weburl, headers=random.choice(headers))
req.encoding = req.apparent_encoding # 防止中文乱码
webJSON = req.text
imgurlReg = '"thumbURL":"(.*?)"' # 正则
picList = picList + re.findall(imgurlReg, webJSON, re.DOTALL | re.I)


for i in range(150): # 循环数比较大,如果实际上没有这么多图,那么 picList 数据不会增加。
getPicList(kw, i)

for item in picList:
# 后缀名 和名字
itemList = item.split(".")
hz = ".jpg"
picName = str(int(time.time() * 1000)) # 毫秒级时间戳
# 请求图片
imgReq = requests.get(url=item, headers=random.choice(headers))
# 保存图片
with open(imgDir + picName + hz, "wb") as f:
f.write(imgReq.content)
# 用 Image 模块打开图片
im = Image.open(imgDir + picName + hz)
bili = im.width / im.height # 获取宽高比例,根据宽高比例调整图片大小
newIm = None
# 调整图片的大小,最小的一边设置为 50
if bili >= 1:
newIm = im.resize((round(bili * 50), 50))
else:
newIm = im.resize((50, round(50 * im.height / im.width)))
# 截取图片中 50*50 的部分
clip = newIm.crop((0, 0, 50, 50)) # 截取图片,crop 裁切
clip.convert("RGB").save(imgDir + picName + hz) # 保存截取的图片
print(picName + hz + " 处理完毕")

爬取成功:

总结: 三个网站共爬取 1600 张猫咪图片!

千图成像

爬取千张图片之后,接下来就需要使用图片拼接成一张猫咪图片,即千图成像。

1、Foto-Mosaik-Edda 软件实现

首先下载软件:Foto-Mosaik-Edda Installer,如果无法下载,直接百度搜索 foto-mosaik-edda!

Windows 安装 Foto-Mosaik-Edda 过程比较简单!

📢 注意: 但是需要提前安装 .NET Framework 2,否则报错如下无法成功安装!

启用 .NET Framework 2 的方式:

确认已经成功启用:

接下来就可以继续安装!

安装完成后,打开如下:

第一步,创建一个图库:

第二步,千图成像:

这里勾选第一步创建好的图库:

见证奇迹的时刻:

再制作一张可爱的猫咪:

大功告成!

2、使用 Python 实现

首先,选取一张图片:

运行以下代码:

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
python复制代码# -*- coding:utf-8 -*-
from PIL import Image
import os
import numpy as np

imgDir = r"/Volumes/DBA/python/img/"
bgImg = r"/Users/lpc/Downloads/494.jpg"


# 获取图像的平均颜色值
def compute_mean(imgPath):
'''
获取图像平均颜色值
:param imgPath: 缩略图路径
:return: (r,g,b)整个缩略图的rgb平均值
'''
im = Image.open(imgPath)
im = im.convert("RGB") # 转为 rgb模式
# 把图像数据转为数据序列。以行为单位,每行存储每个像素点的色彩
'''如:
[[ 60 33 24]
[ 58 34 24]
...
[188 152 136]
[ 99 96 113]]

[[ 60 33 24]
[ 58 34 24]
...
[188 152 136]
[ 99 96 113]]
'''
imArray = np.array(im)
# mean()函数功能:求指定数据的取均值
R = np.mean(imArray[:, :, 0]) # 获取所有 R 值的平均值
G = np.mean(imArray[:, :, 1])
B = np.mean(imArray[:, :, 2])
return (R, G, B)


def getImgList():
"""
获取缩略图的路径及平均色彩
:return: list,存储了图片路径、平均色彩值。
"""
imgList = []
for pic in os.listdir(imgDir):
imgPath = imgDir + pic
imgRGB = compute_mean(imgPath)
imgList.append({
"imgPath": imgPath,
"imgRGB": imgRGB
})
return imgList


def computeDis(color1, color2):
'''
计算两张图的颜色差,计算机的是色彩空间距离。
dis = (R**2 + G**2 + B**2)**0.5
参数:color1,color2 是色彩数据 (r,g,b)
'''
dis = 0
for i in range(len(color1)):
dis += (color1[i] - color2[i]) ** 2
dis = dis ** 0.5
return dis


def create_image(bgImg, imgDir, N=2, M=50):
'''
根据背景图,用头像填充出新图
bgImg:背景图地址
imgDir:头像目录
N:背景图缩放的倍率
M:头像的大小(MxM)
'''
# 获取图片列表
imgList = getImgList()

# 读取图片
bg = Image.open(bgImg)
# bg = bg.resize((bg.size[0] // N, bg.size[1] // N)) # 缩放。建议缩放下原图,图片太大运算时间很长。
bgArray = np.array(bg)
width = bg.size[0] * M # 新生成图片的宽度。每个像素倍放大 M 倍
height = bg.size[1] * M # 新生成图片的高度

# 创建空白的新图
newImg = Image.new('RGB', (width, height))

# 循环填充图
for x in range(bgArray.shape[0]): # x,行数据,可以用原图宽替代
for y in range(bgArray.shape[1]): # y,列数据,,可以用原图高替代
# 找到距离最小的图片
minDis = 10000
index = 0
for img in imgList:
dis = computeDis(img['imgRGB'], bgArray[x][y])
if dis < minDis:
index = img['imgPath']
minDis = dis
# 循环完毕,index 就是存储了色彩最相近的图片路径
# minDis 存储了色彩差值
# 填充
tempImg = Image.open(index) # 打开色差距离最小的图片
# 调整图片大小,此处可以不调整,因为我在下载图的时候就已经调整好了
tempImg = tempImg.resize((M, M))
# 把小图粘贴到新图上。注意 x,y ,行列不要搞混了。相距 M 粘贴一张。
newImg.paste(tempImg, (y * M, x * M))
print('(%d, %d)' % (x, y)) # 打印进度。格式化输出 x,y

# 保存图片
newImg.save('final.jpg') # 最后保存图片


create_image(bgImg, imgDir)

运行结果:

从上图可以发现,图片的清晰度堪比原图,放大之后小图依然清晰可见!

📢 注意: 使用 Python 运行会比较慢!

写在最后

😄 真好,又可以愉快地吸猫了~

本文参考:

  • python批量爬取猫咪图片
  • Python实现多线程并发下载大文件
  • python爬取ZOL桌面壁纸高清图片
  • Python爬取百度图片
  • Python—如何实现千图成像:初级篇
  • Python学习笔记17:玩转千图成像

本文转载自: 掘金

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

php合并数组并保留键值的方法

发表于 2021-11-02

合并数组一般会使用array_merge,但是结果会重新排列键名,覆盖原来的键。使用array_merge,如果是关联数组,合并后相同的键名会被后面的覆盖;如果是索引数组,键名相同不会覆盖,会重新索引。那如果要合并两个数组且不改变他们原来的键,怎么办?

解决

使用 “+” 运算符合并数组,可以保留数组的键值,如果合并的数组中含有相同的键值,后面的不会覆盖前面的键值(前面的优先)

1
2
3
4
5
6
7
8
php复制代码<?php
$form_data1 = array(11=>'A',12=>'B',13=>'C',14=>'D');
$form_data2 = array(25=>'B',26=>'A',27=>'D',28=>'C');

$result = $form_data1 + $form_data2;

print_r($result);
?>

输出:

1
2
php复制代码
Array ( [11] => A [12] => B [13] => C [14] => D [25] => B [26] => A [27] => D [28] => C )

laravel collect

如果是在laravel下,可以将数组变成collect实例,再使用->values(),输出结果的序号就是0开头的连续编号了

本文转载自: 掘金

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

【设计模式】通过简单案例学习桥接模式

发表于 2021-11-02

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

桥接模式

  桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。

  在有多种可能会变化的情况下,用继承会造成类很多,扩展起来不灵活,因此使用桥接模式将抽象部分与实现部分分离,使它们都可以独立的变化。
桥接模式优点:

  • 1、抽象和实现的分离。
  • 2、优秀的扩展能力。
  • 3、实现细节对客户透明。

  下面结合案例进行简单的介绍。

案例分析

  本次采用的是比较经典的大家都熟悉的案例,是某年软件设计师的试题。具体如下:

  现欲实现一个图像浏览系统,要求该系统能够显示BMP、JPEG和GIF三种格式的文件,并且能够在 Windows和 Linux两种操作系统上运行。系统首先将BMP、JPEG和GIF三种格式的文件解析为像素矩阵,然后将像素矩阵显示在屏幕上。系统需具有较好的扩展性以支持新的文件格式和操作系统。为满足上述需求并减少所需生成的子类数目,采用桥接( Bridge设计模式进行设计,所得类图如下图所示。
图片.png

  通过题干分析可以知道需要读取的文件格式可能是多种的,而且可能在不同的操作系统中去展示图像,而需要将文件的展示与系统的实现集合起来。经过分析,文件的格式可能是多种多样的,操作系统也是多种多样的,因此使用桥接模式对该题设计比较好。
下面开始进行简单代码实现。下图是建好的文件。
图片.png

编写图像展示抽象类

编写图像展示抽象类,是为了在后期,新加入不同的操作系统之后,只需要集成此抽象类,就可以快速实现不同类型文件的图像展示。使图像文件类型与操作系统之间进行解耦。
编写代码如下,只有一个图像输出的方法。

1
2
3
4
5
6
7
8
9
java复制代码/**
* @ClassName ImageHandler
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public abstract class ImageHandler {
public abstract void doPaint(PictureElement pe);
}

其中PictureElement 是展示的图像像素点,由于是模拟展示,本次针对此对象只显示文件名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码/**
* @ClassName PictureElement
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public class PictureElement {
private String fileName;

public String getFileName() {
return fileName;
}

public PictureElement(String fileName) {
this.fileName = fileName;
}

public void setFileName(String fileName) {
this.fileName = fileName;
}
}

Linux系统展示

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码/**
* @ClassName LinuxImageImpl
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public class LinuxImageImpl extends ImageHandler {
@Override
public void doPaint(PictureElement pe) {
System.out.println("Linux 输出图像。文件名称是:" + pe.getFileName());
}
}

Windows系统展示

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码/**
* @ClassName WinImageImpl
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public class WinImageImpl extends ImageHandler {
@Override
public void doPaint(PictureElement pe) {
System.out.println("Windows 输出图像。文件名称是:" + pe.getFileName());
}
}

编写图像文件处理类

创建一个抽象ImageInfo的类,其中包含图像显示类和图像文件转换类两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码/**
* @ClassName ImageInfo
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public abstract class ImageInfo {
protected ImageHandler imageHandler;

public void setImageHandler(ImageHandler image) {
this.imageHandler = image;
}

public abstract void parseFile(String fileName);
}

编写不同文件处理方法

JPEG文件处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala复制代码/**
* @ClassName JPEG
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public class JPEG extends ImageInfo {
@Override
public void parseFile(String fileName) {
System.out.println("将JPEG格式文件转成图像像素矩阵对象");
PictureElement pe = new PictureElement(fileName);
imageHandler.doPaint(pe);
}
}

GIF文件处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala复制代码/**
* @ClassName GIF
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public class GIF extends ImageInfo {
@Override
public void parseFile(String fileName) {
System.out.println("将GIF格式文件转成图像像素矩阵对象");
PictureElement pe = new PictureElement(fileName);
imageHandler.doPaint(pe);
}
}

BMP文件处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala复制代码/**
* @ClassName BMP
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public class BMP extends ImageInfo {
@Override
public void parseFile(String fileName) {
System.out.println("将BMP格式文件转成图像像素矩阵对象");
PictureElement pe = new PictureElement(fileName);
imageHandler.doPaint(pe);
}
}

运行

通过main运行一下简单的测试类可以看到运行结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* @ClassName MainApplication
* @Description:
* @Author 公众号:Java全栈架构师
* @Version V1.0
**/
public class MainApplication {
public static void main(String[] args) {
//在 Linux 查看GIF图片
ImageInfo imageInfoGIF = new GIF();
LinuxImageImpl linuxImage = new LinuxImageImpl();
imageInfoGIF.setImageHandler(linuxImage);
imageInfoGIF.parseFile("test.GIF");

//在 Windows 查看JPEG图片
ImageInfo imageInfoJPEG = new JPEG();
WinImageImpl winImage = new WinImageImpl();
imageInfoJPEG.setImageHandler(winImage);
imageInfoJPEG.parseFile("test.JPEG");



}
}

运行结果:

1
2
3
4
复制代码将GIF格式文件转成图像像素矩阵对象
Linux 输出图像。文件名称是:test.GIF
将JPEG格式文件转成图像像素矩阵对象
Windows 输出图像。文件名称是:test.JPEG

图片.png

  好了,一个简单的桥接模式就介绍完成了,学习设计模式的时候,尽量结合熟悉的案例进行分析,这样的话就会有事半功倍的奇效,也更容易理解。大家可以尝试着在项目中使用设计模式的方式进行开发。

  感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

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

Spring 的那点事 (二) —— initPropert

发表于 2021-11-02

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

功能

prepareRefresh() 该方法为 refresh() 中的一个方法。

功能为以下几点

  • 设置容器启动时间
  • 设置 closed 和 active 两个标识位
  • 初始化系统属性资源
  • 获取 Environment 对象,校验属性值并赋值到 Environment 对象中
  • 准备监听器和事件监听器

代码

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
java复制代码package org.springframework.context.support

public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext, DisposableBean {

protected void prepareRefresh() {
// Switch to active.
// 设置容器启动的时间
this.startupDate = System.currentTimeMillis();
// 容器的关闭标志位
this.closed.set(false);
// 容器的激活标志位
this.active.set(true);

// 记录日志
if (logger.isDebugEnabled()) {
if (logger.isTraceEnabled()) {
logger.trace("Refreshing " + this);
}
else {
logger.debug("Refreshing " + getDisplayName());
}
}

// Initialize any placeholder property sources in the context environment.
// 留给子类覆盖,初始化属性资源
initPropertySources();

// Validate that all properties marked as required are resolvable:
// see ConfigurablePropertyResolver#setRequiredProperties
// 创建并获取环境对象,验证需要的属性文件是否都已经放入环境中
getEnvironment().validateRequiredProperties();

// Store pre-refresh ApplicationListeners...
// 判断刷新前的应用程序监听器集合是否为空,如果为空,则将监听器添加到此集合中
if (this.earlyApplicationListeners == null) {
this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
}
else {
// Reset local application listeners to pre-refresh state.
// 如果不等于空,则清空集合元素对象
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}

// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
// 创建刷新前的监听事件集合
this.earlyApplicationEvents = new LinkedHashSet<>();
}

protected void initPropertySources() {
}
}

扩展 initPropertySources()

Spring的强大之处不仅仅在于它为Java开发者提供了极大便利,而且在于它的开放式架构,使得用户可以拥有最大扩展 Spring 的能力。其中,initPropertySources() 方法就是一个例子。

可以看到 prepareRefresh() 中的 initPropertySources() 方法并没有任何参数,而如果进入initPropertySources() 方法内部,也会发现该方法是空的,没有任何逻辑。

Spring之所这样设计,就是为了让用户根据自己的需要,可以重写 initPropertySources() 方法,并在方法中进行个性化的属性处理以及设置。

举例

我们可以继承此类或其子类来重写initPropertySources方法,实现一些扩展。此处我们做了两个扩展:

  • 向Environment中添加了一个属性值。
  • 我们设置了一个必要的系统属性username,当Environment中不包含username属性时系统会抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class MyClassPathXmlApplicationContext extends ClassPathXmlApplicationContext {

public MyClassPathXmlApplicationContext(String... configLocations){
super(configLocations);
}

@Override
protected void initPropertySources() {
System.out.println("扩展initPropertySource");

//这里添加了一个name属性到Environment里面,以方便我们在后面用到
getEnvironment().getSystemProperties().put("name","bobo");

//这里要求Environment中必须包含username属性,如果不包含,则抛出异常
getEnvironment().setRequiredProperties("username");
}
}

本文转载自: 掘金

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

Kotlin遇到一个任务由多个接口才能完成时,如何优雅的执行

发表于 2021-11-02

前言

不想华丽胡哨的写了,直接说问题和处理办法。

今天遇到一个需求,举个相同的例子说明,就是有两个接口A、B,A是获取某一段范围内用户的姓名列表,接口B中是根据用户的姓名查找这个用户的爱好,那么要通过ListView展示A接口+B接口中的数据,如何优雅的写?

image.png

肯定要先调用A接口吧,然后循环调用B接口?,B接口的请求回调中关联上指定的姓名,然后在设置适配器。

不同的人肯定有不同的写法,所以直接上我认为比较优雅的方法吧。

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
kotlin复制代码import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionService
import java.util.concurrent.ExecutorCompletionService
import java.util.concurrent.ExecutorService
import java.util.function.Supplier
import java.util.stream.Collector
import java.util.stream.Collectors
import java.util.stream.Stream

fun syncPost(name: String): String {
return "爱好${(0..10).random()}"
}

var map = mutableMapOf<String, String>()
fun getHobby(name: String, map: MutableMap<String, String>): CompletableFuture<Unit> {
return CompletableFuture.supplyAsync {
Thread.sleep((0..1000).random().toLong())
map[name] = syncPost(name)
}
}

fun main() {
var listName = Stream.generate { "李${(0..10).random()}" }.limit(10).collect(Collectors.toSet())
var exec = mutableListOf<CompletableFuture<Unit>>()
println(map)
listName.forEach {
exec.add(getHobby(it, map))
}
var allOf = CompletableFuture.allOf(*exec.toTypedArray()).thenAccept {
println("执行完成")
println(map)
}
allOf.get()

}

原理还是利用了CompletableFuture,首先 CompletableFuture是Future的扩展,意味着更强,并且解决了Future不能完成的事情,如本例就用到了Future所没有的功能,也就是将多个异步任务一起执行。

还有个更方便的方法,CompletableFuture提供了一个回调功能,这个回调是在所有任务都完成后被调用,也就是thenAccept方法,如果通过循环调用请求,如何判断所有的请求都执行完毕?是不是需要一个变量累加或是别的方法,但是感觉还有没有thenAccept方便吧。

同样我们可以指定超时时间,时间到达后如果还没有全部完成,则抛出TimeoutException。

1
2
3
4
5
java复制代码try {
allOf.get(500, TimeUnit.MILLISECONDS)
} catch (ep: TimeoutException) {
ep.printStackTrace()
}

另外如果在任务中异常没有捕获,那么并不会影响其他的任务,但是不会执行thenAccept,需要注意的是,如果在任务中抛出异常,并不会立马输出,而是在全部任务结束后才被真正抛出。

1
2
3
4
5
6
7
8
9
10
java复制代码fun getHobby(name: String, map: MutableMap<String, String>): CompletableFuture<Unit> {
return CompletableFuture.supplyAsync {
Thread.sleep((0..1000).random().toLong())
map[name] = syncPost(name)
if (name == "李2")
throw NullPointerException("null")
println("${Thread.currentThread().name}执行完毕")

}
}

如果某个任务失败,还需要把他加入到一个重试队列中。

本文转载自: 掘金

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

细说一下RedisTemplate的使用方法(三)

发表于 2021-11-02

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

温故才能知新(复习一下)

还记得上篇文章学习了什么知识吗?跟我读,RedisTemplate的execute方法。

开始学习,RedisTemplate系列方法之战

executePipelined(SessionCallback<?> session)

功能描述:executePipelined方法主要是提供了可批量处理的特征,还有就是当前方法的传入参数限制了会话的回调参数。

使用场景:此方法是可以在多次执行,或者是多任务频繁执行时进行使用,当然这里还要去考虑SessionCallback参数才可以。

具体代码使用:这里咱们先买个关子,接着看下一个方法,大家就能明白了。

注意事项:我在这个环节不能为大家一个非常好的建议,如果有大佬路过希望指点指点。

源码截图:源码过长,可能不能贴全,请大家移步到IDEA中自行查看RedisTemplate.java

image.png

executePipelined(SessionCallback session, @Nullable RedisSerializer resultSerializer)

功能描述:刚才我们说了executePipelined方法主要是提供了可批量处理的特征,那么这个方法的不同就是参数变得多了,如果从源码上来看,我们就能看出,其实上面的方法也是基于当前方法的。

源码:大家可以看到源码中是还是通过连接connection来指定一个session的,这也是我们传入参数的用法了。

源码截图:源码过长,可能不能贴全,请大家移步到IDEA中自行查看RedisTemplate.java

image.png

executePipelined(RedisCallback<?> action)

功能描述:同样也是批量处理的支持,但是传入的回调参数不一样,这个是Redis的回调,在Redis的回调对象里可以继续操作Redis。

具体代码使用:和上一个方法一样,需要去源码中看具体的实现才行,大家有时间去瞜一眼吧。

源码截图:源码过长,可能不能贴全,请大家移步到IDEA中自行查看RedisTemplate.java

image.png

executePipelined(RedisCallback action , @Nullable RedisSerializer resultSerializer)

功能描述:此方法是上一个方法的基础方法,内含有具体的方法实现,有兴趣的话,大家可以去读一下。

源码截图:源码过长,可能不能贴全,请大家移步到IDEA中自行查看RedisTemplate.java

image.png

小结

今天的RedisTemplate中的方法就讲到这了,大家是否学到了呢?

本文转载自: 掘金

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

1…432433434…956

开发者博客

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