Python 爬虫入门(六):urllib库的使用方法

阅读 19

2024-08-05

项目背景介绍:

首先介绍一下项目背景,这个项目是API开发平台,需要完成的接口的功能是:统计谁调用了这个接口,并且将这个接口的调用次数+1,剩余次数-1。

首先看到这个需求第一反应:

得先建个表,建一张用户接口关系表

并且分析用户和接口时多对多关系,一个用户可以调用多个接口,一个接口也可以被多个用户调用。这里什么建表就简单提一句,不是这个文章的重点。

当我们建完表之后,我们再来思考,如果完成这个功能?

在我没有接触API网关之前,第一反应就是AOP,每个接口次数都要加一嘛,这不是很简单用AOP嘛,

后面看了鱼皮老师画的图:

如果单纯只在这个项目中用AOP,就会发现,不太行,因为我们调用的接口有可能来自不同的项目。

每个项目都写一次AOP嘛。这肯定不够优雅,并且也有可能出错。

所以就引入了这个API网关的概念。

给我第一感觉这更像是一个更大的拦截器,

原来写代码都是在一个单体项目里,并且都是一个一个接口为单位去思考问题

这个项目需要以一个更大的视角来看问题了。

 介绍先到这里,下面开始API网关的介绍及使用:


API网关:

先贴一个官网:Spring Cloud 网关 --- Spring Cloud Gateway

该项目提供了一个库,用于在 Spring WebFlux 或 Spring WebMVC 之上构建 API 网关。Spring Cloud Gateway 旨在提供一种简单而有效的方式来路由到 API,并为它们提供跨领域关注点,例如:安全性、监控/指标和弹性。

Features 特征

        Spring Cloud Gateway features:
        Spring Cloud Gateway功能:

  • Built on Spring Framework and Spring Boot
    基于 Spring Framework 和 Spring Boot 构建

  • Able to match routes on any request attribute.
    能够在任何请求属性上匹配路由。

  • Predicates and filters are specific to routes.
    谓词和筛选器特定于路由。

  • Circuit Breaker integration.
    断路器集成。

  • Spring Cloud DiscoveryClient integration
    Spring Cloud Discovery客户端集成

  • Easy to write Predicates and Filters
    易于编写的谓词和过滤器

  • Request Rate Limiting 请求速率限制

  • Path Rewriting 路径重写

这些都是官网的介绍和特征。

大概理解下来就是这个网关可以帮我们转发路径,

就比如我后端的接口地址是:http://localhost:8123/api/name

但是我肯定不能直接把我这个暴露出去,一个因为不安全,一个就是如果我暴露出去,那我上面提到的需求不是做不了了。

这个API网关就可以帮我们转发,比如这个网关项目的地址是:http://localhost:8090

然后我们在配置文件中定于路由的匹配规则就可以定向到http://localhost:8123/api/name

并且呢,我们转发了这个路由,我们就可以做一些我们想要的操作在上面,这个后面会介绍。

介绍完网关的特征和基础概念之后,下一步就是解释一下网关的三个核心概念和两种配置方式:


 网关的三个核心概念和两种配置方式:

路由

网关的基本构建块。它由 ID、目标 URI、谓词集合和筛选器集合定义。如果聚合谓词为 true,则匹配路由。

这个很好理解,就和前端的路由是一样的,当你访问某个url的时候帮你进行跳转到指定的页面,

API网关的路由就是当你访问某个url的时候跳转到特定的url。

谓词

这是 Java 8 函数谓词。输入类型是 Spring Framework ServerWebExchange。这使您可以匹配 HTTP 请求中的任何内容,例如标头或参数

谓词的概念我刚上来也懵了,后面看了一下,可以把这个当成if,我们上面说会匹配到特定的url,但是并不是所有的路径都需要获取,或者换句话说,我的配置文件中用了很多的路由规则,就是靠这个匹配,

      routes:
- id: api_route
uri: http://localhost:8123
predicates:
- Path=/api/**

这是其中的一个路由,下面的predicates就是谓词,当你的这个是/api结尾并且后面是两个**说明任何路径都能匹配,当你匹配上这个路径之后就给你转发到localhost:8123这个地址去

过滤器

这些是使用特定工厂构建的 GatewayFilter 实例。在这里,您可以在发送下游请求之前或之后修改请求和响应。

过滤器也很好理解,就是我们转发了地址,我们可以对这个请求进行一下自己的加工,比如在请求头上加一点标识,这也是后面流量染色的思路。

两种配置方式:

  1. 配置式(方便、规范)
    1. 简化版
    2. 全称版

这个配置式其实就是在配置文件中写配置项,这样写的好处就很明白了简单,固定,但是不灵活

  1. 编程式(灵活、相对麻烦)

这个相对麻烦,但是灵活,我们还可以利用装饰器的设计模式进行增强

讲完了三个核心概念和两种配置方式,下面就具体讲网关的作用:


 网关的作用:

路由:

进行一个转发,上面已经介绍过了

Spring Cloud Gateway

里面这个路由谓词工厂有很多的谓词操作,可以进行使用。

负载均衡:

在nginx中也听过这个概念

就是一个更大的操作,对于一个集群来说,比如其中有一台服务器请求压力过大,就将一部分的请求转发到其它服务器上。

统一处理跨域

网关统一处理跨域,不用在每个项目里单独处理

这个在springbMVC中肯定有见过类似的表达式,就是配置跨域。

统一鉴权:

这里的统一鉴权应该要结合具体的实现逻辑,可以和AOP一起理解

这里可以和AOP一起理解是什么意思呢,就是你可以把AOP写的代码直接复制粘贴到这个网关的过滤器中

发布控制:

不同的接口分配不同的权值,比如要测试一个接口,就给这个接口分配较少的权值进行测试,等测试没问题再调回来或者像圆梦之星的图,新出的图有更多的机会被匹配到是一个道理

 流量染色:

流量染色,听名字好唬人

我们来看这个的中文翻译:

标头添加到所有匹配请求的下游请求的标头中

其实就是给这个请求头打个标记

 接口保护:
        限制请求:

                黑白名单,违规的用户不能访问

        信息脱敏:

                保护用户信息

        降级:

                如果你需要转化的页面访问出现问题,就让用户访问我们实现准备好的降级页面
        

        限流:

                限流简单理解就是当请求服务器的请求太多,稍微平衡一下。

限流这里的参数可能有点不理解:

首先想理解,需要先知道两个算法:漏桶算法和令牌桶算法

 令牌桶算法:也是一个桶,这个桶比较高级一点,

我们往这个桶里匀速的放入令牌,每个请求来拿走一些令牌(不一定是一个),然后如果令牌没有了,那么请求就等待

上面使用的算法就是令牌桶算法:

统一日志:

类似于我刚刚开始学习AOP的第一个案例:对数据库操作进行一个记录,比如记录什么时间点,谁,做了什么事

讲完了这个API网关的作用:

下面就是在具体项目中的应用了:


具体项目中的应用:

在项目中一般采取编程式和配置式向结合的方式来实现。

1:首先做一个大的路由配置:
server:
port: 8090
spring:
main:
web-application-type: reactive
cloud:
gateway:
routes:
- id: api_route
uri: http://localhost:8123
predicates:
- Path=/api/**
logging:
level:
org:
springframework:
cloud:
gateway: trace

这里就是上面看到的了,给用户的接口是localhost:8090/api

然后进行路由匹配到这个localhost:8123/api/**

后面就是真正的服务器接口地址。

这里还有一个配置logging:那个

这个就是降低日志级别,降到最低,把所有的日志信息都进行输出。

2:具体配置一个全局拦截器来执行我们想要的操作:

这个就是全局过滤器的编程式代码。直接复制

解释一下这个

具体的代码:
private static ArrayList<String> IP_WHITE_LIST = new ArrayList<>();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1:请求日志
final ServerHttpRequest request = exchange.getRequest();
log.info("请求唯一标识:"+request.getId());
final URI uri = request.getURI();
log.info("请求路径:"+uri);
final HttpMethod method = request.getMethod();
log.info("请求方法:"+method);
MultiValueMap<String, String> queryParams = request.getQueryParams();
log.info("请求参数:"+queryParams);
String sourceAddress = request.getLocalAddress().getHostString();
log.info("请求地址:"+sourceAddress);
ServerHttpResponse response = exchange.getResponse();
//2:访问控制 -黑白名单
IP_WHITE_LIST.add("127.0.0.1");
if(!IP_WHITE_LIST.contains(sourceAddress)){
handleNoAuth(response);
}
//3:用户鉴权
//4:从数据库中查询模拟接口是否存在
//5:请求转化,调用模拟接口
final Mono<Void> filter = chain.filter(exchange);
return handleResponse(exchange,chain);
}

这段代码的整体逻辑就是(其实单看这个项目完成用户调用接口次数+1根本不需要写这么多,这里作为一个学习的案例,调用一下里面的方法加深印象):

先请求日志,就是看一下请求的信息

再进行访问控制就是设置一个白名单

用户鉴权和查询接口是否存在后面好像鱼皮老师有其它办法,我这里先留一个todo

请求转化这一步就挺难的了

其实第五步后面还有两步就是调用接口次数+1,和打印日志

但是出现了一个问题:

在我和GPT大战500回合下,我总算是领悟到了一点皮毛。

首先描述一下这个问题:

先精简一下这个filter的代码:

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
一系列鉴权操作
chain.filter(exchange);
打印日志和操作数据库
}

我的本意是想就是在执行了这个放行操作之后呢,再打印日志,这也是肯定的,这个操作都没执行打印啥日志,可是我碰到了这个问题,就是我这个代码好像是异步执行的,就是先打印了日志才执行完这个方法

反应式编程:

反应式编程和命令式编程是两种方式

命令式编程就是我们常见的,一行一行代码执行下来,最后输出结果

反应式编程就是这里Mono这个例子

chain.filter(exchange) 返回的是一个 Mono<Void>,这意味着它会异步执行,而不是立即执行。因此,后面的日志打印和数据库操作会在 chain.filter(exchange) 被调用后立即执行,而不等待过滤器链的完成

换句话说就是执行chain.filter(exchange)这个方法太慢了,你这一个进程就一直堵在这里,后面的操作都没法执行了,然后程序就先执行下面的打印日志操作了。

再举一个例子:

 其实到这里我也只是单纯理解了命令式编程和反应式编程的概念

我们具体来看鱼皮老师的解决方法:

/**
* 处理响应
*
* @param exchange
* @param chain
* @return
*/

public Mono<Void> handleResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpResponse originalResponse = exchange.getResponse();
// 缓存数据的工厂
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
// 拿到响应码
HttpStatus statusCode = originalResponse.getStatusCode();
if (statusCode == HttpStatus.OK) {
// 装饰,增强能力
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
// 等调用完转发的接口后才会执行
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
log.info("body instanceof Flux: {}", (body instanceof Flux));
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
// 往返回值里写数据
// 拼接字符串
return super.writeWith(
fluxBody.map(dataBuffer -> {
// 7. 调用成功,接口调用次数 + 1 invokeCount
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);//释放掉内存
// 构建日志
StringBuilder sb2 = new StringBuilder(200);
List<Object> rspArgs = new ArrayList<>();
rspArgs.add(originalResponse.getStatusCode());
String data = new String(content, StandardCharsets.UTF_8); //data
sb2.append(data);
// 打印日志
log.info("响应结果:" + data);
return bufferFactory.wrap(content);
}));
} else {
// 8. 调用失败,返回一个规范的错误码
log.error("<--- {} 响应code异常", getStatusCode());
}
return super.writeWith(body);
}
};
// 设置 response 对象为装饰过的
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
return chain.filter(exchange); // 降级处理返回数据
} catch (Exception e) {
log.error("网关处理响应异常" + e);
return chain.filter(exchange);
}
}

让GPT分析了一下这段代码:

我再举一个生活中的例子来解释到底为什么上面这个案例可以先让 chain.filter(exchange)这个方法执行完再接着执行后面的打印日志(这个不就是我们想要的嘛)

其实是利用到了Flux这个东西提前把请求给完成了,我们才能继续执行后面写日志的操作。

(先简单这样理解,Flux这个东西我还没学过,前端react也不是很了解

先留个todo) 

todo
最后还有一个知识点:就是上面的装饰器设计模式:

装饰器设计模式就是在原有的基础上增强。感觉蛮好理解这个东西

精彩评论(0)

0 0 举报