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

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


  • 首页

  • 归档

  • 搜索

高频算法面试题(三十二)- 重建二叉树

发表于 2021-11-17

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

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

重建二叉树

题目来源:LeetCode-105. 从前序与中序遍历序列构造二叉树

题目描述

给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点

示例

示例 1

1.png

1
2
yaml复制代码Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]

示例 2

1
2
ini复制代码Input: preorder = [-1], inorder = [-1]
Output: [-1]

提示:

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • 3000 <= preorder[i], inorder[i] <= 3000
  • preorder 和 inorder 均无重复元素
  • inorder 均出现在 preorder
  • preorder 保证为二叉树的前序遍历序列
  • inorder 保证为二叉树的中序遍历序列

解题

解法一:递归

思路

首先我们需要知道的是,如何根据前序遍历和中序遍历来推出一棵树长什么样

根据前序遍历的结果,我们可以知道什么?

显然我们可以知道,前序遍历的第一个节点就是根节点

根据中序遍历的结果,我们可以知道什么?

我们可以知道,在中序遍历中,根节点的左边部分,都在树的左子树上。根节点的右边部分,都在树的右子树上

比如例一种给出

1
2
复制代码前序遍历:3,9,20,15,7
中序遍历:9,3,15,20,7

首先根据前序遍历,可以知道树的根节点是3

然后根据中序遍历,可以知道9在树的左子树上,15、20、7在树的右子树上,因此,首先第一步我们可以得出下边这样的树结构

2.png

左边只有一个节点,我们可以直接确定位置,右边有多个节点,因此我们需要继续重复上边的步骤。首先15、20、7这三个结点的前序遍历是20、15、7(从原来给的前序遍历结果中可以知道),中序遍历是15、20、7

所以可以知道这三个结点中,20是根节点,15是左子树,20是右子树

3.png
理解了上边,你会发现,非常适合用递归来实现。如果你不知道,我怎么知道一道题适合用递归来实现?可以看这篇总结

树的递归代码挺难想到的,有时候有思路也写不出来代码。个人感觉比较有效的办法就是多做二叉树的题,直接分类做,先把LeetCode的前300道题中的二叉树题刷一下,做的多了就有感觉了(当然,所有的题,并不是做一遍就行,要做到能不看答案就能写出bug free的代码,当然少不了勤总结)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码//构建二叉树树
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(preorder) == 0 {
return nil
}

root := TreeNode{preorder[0], nil, nil}
//找到跟结点在中序遍历结果中的位置
i := 0
for ; i < len(inorder); i++ {
if preorder[0] == inorder[i] {
break
}
}

root.Left = buildTree(preorder[1:len(inorder[:i])+1], inorder[:i]) //因为切片是左闭右开的,所以需要+1
root.Right = buildTree(preorder[len(inorder[:i])+1:], inorder[i+1:])

return &root
}

本文转载自: 掘金

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

「Linux 奏章 8」查找指令

发表于 2021-11-17

⭐find

  • find 指令将从指定目录向下递归遍历其各个子目录,将满足条件的文件/目录显示在终端。
  • 语法:find [搜索范围] [选项]
    • -name <查询方式>
    • -user <用户名>
    • -size <文件大小>
  • 案例
    • 根据名称查找 /home 目录下的 hello.txt:find /home -name hello.txt
    • 根据拥有者查找 /opt 目录下,用户名为 nobody 的文件:find /opt -user nobody
    • 根据文件大小查找整个 linux 系统下大于 200M 的文件(+n 大于、-n 小于、n 等于;单位: K、M、G):find / -size +200M

⭐locate

  • locate 命令可以快速定位文件路径,locate 命令利用实现建立的系统中所有文件名称及路径的 locate 数据库快速定位给定文件,无需遍历整个文件系统,查询速度较快。为了保证查询结果的准确度,管理员必须定期更新 locate : updatedb
  • 例:locate cfg-ens33

⭐which

查看指令的位置,例:which ls、which reboot

⭐grep 指令与管道符号 |

  • grep:过滤查找,表示将前一个命令的处理结果输出给后面的命令处理;通常搭配管道符 | 一起使用。
  • 语法:grep [选项] 查找内容 源文件
    • -n : 显示匹配行及其行号
    • -i : 忽略字母大小写

希望本文对你有所帮助🧠

欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥

本文转载自: 掘金

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

从 Jenkins 迁移到 Azure Pipelines

发表于 2021-11-17

这是我参与11月更文挑战的第8天,活动详情查看:11月更文挑战

从 Jenkins 迁移到 Azure Pipelines

Jenkins是一种开源自动化服务器,传统上由企业安装在自己的数据中心并在本地进行管理。许多提供商还提供托管 Jenkins 托管,或者,Azure Pipelines 是一个云原生持续集成管道,提供多阶段管道的管理并构建托管在云中的代理虚拟机。

jenkins 有两种写jenkinsfils的风格,声明式和脚本式,脚本式是以前使用groovy写的,为了支持pipeline,更新了版本也支持了声明式.

根据Microsoft的文档我们可以轻松地将声明式的jenkinsfile变成让ADO-pipeline能理解的yaml文件

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javascript复制代码pipeline {
   agent none
   stages {
       stage('Build') {
           steps {
               sh 'npm install'
               sh 'npm run build'
          }
      }
       stage('Test') {
           steps {
               sh 'npm test'
          }
      }
  }
}

可以换成这样的yaml文件

原先的stage->job , 执行的命令->script

1
2
3
4
5
6
7
8
yaml复制代码jobs:
- job: Build
steps:
- script: npm install
- script: npm run build
- job: Test
steps:
- script: npm test

基于容器的构建

在构建管道中使用容器允许您在 docker 镜像中构建和测试,该镜像具有管道所需的确切依赖项,并且已经配置。它使您不必包含安装更多软件或配置环境的构建步骤。Jenkins 和 Azure Pipelines 都支持基于容器的构建。

例如,要在 Ubuntu 14.04(“Trusty”)容器中运行构建,然后在 Ubuntu 16.04(“Xenial”)容器中运行测试:

Jenkinsfile

Copy

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
javascript复制代码pipeline {
   agent none
   stages {
       stage('Build') {
           agent {
               docker {
                   image 'ubuntu:trusty'
                   args '-v $HOME:/build -w /build'
              }
          }
           steps {
               sh 'make'
          }
      }
       stage('Test') {
           agent {
               docker {
                   image 'ubuntu:xenial'
                   args '-v $HOME:/build -w /build'
              }
          }
           steps {
               sh 'make test'
          }
      }
  }
}

转换为azure-pipelines.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码resources:
containers:
- container: trusty
  image: ubuntu:trusty
- container: xenial
  image: ubuntu:xenial
​
jobs:
- job: build
container: trusty
steps:
- script: make
- job: test
dependsOn: build
container: xenial
steps:
- script: make test

代理选择

jenkins 使用agent选项提供构建代理选择,以确保您的构建管道 - 或管道的特定阶段 - 在特定构建代理机器上运行。同样,Azure Pipelines 提供了许多选项来配置构建环境的运行位置。

托管代理选择

Azure Pipelines 为 Linux、Windows 和 macOS 构建提供云托管构建代理。要选择构建环境,您可以使用 vmimage 关键字。例如,要选择 macOS 版本:

1
2
yaml复制代码pool:
vmimage: macOS-latest

也指定一个container并指定一个 docker 映像,以便对构建的运行方式进行更细粒度的控制

本地代理选择

如果您在本地托管构建代理,则可以 根据机器的架构或安装在其上的软件定义 构建代理“功能”。例如,如果您已经设置了具有这些java功能的本地构建代理 ,那么您可以使用demands关键字确保您的作业在其上运行 :

YAML复制

1
2
yaml复制代码pool:
demands: java

环境变量

在 Jenkins 中,您通常为整个管道定义环境变量。例如,要设置两个环境变量,CONFIGURATION=debug 以及PLATFORM=x86:

1
2
3
4
5
6
ini复制代码pipeline {
   environment {
       CONFIGURATION = 'debug'
       PLATFORM      = 'x64'
  }
}

同样,在 Azure Pipelines 中,配置在 YAML 配置中使用并在作业执行期间设置为环境变量的变量:

1
2
3
yaml复制代码variables:
configuration: debug
platform: x64

azure-pipelines.yml

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码jobs:
- job: debug build
variables:
  configuration: debug
steps:
- script: ./build.sh $(configuration)
- job: release build
variables:
  configuration: release
steps:
- script: ./build.sh $(configuration)

成功失败时处理

Jenkins 允许您在构建完成后使用post管道部分运行命令 。您可以指定在构建成功时(使用该success部分)、构建失败时(使用该failure部分)或始终(使用该always部分)运行的命令。例如:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码post {
   always {
       echo "The build has finished"
  }
   success {
       echo "The build succeeded"
  }
   failure {
       echo "The build failed"
  }
}

同样,Azure Pipelines 具有丰富的条件执行框架,允许您根据许多条件(包括管道成功或失败)运行作业或作业步骤。

要模拟 Jenkins post-build 条件,您可以定义基于always(),succeeded()或failed()条件运行的作业:

azure-pipelines.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码jobs:
- job: always
steps:
- script: echo "The build has finished"
  condition: always()
​
- job: success
steps:
- script: echo "The build succeeded"
  condition: succeeded()
​
- job: failed
steps:
- script: echo "The build failed"
  condition: failed()

本文转载自: 掘金

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

Mybatis执行流程

发表于 2021-11-17

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

我们在之前为了方便写了一获取SqlSession的工具类,我们今天来分析一下Mybatis的执行流程

1
2
3
4
5
6
7
8
ini复制代码String resource = "mybatsi-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student studentById = mapper.findStudentById(38);
System.out.println(studentById);
sqlSession.close();

这是我们根据id查询学生信息的代码,通过代码我们发现Myabtis的执行流程是

  1. 获取Mybatis主配置文件
  2. 通过SqlSessionFactoryBuilder().build()方法传入主配置文件构建SqlSessionFactory对象
  3. SqlSessionFactory对象通过openSession()获取SqlSession对象
  4. sqlSession.getMapper()方法获取我们xml文件得到mapper对象去执行我们的sql返回对象

到底具体怎么只执行,我们来看源码看都做了些什么

Mybatis的组件

SqlSessionFactoryBuilder

通过SqlSessionFactoryBuilder这个建造者通过XMLConfigBuilder()读取传入的inputStream的Mybatis主配置文件的流,XMLConfigBuilder.parse()解析主配置文件返回一个Configuration()对象,在调用一个build方法返回一个SqlSessionFactory()对象,还有两个参数SqlSessionFactory和properties,environment 决定加载哪种环境,包括数据源和事务管理器,比如我们的项目有 开发环境,UAT测试环境,在配置文件中<environments中有多个environment。properties是来加载一些属性的。

上边结束后我们返回一个SqlSessionFactory()对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码public SqlSessionFactory build(InputStream inputStream, String SqlSessionFactory, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();

try {
inputStream.close();
} catch (IOException var13) {
}

}

return var5;
}

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

SqlSessionFactory

有了SqlSessionFactory之后我们就可以tong过他来获取SqlSession对象,SqlSessionFactory重载的多个 openSession() 方法供使用,通过参数命名我们可以知道下边的方法参数是做什么的,我们默认的openSession()方法是没有参数的,会创建包含如下属性的SqlSession对象

  1. autoCommit是否自动提交
  2. Connection创建数据库会话
  3. TransactionIsolationLevel事务隔离级别
  4. ExecutorType执行器类型

ExecutorType有三种类型

  1. SIMPLE 该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句
  2. REUSE 复用预处理语句
  3. BATCH该类型的执行器会批量执行所有更新语句
1
2
3
4
5
6
7
8
csharp复制代码public enum ExecutorType {
SIMPLE,
REUSE,
BATCH;

private ExecutorType() {
}
}

还有一个getConfiguration方法,获取Configuration对象,也就是解析xml配置文件中的<configuration标签后的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码public interface SqlSessionFactory {
SqlSession openSession();

SqlSession openSession(boolean var1);

SqlSession openSession(Connection var1);

SqlSession openSession(TransactionIsolationLevel var1);

SqlSession openSession(ExecutorType var1);

SqlSession openSession(ExecutorType var1, boolean var2);

SqlSession openSession(ExecutorType var1, TransactionIsolationLevel var2);

SqlSession openSession(ExecutorType var1, Connection var2);

Configuration getConfiguration();
}

看我啊接口我们再去看SqlSessionFactory的实现类 DefaultSqlSessionFactory()每一个openSession方法都要调用openSessionFromDataSource去返回SqlSession对象

  1. 先获取我们主配置文件的environment包函数据库配置和事务信息,
  2. 创建TransactionFactory事务工厂,调用newTransaction方法传入,数据库信息,隔离级别,是否自动提交。
  3. 新建一个构造器executor传入事务,构造器类型
  4. 返回一个 DefaultSqlSession对象

这是我们就获得一个SqlSession对象,我们的sql语句都是executor在帮我们执行,excutor是对于Statement的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;

DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}

return var8;
}

SqlSession

SqlSession是Mybatis最强大的一个类,通过DefaultSqlSession的getMapper()来生成mapper的代理对象

SqlSessionManager.java

1
2
3
typescript复制代码public <T> T getMapper(Class<T> type) {
return this.getConfiguration().getMapper(type, this);
}

Configuration.java

1
2
3
typescript复制代码public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}

MapperRegistry.java

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}

MapperProxyFactory.java 动态代理我们的接口

1
2
3
4
5
6
7
8
kotlin复制代码protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}

这一通操作之后我们就拿到了代理对象去执行我们的方法,走的方法是MapperProxy的invoik方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}

if (method.isDefault()) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}

MapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}

调用 mapperMethod.execute(this.sqlSession, args)判断是什么类型的sql,返回给sqlSession

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
kotlin复制代码public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}

if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}

我们现在看是如何实现getlist的

1
arduino复制代码<E> List<E> selectList(String var1);
1
2
3
typescript复制代码public <E> List<E> selectList(String statement) {
return this.selectList(statement, (Object)null);
}

这里调用的.executor.query方法。可以看到有两个实现

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}

return var5;
}

走CachingExecutor的query方法,先从二级缓存中获取,,没有的话走走另一个方法去一级缓存中那,没有的话
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
kotlin复制代码public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}

List list;
try {
++this.queryStack;
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
--this.queryStack;
}

if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();

while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}

this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}

return list;
}
}

没有的话走queryFromDatabase方法去数据库查数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}

this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}

return list;
}

走doquery方法有四个实现,我们看SimpleExecutor的DoQuery方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;

List var9;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}

return var9;
}

然后走StatementHandler的query方法,可以看到是statement去执行SQL语句了

1
2
3
4
5
java复制代码public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = this.boundSql.getSql();
statement.execute(sql);
return this.resultSetHandler.handleResultSets(statement);
}

本文转载自: 掘金

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

Junit 学习笔记 Junit

发表于 2021-11-17

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

Junit


一、Junit概述

  • 测试的分类:
+ 黑盒测试:封装代码,只显示输入和输出。通过比较实际的输出和预想的输出从而得到软件或者程序是否正确
+ 白盒测试:不封装代码,看过程和流程,思考代码是否高效,一般写代码。Junit测试属于白盒测试的一种。

二、 Junit的使用步骤

  • 定义一个测试类(测试用例)
+ 测试类名: 类名Test
+ 包名:xxx.xxx.xx.test 将测试类单独放在一个包之下
  • 定义一个测试方法(可以独立运行)
+ 方法名:test方法名
+ 返回值:void
+ 参数列表:空参
  • 给方法加上注解@Test
  • 【注意】:使用Junit单元测试的时候,看的是颜色
+ 红色代表出错
+ 绿色代表正常
+ 一般我们使用断言来进行结果的处理


    - Assert.assertEquals(期待值, 真实值);
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
java复制代码 package com.zhang.test;
 ​
 import com.zhang.junit.Calculator;
 import org.junit.Assert;
 import org.junit.Test;
 ​
 public class CalculatorTest {
 ​
     // 测试add方法
     @Test
     public void testAdd(){
         // 创建对象
         Calculator calculator = new Calculator();
 ​
         // 测试方法
         int result = calculator.add(1 , 2);
         // System.out.println(result);
          /**因为输出不能0检测出代码的错误(想要的因为代码逻辑无法实现)
            * 所以我们用断言Assert和真实的结果进行比对*/
 ​
          Assert.assertEquals(3 , result);   // 断言成功
 //         Assert.assertEquals(4,result);     // 断言失败
 ​
    }
 ​
     // 测试sub方法
     @Test
     public void testSub(){
         Calculator calculator = new Calculator();
         int result = calculator.sub(2 , 1);
         System.out.println(result);
    }
 }

三、Junit的其他注解

  • @Before
+ 在所有测试方法被执行之前执行,一般用来申请资源
+ 所有的测试方法都会执行
  • @After
+ 在所有测试方法被执行之后执行,一般用来释放资源
+ 所有的测试方法都会执行
+ 即使出现了异常,也不会影响该方法的执行
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
java复制代码 package com.zhang.test;
 ​
 import com.zhang.junit.Calculator;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 ​
 public class CalculatorTest {
 ​
 ​
     // 在测试方法被执行之前自动执行,一般用来申请资源
     @Before
     public void init(){
         System.out.println("init...");
    }
 ​
     // 在所有测试方法被执行完成之后都会执行该方法,一般用来释放资源
     // 即使出现了异常,也会执行该方法
     @After
     public void close(){
         System.out.println("close");
    }
 ​
 ​
 ​
     // 测试add方法
     @Test
     public void testAdd(){
         // 创建对象
         Calculator calculator = new Calculator();
 ​
         // 测试方法
         int result = calculator.add(1 , 2);
         // System.out.println(result);
          /**因为输出不能0检测出代码的错误(想要的因为代码逻辑无法实现)
            * 所以我们用断言Assert和真实的结果进行比对*/
 ​
          Assert.assertEquals(3 , result);   // 断言成功
 //         Assert.assertEquals(4,result);     // 断言失败
 ​
    }
 ​
     // 测试sub方法
     @Test
     public void testSub(){
         Calculator calculator = new Calculator();
         int result = calculator.sub(2 , 1);
         System.out.println(result);
    }
 }

本文转载自: 掘金

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

服务注册与发现 上手实践Spring Cloud Eur

发表于 2021-11-17

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

承接上一篇文章《上手实践Spring Cloud Eureka 和 Feign(二)》,上篇文章成功启动了注册中心,服务提供方,这篇文章基于Feign发现并消费服务。

三、Feign

这一章节我们从三个方面递进的讲Feign,第一如果没有Feign,我们将如何调用注册中心的服务;第二Feign是什么,能为我们带来哪些便利?第三,如何上手使用Feign。

3.1 如果没有Feign

为什么是Feign呢,如果不使用Feign我们该如何调用注册到Eureka上的服务呢?

如果不使用Feign,为了保证正常的服务发现和调用,我们必须要做到以下核心几步:

  • 第一步,使用Ribbon进行负载均衡
  • 第二步,获取服务的实例,并且获取根URL,再拼凑方法的URL
  • 第三步,最后使用 REST 模板或者其它方式来使用指定的服务

对于第一步,使用Ribbon获取服务实例,大致的代码如下。

1
2
3
4
5
6
java复制代码 @Autowired
private LoadBalancerClient loadBalancer;

pulic void method(){
ServiceInstance serviceInstance=loadBalancer.choose("producer");
}

对于第二步,大致的代码如下:

1
2
3
4
java复制代码 pulic void method(){
String baseUrl=serviceInstance.getUri().toString();
baseUrl=baseUrl+"/targetURL";
}

最后对于第三步,执行访问,并获取结果,大致的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码pulic void method(){
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response=null;
try{
response=restTemplate.exchange(baseUrl,
HttpMethod.GET, getHeaders(),String.class);
}catch (Exception ex)
{
System.out.println(ex);
}
System.out.println(response.getBody());
}

在整个调用过程中,是很复杂的,我们还需要处理一些空异常等等,我们使用Feign可以做到简化以上的步骤。

image.png

3.2 Feign介绍

Feign旨在简化HTTP API客户端,它是一个声明式Web Service客户端。Fegin是一个java调用HTTP的客户端binder,其灵感来自于Retrofit、JAXRS-2.0和WebSocket。

使用Feign能让编写Web Service客户端更加简单, 它的使用方法是定义一个接口,然后在上面添加注解,同时也支持JAX-RS标准的注解,并且Feign也支持可拔插式的编码器和解码器。

那么Feign是如何工作的呢?简单地说,Feign基于将注解转换成请求模板的方式工作,参数会简单直接的应用到模板上。具体原理不做过深的阐述。

像上面提供的未使用Feign的例子,使用Feign后其调用链图如下。

image.png

正如Eureka,Spring Cloud也提供了方便使用的OpenFeign Starter,我们将看到如何使用 Netflix Feign 使服务发现和调用变得更加容易和整洁。

3.3 上手实践

在这里,我们就使用上篇文章介绍提供的服务注册中心,和服务提供方。

首先新建一个模块,引入相关依赖。

1
2
3
4
5
6
7
8
maven复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

注意,我们因为要去发现Eureka的提供的服务,所以还是要引入Eureka Client的依赖。

接下来提供与服务方调用一模一样的接口GreetingClient。

1
2
3
4
5
6
java复制代码@FeignClient("spring-cloud-eureka-client")
public interface GreetingClient {

@GetMapping("/greeting")
String greeting();
}

所以这里就有一个问题,服务提供方和消费要声明同样一套接口,按照最佳工程实践,建议把这部分接口抽象出来作为单独Modeling,然后对应的服务提供方和消费方引入这个包。

接下来是,写启动类和Web服务,为了方便,把两者都写到启动类Launcher吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@SpringBootApplication
@EnableFeignClients
@RestController
public class Launcher {

@Autowired
private GreetingClient greetingClient;

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

@GetMapping("/get-greeting")
public String greeting(){
return greetingClient.greeting();
}
}

application.yml配置信息如下。

1
2
3
4
5
6
7
8
9
yml复制代码spring:
application:
name: spring-cloud-eureka-feign-client
server:
port: 8080
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}

然后启动这个消费服务,观察Eureka的管理页面,可以看到这个消费方也注册到Eureka注册中心了。

screenshot-127-0-0-1-8761-1636963294276.png

然后访问http://{yourhost}:8080/get-greeting,即得到hello...的回复,代表调用链通畅。

在这个例子中,并没有引入负载均衡,如果想要做到,引入Ribbon的依赖后,在代码调用服务的地方,正常去调用远程服务即可。

1
2
3
4
5
ini复制代码    @Autowired
private RemoteCallService loadBalancer;

#服务方法中这样使用去调用远程方法去获取数据
data tmp = loadBalancer.getData();

四、总结

相较而言,这一整篇文章还是比较简单,上手实践一点都不难,因为整个系列都是以入门教程为主,让读者认识到这些分布式组件为什么要提供,我们能用它们分别做什么。服务注册中心还有很多,比如Zuul、ZK等等,接下来的教程会一一讲到,敬请期待!


少年,没看够?点击石头的主页,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

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

Python爬虫从入门到精通(四)提取网页中的信息 一、数据

发表于 2021-11-17

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

一、数据的类型

网页中数据的类型简单来说可以分成以下三类:

1、结构化数据

可以用统一的结构加以表示的数据。可以使用关系型数据库表示和存储,表现为二维形式的数据。一般特点是:数据以行为单位,一行数据表示一个实体的信息,每一行数据的属性是相同的。

比如MySQL数据库表中的数据:

id name age gender

aid1 马化腾 46 male

aid2 马云 53 male

aid3 李彦宏 49 male

2、半结构化数据

是结构化数据的一种形式,并不符合关系型数据库或其他数据表的形式关联起来的数据模型结构,但包含相关标记,用来分隔语义元素以及对记录和字段进行分层。因此,它也被称为自描述的结构。常见的半结构数据有HTML,XML和JSON等,实际上是以树或者图的结构来存储的。

比如,一个简单的XML表示:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<person>

    <name>A</name>

<age>13</age>

<class>aid1710</class>

    <gender>female</gender>

</person>

或者

1
2
3
4
5
6
7
xml复制代码<person>

    <name>B</name>

    <gender>male</gender>

</person>

结点中属性的顺序是不重要的,不同的半结构化数据的属性的个数是不一定一样的。这样的数据格式,可以自由地表达很多有用的信息,包括自描述信息(元数据)。所以,半结构化数据的扩展性很好,特别适合于在互联网中大规模传播。

3、非结构化数据

就是没有固定结构的数据。各种文档、图片、视频/音频等都属于非结构化数据。对于这类数据,我们一般直接整体进行存储,而且一般存储为二进制的数据格式;

除了结构化和半结构数据之外的数据都是非结构化数据。

二、关于XML,HTML,DOM和JSON文件

1、XML, HTML, DOM

XML即Extentsible Markup Language(可扩展标记语言),是用来定义其它语言的一种元语言,其前身是SGML(标准通用标记语言)。它没有标签集(tagset),也没有语法规则(grammatical rule),但是它有句法规则(syntax rule)。任何XML文档对任何类型的应用以及正确的解析都必须是良构的(well-formed),即每一个打开的标签都必须有匹配的结束标签,不得含有次序颠倒的标签,并且在语句构成上应符合技术规范的要求。XML文档可以是有效的(valid),但并非一定要求有效。所谓有效文档是指其符合其文档类型定义(DTD)的文档。如果一个文档符合一个模式(schema)的规定,那么这个文档是模式有效的(schema valid)。

HTML(Hyper Text Mark-up Language)即超文本标记语言,是WWW的描述语言。HTML与XML的区别与联系:

XML和HTML都是用于操作数据或数据结构,在结构上大致是相同的,但它们在本质上却存在着明显的区别。综合网上的各种资料总结如下。

(一)语法要求不同:

  1. 在HTML中不区分大小写,在XML中严格区分。
  2. 在HTML中,有时不严格,如果上下文清楚地显示出段落或者列表键在何处结尾,那么你可以省略

或者
之类的结束标记。在XML中,是严格的树状结构,绝对不能省略掉结束标记。5. 在XML中,拥有单个标记而没有匹配的结束标记的元素必须用一个/ 字符作为结尾。这样分析器就知道不用查找结束标记了。
6. 在XML中,属性值必须分装在引号中。在HTML中,引号是可用可不用的。
7. 在HTML中,可以拥有不带值的属性名。在XML中,所有的属性都必须带有相应的值。
8. 在XML文档中,空白部分不会被解析器自动删除; 但是html是过滤掉空格的。

XML的语法要求比HTML严格。

(二)标记不同:

  1. HTML使用固有的标记; 而XML没有固有的标记。
  2. HTML标签是预定义的; XML标签是免费的、自定义的、可扩展的。

(三)作用不同:

  1. HTML是用来显示数据的; XML是用来描述数据、存放数据的,所以可以作为持久化的介质。HTML将数据和显示结合在一起,在页面中把这数据显示出来;xml则将数据和显示分开。 XML被设计用来描述数据,其焦点是数据的内容。HTML被设计用来显示数据,其焦点是数据的外观。
  2. XML不是HTML的替代品,XML和HTML是两种不同用途的语言。 XML 不是要替换 HTML;实际上XML 可以视作对 HTML 的补充。XML 和HTML 的目标不同HTML 的设计目标是显示数据并集中于数据外观,而XML的设计目标是描述数据并集中于数据的内容。
  3. 没有任何行为的XML, 与HTML 相似, XML不进行任何操作(共同点)。
  4. 对于XML最好的形容可能是: XML是一种跨平台的,与软、硬件无关的,处理与传输信息的工具。
  5. XML未来将会无所不在,XML将成为最普遍的数据处理和数据传输的工具。

关于DOM:

文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标志语言的标准编程接口。在网页上,组织页面(或文档)的对象被组织在一个树形结构中,用来表示文档中对象的标准模型就称为DOM。Document Object Model的历史可以追溯至1990年代后期微软与Netscape的“浏览器大战”,双方为了在JavaScript与JScript一决生死,于是大规模的赋予浏览器强大的功能。微软在网页技术上加入了不少专属事物,既有VBScript、ActiveX、以及微软自家的DHTML格式等,使不少网页使用非微软平台及浏览器无法正常显示。DOM即是当时蕴酿出来的杰作。
​

DOM= Document Object Model,文档对象模型,DOM可以以一种独立于平台和语言的方式访问和修改一个文档的内容和结构。换句话说,这是表示和处理一个HTML或XML文档的常用方法。DOM很重要,DOM的设计是以对象管理组织(OMG)的规约为基础的,因此可以用于任何编程语言。最初人们把它认为是一种让JavaScript在浏览器间可移植的方法,不过DOM的应用已经远远超出这个范围。DOM技术使得用户页面可以动态地变化,如可以动态地显示或隐藏一个元素,改变它们的属性,增加一个元素等,DOM技术使得页面的交互性大大地增强。

DOM实际上是以面向对象方式描述的文档模型。DOM定义了表示和修改文档所需的对象、这些对象的行为和属性以及这些对象之间的关系。可以把DOM认为是页面上数据和结构的一个树形表示,不过页面当然可能并不是以这种树的方式具体实现。

)​

通过 JavaScript,您可以重构整个 HTML 文档。您可以添加、移除、改变或重排页面上的项目。要改变页面的某个东西,JavaScript 就需要获得对 HTML 文档中所有元素进行访问的入口。这个入口,连同对 HTML 元素进行添加、移动、改变或移除的方法和属性,都是通过文档对象模型来获得的(DOM)。

2、JSON文件

JSON(JavaScript Object Notation, JS对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的JS规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

JSON 语法规则:

在JS 语言中,一切都是对象。因此,任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。

但是对象和数组是比较特殊且常用的两种类型:

 1.对象表示为键值对


2.数据由逗号分隔


3.花括号保存对象


4.方括号保存数组

JSON键值对是用来保存 JS 对象的一种方式,和 JS 对象的写法也大同小异,

键/值对组合中的键名写在前面并用双引号 “” 包裹,使用冒号 : 分隔,然后紧接着值

{“firstName”: “Json”,”class”:”aid1710”}

这很容易理解,等价于这条 JavaScript 语句:

{firstName : “Json”,”class”:”aid1710”}

JSON与JS对象的关系:

很多人搞不清楚 JSON 和JS对象的关系,甚至连谁是谁都不清楚。其实,可以这么理解:JSON是 JS对象的字符串表示法,它使用文本表示一个 JS 对象的信息,本质是一个字符串。

如:

1
2
3
css复制代码var obj = {a: 'Hello', b: 'World'}; //这是一个对象,注意键名也是可以使用引号包裹的

var json = '{"a": "Hello", "b": "World"}'; //这是一个 JSON 字符串,本质是一个字符串。

Python中关于JSON的操作简单演示:

代码示例见 josnTest.py

JSON和XML的比较:

1.可读性:

JSON和XML的可读性可谓不相上下,一边是简易的语法,一边是规范的标签形式,很难分出胜负。

2.可扩展性:

XML天生有很好的扩展性,JSON当然也有,没有什么是XML可以扩展而JSON却不能扩展的。不过JSON在Javascript主场作战,可以存储Javascript复合对象,有着xml不可比拟的优势。

3.编码难度:

XML有丰富的编码工具,比如Dom4j、JDom等,JSON也有提供的工具。无工具的情况下,相信熟练的开发人员一样能很快的写出想要的xml文档和JSON字符串,不过,xml文档要多很多结构上的字符。
​

4.解码难度

XML的解析方式有两种:

一是通过文档模型解析,也就是通过父标签索引出一组标记。例如:xmlData.getElementsByTagName(“tagName”),但是这样是要在预先知道文档结构的情况下使用,无法进行通用的封装。

另外一种方法是遍历节点(document 以及 childNodes)。这个可以通过递归来实现,不过解析出来的数据仍旧是形式各异,往往也不能满足预先的要求。凡是这样可扩展的结构数据解析起来一定都很困难。JSON也同样如此。如果预先知道JSON结构的情况下,使用JSON进行数据传递简直是太美妙了,可以写出很实用美观可读性强的代码。

如果你是纯粹的前台开发人员,一定会非常喜欢JSON。但是如果你是一个应用开发人员,就不是那么喜欢了,毕竟xml才是真正的结构化标记语言,用于进行数据传递。而如果不知道JSON的结构而去解析JSON的话,那简直是噩梦。费时费力不说,代码也会变得冗余拖沓,得到的结果也不尽人意。

但是这样也不影响众多前台开发人员选择JSON。因为json.js中的toJSONString()就可以看到JSON的字符串结构。当然不是使用这个字符串,这样仍旧是噩梦。常用JSON的人看到这个字符串之后,就对JSON的结构很明了了,就更容易的操作JSON。以上是在Javascript中仅对于数据传递的xml与JSON的解析。

在Javascript地盘内,JSON毕竟是主场作战,其优势当然要远远优越于xml。

如果JSON中存储Javascript复合对象,而且不知道其结构的话,相信很多程序员也一样是哭着解析JSON的。除了上述之外,JSON和XML还有另外一个很大的区别在于有效数据率。JSON作为数据包格式传输的时候具有更高的效率,这是因为JSON不像XML那样需要有严格的闭合标签,这就让有效数据量与总数据包比大大提升,从而减少同等数据流量的情况下,网络的传输压力。

实例比较:

XML和JSON都使用结构化方法来标记数据,下面来做一个简单的比较。

用XML表示中国部分省市数据如下:

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
xml复制代码<?xml version="1.0" encoding="utf-8"?>

<country>

    <name>中国</name>

    <province>

        <name>黑龙江</name>

        <cities>

            <city>哈尔滨</city>

            <city>大庆</city>

        </cities>

    </province>

    <province>

        <name>广东</name>

        <cities>

            <city>广州</city>

            <city>深圳</city>

            <city>珠海</city>

        </cities>

    </province>

    <province>

        <name>台湾</name>

        <cities>

            <city>台北</city>

            <city>高雄</city>

        </cities>

    </province>

    <province>

        <name>新疆</name>

        <cities>

            <city>乌鲁木齐</city>

        </cities>

    </province>

</country>

用JSON表示如下:

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

    "name": "中国",

    "province": [{

        "name": "黑龙江",

        "cities": {

            "city": ["哈尔滨", "大庆"]

        }

    }, {

        "name": "广东",

        "cities": {

            "city": ["广州", "深圳", "珠海"]

        }

    }, {

        "name": "台湾",

        "cities": {

            "city": ["台北", "高雄"]

        }

    }, {

        "name": "新疆",

        "cities": {

            "city": ["乌鲁木齐"]

        }

    }]

}

可以看到:JSON简单的语法格式和清晰的层次结构明显要比XML容易阅读,并且在数据交换方面,由于JSON所使用的字符要比XML少得多,可以大大得节约传输数据所占用得带宽。

三、 怎么提取网页中的信息

1、 XPath与lxml

XPath是一门在XML文档中查找信息的语言,对XPath的理解是很多高级XML应用的基础,XPath在XML中通过元素和属性进行导航。

lxml是一个用来处理XML的第三方 Python 库,它在底层封装了用 C 语言编写的 libxml2和libxslt,并以简单强大的Python API,兼容并加强了著名的Element Tree API。

安装:pip install lxml

使用:from lxml import etree
​

  1. XPath术语:

在XPath语境中,XML 文档被视作节点树,节点树的根节点也被称作文档节点。 XPath 将节点树中的节点(Node)分为七类:元素(Element),属性(Attribute),文本(Text),命名空间(Namespace),处理指令(Processing-instruction),注释(Comment)和文档节点(Document nodes)。

看一下 XML 文档例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="ISO-8859-1"?>

<bookstore>

<book>

  <title lang="en">Harry Potter</title>

  <author>J K. Rowling</author>

  <year>2005</year>

  <price>29.99</price>

</book>

</bookstore>

以上的XML文档中:

(这是一个“根”)


J K. Rowling (这是一个“元素”)


lang="en" (这是一个“属性”)

从另一个视角来看它:

bookstore (根)

book (元素)

title (元素)

    lang  = en                   (属性)


    text = Harry Potter          (文本)


    author                       (元素)


    text = J K. Rowling          (文本)


    year                         (元素)


    text = 2005                  (文本)


    price                        (元素)


text = 29.99                 (文本)
  1. 节点之间的关系

父(Parent):每个元素都肯定有一个父节点,最顶层的元素父亲是根节点。同理每个属性必然有一个父,它们的父是元素。 上例XML文档中,根bookstore是元素 book 的父节点,book是元素title, author, year, price 的父节点,title是lang 的父节点。

子(Children):元素可以有零或多个子。上例XML文档中,title, author, year, price是book的子节点。

同胞(Sibling):父节点相同的节点之间互为同胞,也称彼此的兄弟节点。上例XM文档中,title, author, year, price 彼此互为同胞。

先辈(Ancestor):某节点的父节点、父的父,以此类推一直追溯至根节点之间所有节点。上例XM文档中,title, author, year, price 的先辈就是 book, bookstore。

后代(Descendant):某节点的子节点、子的子,以此类推至最后一个子节点之间所有节点。上例XM文档中,bookstore 的后代就是 title, author, year, price 。

3.选取节点

以下为基本路径的表达方式,记住XPath的路径表达式都是基于某个节点之上的,例如最初的当前节点一般是根节点,这与Linux下路径切换原理是一样的。

表达式描述:

nodename 选取已匹配节点下名为 nodename 的子元素节点

/ 如果以 / 开头,表示从根节点作为选取起点。

// 在已匹配节点后代中选取节点,不考虑目标节点的位置。

. 选取当前节点。

.. 选取当前节点的父元素节点。

@ 选取属性。

4.通配符

* 匹配任何元素。

@* 匹配任何属性。

node() 匹配任何类型的节点。

5.预判(Predicates)或 条件选取

预判是用来查找某个特定的节点或者符合某种条件的节点,预判表达式位于方括号中。使用 “|” 运算符,你可以选取符合“或”条件的若干路径。

具体例子见下面代码lxmlTest.py。

6.坐标轴

XPath 坐标轴:坐标轴用于定义当对当前节点的节点集合。

坐标轴名称 含义

ancestor 选取当前节点的所有先辈元素及根节点。

ancestor-or-self 选取当前节点的所有先辈以及当前节点本身。

attibute 选取当前节点的所有属性。

child 选取当前节点的所有子元素。

descendant 选取当前节点的所有后代元素。

descendant-or-self 选取当前节点的所有后代元素以及当前节点本身。

following 选取文档中当前节点的结束标签之后的所有节点。

following-sibling 选取当前节点之后的所有同级节点。

namespace 选取当前节点的所有命名空间节点。

parent 选取当前节点的父节点。

preceding 选取当前节点的开始标签之前的所有节点。

preceding-sibling 选取当前节点之前的所有同级节点。

self 选取当前节点。

7.位置路径的表达式

位置路径可以是绝对路径,也可以是相对路径。绝对路径以 “/” 开头。每条路径包括一个或多个步,每步之间以“/”分隔。

绝对路径:/step/step/…


相对路径:step/step/…

每步根据当前节点集合中的节点计算。

步(step)包括三部分:

坐标轴(axis):      定义所选节点与当前节点之间的关系。


节点测试(node-test):识别某个坐标轴内部的节点。


预判(predicate):    提出预判条件对节点集合进行筛选。

步的语法:坐标轴::节点测试[预判]

2、 BeautifulSoup4

Beautiful Soup是用Python写的一个HTML/XML的解析器,它可以很好的处理不规范标记并生成剖析树(parse tree)。 它提供简单又常用的导航(navigating),搜索以及修改剖析树的操作。它可以大大节省你的编程时间。

安装:(sudo) pip install beautifuilsoup4

使用:
​

在程序中导入 Beautiful Soup库:

1
2
3
4
5
python复制代码from BeautifulSoup import BeautifulSoup          # For processing HTML

from BeautifulSoup import BeautifulStoneSoup     # For processing XML

import BeautifulSoup                             # To get everything

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码
# 代码例子

from bs4 import BeautifulSoup

import re

doc = ['<html><head><title>Page title</title></head>',

       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',

       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',

       '</html>']

soup = BeautifulSoup(''.join(doc))

print soup.prettify()

定位某些 soup元素很简单,比如上例:

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
shell复制代码soup.contents[0].name

# u'html'



soup.contents[0].contents[0].name

# u'head'



head = soup.contents[0].contents[0]

head.parent.name

# u'html'



head.next

# <title>Page title</title>



head.nextSibling.name

# u'body'



head.nextSibling.contents[0]

# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>



head.nextSibling.contents[0].nextSibling

# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

也可以利用soup,获得特定标签或有着特定属性的标签,修改soup也很简单;

BS4 与 lxml的比较:

lxml C实现,只会局部遍历,快; 复杂,语法不太友好;

BS4 Python实现,会加载整个文档,慢; 简单,API人性化;

3、正则表达式re

被用来检索\替换那些符合某个模式(规则)的文本,对于文本过滤或规则匹配,最强大的就是正则表达式,是python爬虫里必不可少的神兵利器。

基本匹配规则:

[0-9] 任意一个数字,等价\d

[a-z] 任意一个小写字母

[A-Z]任意一个大写字母

[^0-9] 匹配非数字,等价\D

\w 等价[a-z0-9_],字母数字下划线

\W 等价对\w取非

. 任意字符

[] 匹配内部任意字符或子表达式

[^] 对字符集合取非

  • 匹配前面的字符或者子表达式0次或多次
  • 匹配前一个字符至少1次

? 匹配前一个字符0次或多次

^ 匹配字符串开头

$ 匹配字符串结束

Python使用正则表达式

Python的re模块

pattern 编译好的正则表达式

几个重要的方法:

match: 匹配一次从开头;

search: 匹配一次,从某位置;

findall: 匹配所有;

split: 分隔;

sub: 替换;

需要注意的两种模式:

贪婪模式:(.*)

懒惰模式:(.*?)

)​

)​

  1. 用正则表达式实现下面的效果:

把 i=d%0A&from=AUTO&to=AUTO&smartresult=dict

转换成下面的形式:

i:d%0A

from:AUTO

to:AUTO

smartresult:dict

总结:正则,BS,lxml的比较

)

本文转载自: 掘金

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

HTTP 长连接和短连接

发表于 2021-11-17

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

你好,我是看山。

一直听别人说 HTTP 长连接,只知道长连接比短连接更节省资源、更快捷,但是并不真的知道原因。知其然不知其所以然,对于技术来说,这种状态是比较危险的。所以,还是要挖一下原理,即使挖的比较浅,也要迈出这一步。

HTTP 是应用层协议,传输层使用的是 TCP 协议,网络层使用的是 IP 协议。

IP 协议主要解决网络路由和寻址问题,TCP 协议主要解决如何在 IP 层之上可靠的传递数据包,使在网络上的另一端收到发送端发出的所有包,并且顺序与发出顺序一致,HTTP 协议主要基于 TCP 协议完成数据传递。

图片

首先说下 TCP 连接与断开。

TCP 的生命周期被戏称为三次握手和四次挥手,每次握手(挥手)都需要通信双方交互数据,所以 TCP 连接传输数据是比较耗费资源的,也就是通常所说的成本较高。

图片

TCP 的三次握手和四次挥手

然后,HTTP 协议是无状态的、面向连接的,即协议本身不具备记忆能力,两次不同的 HTTP 请求之间没有任何联系。

在 HTTP/1.1 之前,一个网页加载资源的时候,每需要加载一个资源,就需要进行一次 HTTP 请求,建立一次连接,请求结束就断开连接,而每次连接(TCP 连接)都需要耗费资源(时间资源、网络资源等)。

所以后来在 HTTP/1.1 中引入持久连接(persistent connection),即 TCP 连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive,也就是常说的长连接。

这里所说的长连接,其实本身是 TCP 长连接。比如发起一次 HTTP 请求时,客户端与服务端创建 TCP 连接,在得到响应结果后,不进行 TCP 的四次挥手断开连接,而是会保持一段时间的 TCP 连接。此时,如果又有一次 HTTP 请求相同的服务端,就会继续使用这一个 TCP 连接。这样,节省了 TCP 连接的消耗。

当然,为了资源的有效利用,在一段时间(超时时间,请求头中Keep-alive: timeout=10进行设置)没有活动时,客户端或服务端会主动断开 TCP 连接。建议的做法是,在客户端最后一次请求时,请求头中发送Connection: close,明确要求关闭 TCP 连接。

补充下 TCP 三次握手、四次挥手的知识。

三次握手建立连接:

  1. 第一次握手:客户端发送 SYN 包 (seq=x) 到服务器,并进入 SYN_SEND 状态,等待服务器确认;
  2. 第二次握手:服务器收到 SYN 包,必须确认客户的 SYN(ACK=x+1),同时自己也发送一个 SYN 包(seq=y),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
  3. 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ACK=y+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。

四次挥手断开连接

  1. 第一次挥手:主动关闭方发送一个 FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当然,在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 确认报文,主动关闭方依然会重发这些数据),但此时主动关闭方还可以接受数据。
  2. 第二次挥手:被动关闭方收到 FIN 包后,发送一个 ACK 给对方,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号)。
  3. 第三次挥手:被动关闭方发送一个 FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
  4. 第四次挥手:主动关闭方收到 FIN 后,发送一个 ACK 给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。

注意:中断连接端可以是 Client 端,也可以是 Server 端。

详细的状态说明

  • SYN_SEND:客户端尝试链接服务端,通过 open 方法。也就是 TCP 三次握手中的第 1 步之后,注意是客户端状态
  • SYN_RECEIVED:服务接受创建请求的 SYN 后,也就是 TCP 三次握手中的第 2 步,发送 ACK 数据包之前。注意是服务端状态,一般 15 个左右正常,如果很大,怀疑遭受 SYN_FLOOD 攻击
  • ESTABLISHED:客户端接受到服务端的 ACK 包后的状态,服务端在发出 ACK 在一定时间后即为 ESTABLISHED
  • FIN_WAIT1:主动关闭的一方,在发出 FIN 请求之后,也就是在 TCP 四次挥手的第 1 步
  • CLOSE_WAIT:被动关闭的一方,在接受到客户端的 FIN 后,也就是在 TCP 四次挥手的第 2 步
  • FIN_WAIT2:主动关闭的一方,在接受到被动关闭一方的 ACK 后,也就是 TCP 四次挥手的第 2 步。可以设定被动关闭方返回 FIN 后的超时时间,有效回收链接,避免 syn-flood.
  • LASK_ACK:被动关闭的一方,在发送 ACK 后一段时间后(确保客户端已收到),再发起一个 FIN 请求。也就是 TCP 四次挥手的第 3 步
  • TIME_WAIT:主动关闭的一方,在收到被动关闭的 FIN 包后,发送 ACK。也就是 TCP 四次挥手的第 4 步

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

Python爬虫从入门到精通(三)简单爬虫的实现 一、可能是

发表于 2021-11-17

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

一、可能是史上最简单的爬虫Demo

最简单的爬虫Demo :

第一个爬虫程序,两行代码写一个爬虫:

1
2
3
4
5
6
less复制代码​
import urllib  #Python3

print(urllib.request.urlopen(urllib.request.Request("GitHub - richardpenman/wswp_places")).read() )

​

这两行代码在Python3.6 下可以正常运行,获取example.webscraping.com****

这个页面的内容;

备注:如果是Python3 , 则使用如下两行代码:

1
2
3
arduino复制代码import requests #Python3

print(requests.get('http://example.webscraping.com').text)

如果没有requests 库,则需要使用命令pip install requests 安装一下;

说明:本讲义目前大部分代码以Python3.6 的代码位蓝本,讲义的附录A 中会将Python2 和Python3 在爬虫这块最主要几个库的对照表收录进来,按照这张表就可以方便的实现Python2 与Python3在爬虫这块代码的移植。

二、回顾一下HTTP ,HTTPS 协议

1、关于URL:

URL (Uniform / Universal Resource Locator 的缩写):统一资源定位符,是用于完整地描述Internet 上网页和其他资源的地址的一种标识方法。

基本格式:scheme://host[:port#]/path/ …/[?query-string][#anchor]

scheme :协议( 例如:http, https, ftp)

host :服务器的IP 地址或者域名

port# :服务器的端口(如果是走协议默认端口,缺省端口80 )

path :访问资源的路径

query-string :参数,发送给http 服务器的数据

anchor :锚(跳转到网页的指定锚点位置)

例如:

www.baidu.com

ftp://192.168.1.118:8081/index

URL 是爬虫的入口,非常的重要。****

2、HTTP协议,HTTPS 协议

HTTP 协议(HyperText Transfer Protocol ,超文本传输协议):是一种发布和接收 HTML 页面的方法。HTTP 协议是一个应用层的协议,无连接(每次连接只处理一个请求),无状态(每次连接,传输都是独立的)

HTTPS (Hypertext Transfer Protocol over Secure Socket Layer )协议简单讲是HTTP 的安全版,在HTTP 下加入SSL 层。HTTPS = HTTP+SSL (Secure Sockets Layer 安全套接层)主要用于Web 的安全传输协议,在传输层对网络连接进行加密,保障在Internet 上数据传输的安全

HTTP 的端口号为80 ;HTTPS 的端口号为443 ;

3、HTTP Request 请求常用的两种方法:

Get :是为了从服务器上获取信息,传输给服务器的数据的过程不够安全,数据大小有限制;

Post :向服务器传递数据,传输数据的过程是安全的,大小理论上没有限制;

​4、**关于User-Agent**

User Agent 中文名为用户代理 ,简称 UA ,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、 浏览器及版本、浏览器渲染引擎、浏览器语言、 浏览器插件等。

)​

我们来看下我们最简单的爬虫跑起来时告诉服务器的User-Agent 是什么?
通过这个例子,我们发现Python 爬虫有个默认的带有版本号的User-Agent ,由此很容易能识别出来这是一个Python 写的爬虫程序。所以如果用默认的User-Agent ,那些反爬虫的程序一眼就能识别出来我们是个Python 爬虫,这对Python 爬虫是不利的。

那么,我们如何修改这个User-Agent ,来伪装我们的爬虫程序呢?

1
2
3
4
5
6
7
8
9
10
11
ini复制代码# Http协议中请求头的信息

headers={"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"}

req = request.Request("http://www.sina.com.cn",

                      headers=headers)

# 返回http.client.HTTPResponse

response = request.urlopen(req)

5、HTTP Response 响应的状态码:

200 为成功,300 是跳转;

400 ,500 意味着有错误:

)​

说明:服务器返回给爬虫的信息可以用来判断我们爬虫当前是否正常在运行;

当出现异常错误时:一般来说如果是500 的错误那么爬虫会进入休眠状态,说明服务器已经宕机;如果是400的错误,则需要考虑爬虫的抓取策略的修改,可能是网站更新了,或者是爬虫被禁了。如果在一个分布式的爬虫系统中,更容易发现和调整爬虫的策略。
​

6、HTTP 响应体是我们爬虫需要关心的协议部分的内容:

)​

通过Python 的交互是环境,我们可以直观的方便的看到请求响应的信息,这也看出了Python 瑞士军刀般的作用。

>>> import requests #Python3

>>> html = requests.get(‘example.webscraping.com‘)

>>> print(html.status_code)

200

>>> print(html.elapsed)

0:00:00.818880

>>> print(html.encoding)

utf-8

>>> print(html.headers)

{‘Server’: ‘nginx’, ‘Date’: ‘Thu, 01 Feb 2018 09:23:30 GMT’, ‘Content-Type’: ‘text/html; charset=utf-8’, ‘Transfer-Encoding’: ‘chunked’, ‘Connection’: ‘keep-alive’, ‘Vary’: ‘Accept-Encoding’, ‘X-Powered-By’: ‘web2py’, ‘Set-Cookie’: ‘session_id_places=True; httponly; Path=/, session_data_places=”6853de2931bf0e3a629e019a5c352fca:1Ekg3FlJ7obeqV0rcDDmjBm3y4P4ykVgQojt-qrS33TLNlpfFzO2OuXnY4nyl5sDvdq7p78_wiPyNNUPSdT2ApePNAQdS4pr-gvGc0VvnXo3TazWF8EPT7DXoXIgHLJbcXoHpfleGTwrWJaHq1WuUk4yjHzYtpOhAbnrdBF9_Hw0OFm6-aDK_J25J_asQ0f7”; Path=/‘, ‘Expires’: ‘Thu, 01 Feb 2018 09:23:30 GMT’, ‘Pragma’: ‘no-cache’, ‘Cache-Control’: ‘no-store, no-cache, must-revalidate, post-check=0, pre-check=0’, ‘Content-Encoding’: ‘gzip’}

>>> print(html.content)

# 略,内容太多了

三、关于爬虫抓取的策略

一般在抓取爬虫数据时,我们不会只抓取一个入口的URL 数据就停止了。当有多个URL链接需要抓取时,我们怎么办?

​

1、深度优先算法

深度优先是指搜索引擎先从网站页面上的某个链接进行抓取,进入到这个链接的页面之后,抓取页面上的内容,然后继续顺着当前页面上的这个链接进行抓取下去,直到顺着这个页面上的链接全部抓取完,最深的页面上没有链接了,爬虫再回过头来顺着第一个网站页面上的另外一个链接进行抓取;如下图所示。

)

2、广度/宽度优先算法

广度优先则是另一个过程,它先把该层次的都遍历完,再继续往下走。

如下图所示:

)​

练习: 构造一个完全二叉树,实现其深度优先和广度优先遍历算法。

一棵二叉树至多只有最下面的一层上的结点的度数可以小2 ,并且最下层上的结点都集中在该层最左边的若干位置上,而在最后一层上,右边的若干结点缺失的二叉树,则此二叉树成为完全二叉树。

​

\

完全二叉树如下:

)​

深度优先遍历的结果:[1, 3, 5, 7, 9, 4, 12, 11, 2, 6, 14, 13, 8, 10]

广度优先遍历的结果:[1, 3, 2, 5, 4, 6, 8, 7, 9, 12, 11, 14, 13, 10]

3、 实践中怎么来组合抓取策略

1.一般来说,重要的网页距离入口站点的距离很近;

2.宽度优先有利于多爬虫并行进行合作;

3. 可以考虑将深度与广度相结合的方式来实现抓取的策略:优先考虑广度优先,

对深度进行限制最大深度;

总结:一个通用爬虫的流程如下:

****)

本文转载自: 掘金

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

二叉树顺序结构及实现

发表于 2021-11-17

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

💦 二叉树的顺序结构

1
2
3
scss复制代码普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。
而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆 (一种二叉树) 使用顺序结构的数组来存储。
需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

  ❓ 操作系统和数据结构这两门学科中都有栈和堆的概念,如何区分 ❔

在这里插入图片描述

💦 堆的概念及结构

如果有一个关键码的集合K = {k₀, k₁, k₃,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Ki <= K2*i+1 且 Ki <= K2*i+2 (Ki >= K2*i+1 且 Ki >= K2*i+2) i = 0,1,2…,则称为小堆 (或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

    ❗ 堆的性质 ❕

      ▶ 堆中某个节点的值总是不大于或不小于其父节点的值;

      ▶ 堆总是一棵完全二叉树;

    —————————————Cut—————————————–

    ❗ 大(根)堆和小(根)堆 ❕

      ▶ 大(根)堆,树中所有父亲都大于或者等于孩子,且大堆的根是最大值;

      ▶ 小(根)堆,树中所有父亲都小于或者等于孩子,且小堆的根是最小值;

在这里插入图片描述

💦 堆的概念选择题

1、下列关键字序列为堆的是( )

A. 100, 60, 70, 50, 32, 65

B. 60, 70, 65, 50, 32, 100

C. 65, 100, 70, 32, 50, 60

D. 70, 65, 100, 32, 50, 60

E. 32, 50, 100, 70, 65, 60

F. 50, 100, 70, 65, 60, 32

📝 分析:根据堆的概念分析,A 选项为大根堆;


2、注,请理解下面堆应用的知识再做。已知小根堆为 8, 15, 10, 21, 34, 16, 12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是( )

A. 1

B. 2

C. 3

D. 4

📝 分析:

此题考查的是建堆的过程
在这里插入图片描述

所以选择 C 选项


3、注,请理解下面堆应用的知识再做。一组记录排序码为 (5 11 7 2 3 17),则利用堆排序方法建立的初始堆为( )

A. (11 5 7 2 3 17)

B. (11 5 7 2 17 3)

C. (17 11 7 2 3 5)

D. (17 11 7 5 3 2)

E. (17 7 11 3 5 2)

F. (17 7 11 3 2 5)

📝 分析:

此题考查的是堆排序建堆的过程

根据下面堆排序的过程分析,选择 C 选项

在这里插入图片描述


4、、注,请理解下面堆应用的知识再做。最小堆 [0, 3, 2, 5, 7, 4, 6, 8],在删除堆顶元素0之后,其结果是( )

A. [3,2,5,7,4,6,8]

B. [2,3,5,7,4,6,8]

C. [2,3,4,5,7,8,6]

D. [2,3,4,5,6,7,8]

📝 分析:

此题考查的是 Pop 堆顶后,重新建堆的变化

在这里插入图片描述

所以选择 C 选项

本文转载自: 掘金

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

1…316317318…956

开发者博客

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