为了账号安全,请及时绑定邮箱和手机立即绑定
4. Nginx 配置

反向代理你可以简单的理解为转发,转发重要的一点是要配置转发规则。Nginx 配置的默认位置是在 /conf/nginx.conf,配置中关于 Http 的主要配置如下: http { .... server { listen 80;#监听端口 server_name localhost;#域名 # 禁止访问隐藏文件 # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac). location ~ /\. { deny all; access_log off; log_not_found off; } # 默认请求 location / { # 首先尝试将请求作为文件提供,然后作为目录,然后回退到显示 404。 # try_files 指令将会按照给定它的参数列出顺序进行尝试,第一个被匹配的将会被使用。 # try_files $uri $uri/ =404; try_files $uri $uri/ /index.php?path_info=$uri&$args =404; access_log off; expires max; } # 所有动态请求都转发给tomcat处理 location ~ .(jsp|do)$ { proxy_pass http://test; } upstream test { # 负载均衡配置 server localhost:8080; server localhost:8081; }}Http:某台虚拟服务器;Server : 定义了服务器监听哪个端口,哪个域名(可以有多个域名解析到同一台服务器上面);Location :根据请求路径,做不同的响应和转发;Upstream : 里面可以配置多个监听的服务地址,请求过来可以依次亦或根据配置的权重进行轮询,从而达到负载均衡的效果;Nginx 修改完配置可以不用重启,运行下面命令重新加载下配置。nginx -s reload

2. switch…case

Go 语言对 switch…case 的功能进行了扩展,它变得更加的通用。switch 之后可以什么都不带。case 也无需是一个固定值,也可以是一个布尔表达式,而且每一个 case 都是一个 独立的代码块,执行完了之后立刻跳出 switch,不需要使用 break。所以可以把 if…else 完美的改写成 switch…case 的形式。Tips:还有一种 switch 语句叫做 type switch,我们将在学习接口时介绍它switch…case 传统用法代码示例:package mainimport "fmt"func main() { a := "A" switch a { case "A", "a": fmt.Println("分数区间为90~100") case "B", "b": fmt.Println("分数区间为70~89") case "C", "c": fmt.Println("分数区间为0~70") default: fmt.Println("错误的评分") }}第 7 行:和传统用法一致,去求变量 A 的值和那个 case 匹配;第 8 行:case 后面的值使用逗号隔开,用于表示匹配任意一个值;第 14 行:每一个 switch 中最多可以带一个 default。输出结果:switch…case Go 语言中的新用法:package mainimport "fmt"func main() { a := 50 switch { case a < 60: fmt.Println("不及格") case a < 80: fmt.Println("良好") case a <= 100: fmt.Println("优秀") default: fmt.Println("分数最多为100分") }}第 7 行:switch 后不带任何参数,直接执行第 1 个 case 的判定;第 8 行:case 后面带的是一个布尔表达式,若值为 true ,则执行其后代码块;第 14 行:default 在这里就充当 else 的角色。输出结果:

2. 案例

我们来完成一个简单的自定义 http 模块,来实现前面Echo模块的最简单形式,即使用指令输出 “hello, world” 字符串。首先新建一个目录echo-nginx-module,然后在目录下新建两个文件config和ngx_http_echo_module.c[root@server echo-nginx-module]# pwd/root/shencong/echo-nginx-module[root@server echo-nginx-module]# lsconfig ngx_http_echo_module.c两个文件内容分别如下:[root@server echo-nginx-module]# cat config ngx_addon_name=ngx_http_echo_module# 指定模块名称HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module"# 指定模块源码路径NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c"[root@server echo-nginx-module]# cat ngx_http_echo_module.c#include <ngx_config.h>#include <ngx_core.h>#include <ngx_http.h>/* Module config */typedef struct { ngx_str_t ed;} ngx_http_echo_loc_conf_t;static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf);static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);/* 定义指令 */static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), /* 指令名称,利用ngx_string宏定义 */ NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, /* 用在 location 指令块内,且有1个参数 */ ngx_http_echo, /* 处理回调函数 */ NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), /* 指定参数读取位置 */ NULL }, ngx_null_command};/* Http context of the module */static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */};/* Module */ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING};/* Handler function */static ngx_int_tngx_http_echo_handler(ngx_http_request_t *r){ ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; /* 获取指令的参数 */ elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { /* 如果不是 HEAD/GET/PUT 请求,则返回405 Not Allowed错误 */ return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } /* 向用户发送相应包 */ return ngx_http_output_filter(r, &out);}static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){ ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); /* 指定处理的handler */ clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK;}static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf){ ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf;}static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child){ ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK;}这样一个第三方模块包就完成了,接下来我们要向之前使用第三方模块一样,将它编译进 Nginx,具体操作如下。[root@server shencong]# cd nginx-1.17.6/[root@server nginx-1.17.6]# ./configure --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module...[root@server nginx-1.17.6] # make && make install...[root@server nginx-1.17.6]# cd ../nginx-echo/sbin/[root@server sbin]# ./nginx -Vnginx version: nginx/1.17.6built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) configure arguments: --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module接下来,我们只要在 nginx.conf 中加入我们的指令,并给一个参数,就能看到我们自定义的输出了。...http { ... server { listen 80; server_name localhost; location / { root html; index index.html index.htm; } location /test { echo hello,world; } ... }}...最后我们请求主机的80端口,URI=/test,浏览器输出"hello, world",说明我们的自定义模块成功了!

1. 定义条形雪碧图动画

/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { width: 130px; height: 130px; background: url(../img/rect.png); /* 动画: 动画名(loading) 时长(0.6秒) 运行方式(step-end) 动画次数(无限) */ animation: loading .6s step-end infinite;}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ to { background-position: 0 } /* 最后一帧不显示,可以随便写 */}/* 定义动画:动画名(animate) */@keyframes animate { from { background-position: 0 } to { background-position: -2600px }}咦?条形图只需要定义两行?一个from一个to???是的,这就是为什么推荐制作雪碧图的时候做成一行的原因。你只需要定义一开始的时候图像在原点,然后最后的时候图像有多宽,你就写负多少:这个图是2600像素,所以to里面的background-position就是 -2600px。数了一下这张雪碧图里面一共有 12 个元素,所以 steps() 括号里面要写12。div 盒子的宽高应该正好和雪碧图里面的一个元素的宽高相对应:用雪碧图的 宽 2600 除以 12 等于 216.666… 无限循环。咱们取一个近似值,就 216px 吧。所以宽高设置为 216 * 300,怎么设置呢?要让加载动画结束之后(也就是定义加载动画的最后一帧)div 就变成这个宽高。/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { width: 130px; height: 130px; background: url(../img/rect.png); /* 动画: 动画名(loading) 时长(0.6秒) 运行方式(step-end) 动画次数(无限) */ animation: loading .6s step-end infinite;}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ /* 修改最后一帧,以便动画结束后盒子就应用最后一帧的样式 */ to { /* 下一个动画的宽高 */ width: 216px; height: 300px; /* 下一个动画的雪碧图 */ background-image: url(../img/animate.png); }}/* 定义动画:动画名(animate) */@keyframes animate { from { background-position: 0 } to { background-position: -2600px }}

2. DTL 中的模板继承

在这里将介绍 DTL 中的模板继承语法,主要涉及到 block、extends 两个标签。Django 中的模板和 Python 中的类一样是可以被继承的,通过合理的模板继承可以减少前端的工作量,提高代码的复用性以及开发效率。例如下面的 w3school 的在线教程网站:w3school在线教程首页大致上,该网站有一个固定的头部,有一些侧边栏,以及内容主体部分。现在我们使用 Django 中的模板教程来完成一个这样的简单例子。首先在 template 目录下准备网站的框架模板文件 base.html,其内容如下:<html>{% load staticfiles %}<head></head><link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}"><body><!-- 大容器 --><div class="container"> <div class="header"><center>网站头部</center></div> <div class="sidebar"> {% block sidebar %} {% endblock %} </div> <div class="content"> {% block content %} {% endblock %} </div></div></body></html>由于模板文件中加载了静态资源文件,我们除了加上静态资源文件外,还需要加上在 Django 的全局配置文件中进行相关属性的设置:新建 static 目录,并在其下新建 css 目录,然后准备样式表 main.css:.container { border-style: dotted; border-color: red; border-width: 10px; width: 90%; height: 80%;}.container .header { border-bottom-style: solid; border-color: black; border-width: 5px; font-size: 24px; height: 100px; line-height: 100px;}.container .sidebar { border-right-style: solid; border-color: black; border-width: 5px; font-size: 24px; width: 20%; float: left; height: 80%;}.container .content { float: left; width: 60%;}.container .content .title { margin-top: 20px; margin-left: 40%; width: 100%; font-size: 24px; font-weight: bold;}在 Django 的 settings.py 文件中添加 STATICFILES_DIRS 属性:STATIC_URL = '/static/'# 新添加的配置,方便前面的模板文件中找到静态资源路径STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ]可以看到,在前面的 base.html 文件中,我们定义了页面的基本框架,包括了网站头部、侧边栏数据以及内容主体。其中侧边栏和内容主体部分分别定义了两个 block 标签,并进行了命名。接下来我们新建模板文件 test_extends.html,该模板文件继承自 base.html,并给出两个 block 标签对应的数据:{# 继承base.html模板文件 #}{% extends "base.html" %}{# 侧边栏 #}{% block sidebar %}<ul> {% for lesson in lessons %} <li><a href="{{ lesson.addr }}">{{ lesson.name }}</a></li> {% endfor %}</ul>{% endblock %}{# 内容主体 #}{% block content %}<div class="title">{{ title }}</div>{% endblock %}准备好视图函数,这里我们会实现两个视图,使用的模板是一样的,但是填充的数据不一样而已:# hello_app/views.pyfrom django.shortcuts import renderdef test_django_view(request, *args, **kwargs): data = { 'title': 'Django教程手册', 'lessons': [ {'name': 'web框架', 'addr': '/web_framework'}, {'name': 'django发展历史', 'addr': '/django_history'}, {'name': 'django基础上', 'addr': '/base_one'}, {'name': 'django基础下', 'addr': '/base_two'}, ] } return render(request, 'test_extends.html', context=data)def test_nginx_view(request, *args, **kwargs): data = { 'title': 'Nginx教程手册', 'lessons': [ {'name': 'Nginx介绍', 'addr': '/web_server'}, {'name': 'Nginx发展历史', 'addr': '/nginx_history'}, {'name': 'Nginx优势', 'addr': '/nginx_advantages'}, ] } return render(request, 'test_extends.html', context=data)准备好 URLconf 配置:from django.urls import path, re_pathurlpatterns = [ path('test-django/', views.test_django_view), path('test-nginx/', views.test_nginx_view),]启动 Django 服务页面,然后分别请求/hello/test-django/ 和 /hello/test-nginx/ 两个地址,可以看到如下两个效果图,网页的整体布局不变,但是数据不同。几乎所有的大型网站都是靠这样继承模式实现的优化前端代码和统一页面的风格。test-django效果图test-nginx效果图通过上面的简单实验,我们能够理解并初步掌握 Django 中模板的继承用法。这种基于继承网页的做法能使得我们开发的网站具有统一的风格,也是后面经常会用到的一种模板编写手段。

4.1 connect()

connect () 用来连接服务端,常见的运用场景主要有三点,分别是①监听连接结果;②失败重连;③断开重连。4.1.1 连接监听connect () 方法返回的是 ChannelFuture,也就是说不需要等待连接成功或失败才往下执行代码,后期可以监听连接结果。实例://1.连接Netty服务端ChannelFuture future=bootstrap.connect("127.0.0.1",80);//2.监听连接结果future.addListener(future -> { if (future.isSuccess()) { System.out.println("连接成功!"); } else { System.err.println("连接失败!"); }});总结,这种模式的好处是,连接是异步的,无需等待连接响应代码才会往下执行。4.1.2 失败重连在网络情况差的情况下,客户端第一次连接可能会连接失败,这个时候我们可能会尝试重新连接,具体实现如下:方案一: 通过 ChannelFuture 的返回状态来监听连接是否成功。实例:private static void connect(Bootstrap bootstrap, String host, int port) { bootstrap.connect(host, port).addListener(future -> { if (future.isSuccess()) { System.out.println("连接成功!"); } else { System.err.println("连接失败,开始重连"); //递归调用连接方法 connect(bootstrap, host, port); } });}方案二: 避免短时间内频繁的请求连接,可以使用定时线程池来每隔 n 秒重连一次。实例:private static void connect(Bootstrap bootstrap, String host, int port) { bootstrap.connect(host, port).addListener(future -> { if (future.isSuccess()) { System.out.println("连接成功!"); } else { //获取EventLoopGroup EventLoopGroup thread=bootstrap.config().group(); //每隔5秒钟重连一次 thread.schedule(new Runnable() { public void run() { connect(bootstrap, host, port) } }, 5, TimeUnit.SECONDS); } });}代码说明:bootstrap.config().group() 获取的 EventLoopGroup,它是一个线程池,线程池里面有一个叫定时线程池。

1.3 如何学习和使用第三方模块

这里我们演示在 Nginx 中使用第三方模块。 Openresty 社区提供了一款 Nginx 中的 Echo 模块,即echo-nginx-module。在 Nginx 中添加了该模块后,我们在配置文件中可以使用该模块提供的 echo 指令返回用户响应,简单方便。该模块的源码在 github 上,并且有良好的文档和使用示例,非常方便开发者使用。现在我们在 Nginx 的源码编译阶段加入该第三方模块,具体操作如下:[root@server shencong]# pwd/root/shencong[root@server shencong]# mkdir nginx-echo# 下载 nginx 源码包和第三方模块的源码包 [root@server shencong]# wget http://nginx.org/download/nginx-1.17.6.tar.gz[root@server shencong]# wget https://github.com/openresty/echo-nginx-module/archive/v0.62rc1.tar.gz# 解压[root@server shencong]# tar -xzf nginx-1.17.6.tar.gz[root@server shencong]# tar -xzf v0.62rc1.tar.gz[root@server shencong]# lsecho-nginx-module-0.62rc1 nginx-1.17.6 nginx-1.17.6.tar.gz nginx-echo v0.62rc1.tar.gz[root@server shencong]# cd nginx-1.17.6# 使用--add-module添加第三方模块,参数为第三方模块源码[root@server shencong]# ./configure --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module-0.62rc1编译完成后,我们就可以去nginx-echo目录中的 nginx.conf文件中添加echo 指令 。准备如下的配置(可以参参考社区提供的示例):...http { server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; } # 新增测试 echo 指令配置 location /timed_hello { default_type text/plain; echo_reset_timer; echo hello world; echo "'hello world' takes about $echo_timer_elapsed sec."; echo hiya igor; echo "'hiya igor' takes about $echo_timer_elapsed sec."; } location /echo_with_sleep { default_type text/plain; echo hello world; echo_flush; # ensure the client can see previous output immediately echo_sleep 2.5; # in sec echo "'hello' takes about $echo_timer_elapsed sec."; } }}...启动 Nginx 后,我们就可以在浏览器上请求者两个 URI 地址,看到相应 echo 返回的信息了。第二个配置是使用了 echo_sleep 指令,会使得请求在休眠 2.5s 后才返回。

1. 调用动画

定义好了就可以去调用了,来看一下怎么调用:/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { width: 130px; height: 130px; background: url(../img/rect.png); /* 动画: 动画名(loading) 时长(0.6秒) 运行方式(step-end) 动画次数(无限) */ animation: loading .6s step-end infinite;}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ to { background-position: 0 } /* 最后一帧不显示,可以随便写 */}为了能够让同学们在浏览器里直接看结果,我们这里写了一个可运行的案例:910运行结果: ![图片描述](//img1.sycdn.imooc.com//wiki/5eda04590a708f6c01650135.jpg) 可以看到效果就已经很完美的呈现出来了,那么接下来我们再来添加一下条形雪碧图,看看条形雪碧图的用法有何不同。

1. container 模式

与 host 模式类似,container 模式可以使一个容器共享另一个已存在容器的网络,此时这两个容器共同使用同一网卡、主机名、IP 地址,容器间通讯可直接通过本地回环 lo 接口通讯。新运行一个 busybox 的容器 b1,设定它共享已存在的容器 b0 的网络:docker run -d -t --network container:b0 --name b1 busyboxTips:端口转发设定以已存在的容器为准,出于安全和权限控制的角度,container 模式下运行的容器设定端口转发不生效。查看 b0,b1 的网络配置,验证两者的网络配置是否相同:docker exec b0 ifconfigdocker exec b1 ifconfig此时的网络拓扑图如下:container 网络拓扑不再使用的容器记得删除掉,释放资源和空间docker rm -f b0 b1nginx 镜像自带的网络命令非常少,查看网络不方便,而 busybox 的网络命令比较齐全,使用 container 模式,可以快速解决这个问题。我们新运行一个名为 n0 的 nginx 容器,再将它的网络共享给 busybox 容器 n0-net:docker run -d -t --name n0 nginxdocker run -d -t --network container:n0 --name n0-net busybox使用 n0-net 容器,执行 docker exec n0-net ip a 进行网络状态查看自身网络信息,也就是 nginx 的网络信息执行如下命令,通过 localhost 访问 n0 的 web 服务,说明通过 container 模式下,共享的网络中的容器能够使用 lo 访问其他容器的服务。docker exec n0-net telnet localhost 80# 在交互中输入# GET /#不再使用的容器记得删除掉,释放资源和空间:docker rm -f n0 n0-net

2. 调用两个动画

重点是如何进行调用,先来看一下语法:/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { width: 130px; height: 130px; background: url(../img/rect.png); /* 动画: 动画名(loading) 时长(0.6秒) 运行方式(step-end) 动画次数(3次) 填充模式(双向) */ animation: loading .6s step-end 3 both, /* 动画可以定义多个,每个动画用逗号分隔。*/ /* 第二个动画的动画名(animate) 时长(0.8秒) 运行方式(step-end) 延时(1.8秒) 动画次数(无限) */ animate .8s steps(12) 1.8s infinite;}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ /* 修改最后一帧,以便动画结束后盒子就应用最后一帧的样式 */ to { /* 下一个动画的宽高 */ width: 216px; height: 300px; /* 下一个动画的雪碧图 */ background-image: url(../img/animate.png); }}/* 定义动画:动画名(animate) */@keyframes animate { from { background-position: 0 } to { background-position: -2600px }}运行结果:这是怎么个原理呢?原来调用动画的时候可以一次性调用多个动画,动画与动画直接用逗号进行分隔。第一个加载动画我们让他重复运行 3 次,由于下一个动画的背景图和宽高都和加载动画不同,所以调用第一个动画时用填充模式将最后一帧定义的样式应用到下个动画上。

2. 动画的定义

如果学过一些编程语言的同学会知道,有一个词叫做变量,这个变量通常是需要事先定义好才能够去使用。CSS 动画也是同理,需要先定义,才能够去使用。接下来我们就来看看该如何定义一个 CSS 动画: @keyframes 动画名 { ​动画内容 }@keyframes 是一个固定的写法,表示要定义一个动画,后面要空一格再写你的动画名,然后大括号里面再写上对应的动画内容。学过 JavaScript 的同学(没学过的话也没关系,可以继续往下看)可以把 @keyframes 理解为 JS 中的 var,就相当于定义了一个变量。大括号里面写的可以是百分比,百分比后面的大括号里面就是你自己想要的 CSS 样式啦!假如我们定义一个名为 change-color 的动画:<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>@keyframes</title> <style> /* 先定义一个名为change-color的动画 */ @keyframes change-color { 0% { color: red } /* 红 */ 16% { color: orange } /* 橙 */ 32% { color: yellow } /* 黄 */ 48% { color: green } /* 绿 */ 64% { color: cyan } /* 青 */ 80% { color: blue } /* 蓝 */ 100% { color: purple } /* 紫 */ } </style></head><body> </body></html>TIPS:0% 可以写成 from,100% 可以写成 to,效果完全一致,只是一个别名。我们按照红橙黄绿青蓝紫的这么一个彩虹颜色顺序定义了一个名为 change-color 的动画,但是此时却没有任何的效果,这是因为目前仅仅只是定义了这个动画,并没有去指定哪个元素会用到这个动画,以及该如何使用这个动画。那么接下来就让我一起来看看该如何使用这个动画吧!

3. Linux 发展史

Linux 操作系统的诞生、发展和成长过程始终依赖着五个重要支柱:UNIX 操作系统、MINIX 操作系统、GNU 计划、POSIX 标准和 Internet 网络。20 世纪 80 年代,计算机硬件的性能不断提高,PC 机的市场不断扩大,UNIX、DOS、MacOS。UNIX 操作系统价格昂贵且不能在一般的 PC 机上面运行,DOS 操作系统相对来说比较简陋,且源代码被软件厂商严格保密,MacOS 是一种专门用于苹果计算机的操作系统。因此,计算机应用领域需要更加完善、强大、价格低廉和开源的操作系统。由于供教学使用的典型操作系统很少,当时荷兰一位美国人教授 AndrewS.Tanenbaum 编写了一个操作系统,名为 MINIX,为了向学生讲述操作系统内部工作原理。MINIX 虽然很好,但只是一个用于教学为目的的简单操作系统,它最大的好处就是公开了源代码。全球计算机领域的学生都可以通过钻研 MINIX 源代码来了解和学习 MINIX 操作系统,其中芬兰赫尔辛基大学的学生 Linus Torvalds 就是其中一个,他在吸收了 MINIX 精华的基础上,在 1991 年写出了属于自己的操作系统 Linux,并且版本为 Linux0.01,是 Linux 时代开始的标志。他利用 UNIX 的核心,去掉了那些复杂难处理的核心程序,把它改写成适用于普通计算机的操作系统放在网络上免费供其他学习者下载。1994 年推出完整的核心 Version1.0,至此,Linux 逐渐成为功能完善、稳定的操作系统,并被广泛使用。

3. 调节雪碧图尺寸

不过感觉最后这个跳动的卡通小人还是有点大,像素颗粒感明显,所以我们绝定缩小一下加载动画最后一帧给定的宽高:/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { width: 130px; height: 130px; background: url(../img/rect.png); /* 动画: 动画名(loading) 时长(0.6秒) 运行方式(step-end) 动画次数(3次) 填充模式(双向) */ animation: loading .6s step-end 3 both, /* 动画可以定义多个,每个动画用逗号分隔。*/ /* 第二个动画的动画名(animate) 时长(0.8秒) 运行方式(step-end) 延时(1.8秒) 动画次数(无限) */ animate .8s steps(12) 1.8s infinite;}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ /* 修改最后一帧,以便动画结束后盒子就应用最后一帧的样式 */ to { /* 下一个动画的宽高 */ width: 108px; height: 150px; /* 下一个动画的雪碧图 */ background-image: url(../img/animate.png); }}/* 定义动画:动画名(animate) */@keyframes animate { from { background-position: 0 } to { background-position: -2600px }}注意:宽高一定要按比例缩小,不能宽缩小三分之一,高缩小一半。要宽和高都同时缩小一半才能保持住比例。运行结果:看起来怎么和咱们预想中的效果不太一样呢?因为盒子缩小了一半,但是雪碧图却并没有缩小,那么大家还记得之前的章节中我们讲过的如果雪碧图尺寸不吻合时怎么办吗?对没错!(大概率是不记得了)就是background-size: cover;/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { width: 130px; height: 130px; background: url(../img/rect.png); /* 动画: 动画名(loading) 时长(0.6秒) 运行方式(step-end) 动画次数(3次) 填充模式(双向) */ animation: loading .6s step-end 3 both, /* 动画可以定义多个,每个动画用逗号分隔。*/ /* 第二个动画的动画名(animate) 时长(0.8秒) 运行方式(step-end) 延时(1.8秒) 动画次数(无限) */ animate .8s steps(12) 1.8s infinite;}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ /* 修改最后一帧,以便动画结束后盒子就应用最后一帧的样式 */ to { /* 下一个动画的宽高 */ width: 108px; height: 150px; /* 下一个动画的雪碧图 */ background-image: url(../img/animate.png); /* 雪碧图的最短边(这里是高)刚好能够覆盖住盒子 */ background-size: cover; }}/* 定义动画:动画名(animate) */@keyframes animate { from { background-position: 0 } to { background-position: -2600px }}运行结果:

1 steps()

step的英文原意是迈步,在这里可以理解为步骤。既然是步骤,那就要指定分几步进行。所以括号里面就要写入一个数字,这个数字代表要分多少步来进行这段动画。拿我们定义过的change-color动画来举例:/* 定义动画:动画名(change-color) */ @keyframes change-color { /* 第1步 */ from /* 0% */ { color: red } /* 红 */ /* 第2步 */ 16% { color: orange } /* 橙 */ /* 第3步 */ 32% { color: yellow } /* 黄 */ /* 第4步 */ 48% { color: green } /* 绿 */ /* 第5步 */ 64% { color: cyan } /* 青 */ /* 第6步 */ 80% { color: blue } /* 蓝 */ /* 第7步 */ to /* 100% */ { color: purple } /* 紫 */}可以看到整个动画一共分为了七个步骤,那么是不是我们要在 step() 的括号里写上一个7呢?steps(7)你要是这么想的话,那你可就大错特错了。括号里面的数字不是指从0%到100%之间要被分成几步,而是从第一步到第二步要被分为几步。同理,第二步到第三步,第三步到第四步,……,第N步到最后一步。而我们想要的效果是:从红变橙、从橙变黄、……、再从蓝变紫。所以我们只需要第一步到第二步、第N步到第N+1步之间只经历一个步骤就够了,所以应该写成:steps(1)steps()括号里一共可以写两个参数,第一个参数就是一个数字,代表把这次过渡分成几步。第二个参数不写的话默认就是end,也就是说:steps(1) = steps(1, end)聪明的小伙伴们肯定想了,既然有end,那肯定就会有start嘛!对没错,第二个参数除了end以外还可以写start。知道了它的第二个参数,就比较好理解step-start和start-end了。

1. 一些基本概念

代码覆盖率(Code Coverage)是反映测试用例对被测软件覆盖程度的重要指标,也是衡量测试工作进展情况的重要指标。它也是对测试工作进行量化的重要指标之一,根据其覆盖内容的不同,又可以细分为:语句覆盖、判定覆盖、条件覆盖、路径覆盖以及循环覆盖等等,强度也是从弱到强。在所有这些覆盖中语句覆盖(Statement coverage)是最简单的,也是最常用的,PyCharm 默认支持的就是语句覆盖。语句覆盖/代码行覆盖:目标保证程序中每一条语句最少执行一次,其覆盖标准无法发现判定中逻辑运算的错误。判定覆盖/分支覆盖:是指选择足够的测试用例,使得运行这些测试用例时,每个判定的所有可能结果至少出现一次,但若程序中的判定是有几个条件联合构成时,它未必能发现每个条件的错误。条件覆盖:是指选择足够的测试用例,使得运行这些测试用例时,判定中每个条件的所有可能结果至少出现一次,但未必能覆盖全部分支。条件组合覆盖:是使每个判定中条件结果的所有可能组合至少出现一次,因此判定本身的所有可能解说也至少出现一次,同时也是每个条件的所有可能结果至少出现一次。路径覆盖: 是每条可能执行到的路径至少执行一次,试图覆盖软件中的所有路径;【补充说明】对于敏捷开发团队而言,代码覆盖率是每个Sprint要完成的硬性质量标准(Exit Criteria)之一,覆盖率高低根据项目的不同而不同:75%,80%甚至100%都是可能的。

4.1 基本步骤

step1: 主菜单 File -> New -> Project, 选择 Scientificstep2: 在项目设置对话框窗口中,指定项目名称,确保将 Conda 选为新环境,然后单击"Create"。step3: 自动创建了 main.py并且打开, 在其中加下面的代码。(主要功能是用拆线图展示北京与上海两个城市一小时的温度变化曲线。Tips: 使用Conda 解释器,像Numpy , matplotlib 基本的科学计算包都已经自带了,不需要再单独安装。)# 画出温度变化图import randomimport matplotlib.pyplot as plt# 准备x, y坐标的数据x = range(60)y_shanghai = [random.uniform(15, 18) for i in x]# 增加北京的温度数据y_beijing = [random.uniform(1, 3) for i in x]# 创建画布plt.figure(figsize=(20, 8), dpi=80)# 绘制折线图# plt.plot(x, y_shanghai)plt.plot(x, y_shanghai, label="SHANGHAI")# 使用多次plot可以画多个折线plt.plot(x, y_beijing, color='r', linestyle='--', label="BEIJING")# 显示图例plt.legend(loc="best")# 构造x轴刻度标签x_ticks_label = ["11:{}".format(i) for i in x]# 构造y轴刻度y_ticks = range(40)# 修改x,y轴坐标的刻度显示plt.xticks(x[::5], x_ticks_label[::5])plt.yticks(y_ticks[::5])# 添加网格显示plt.grid(True, linestyle='--', alpha=0.5)# 添加x轴、y轴描述信息及标题plt.xlabel("Time")plt.ylabel("Temperature")plt.title("Temperature Change between 11am and 12am")# 显示图像plt.show()step4: PyCharm 内需启用 Scientific Mode 才能正常显示 matplotlib 相关图表。主菜单View -> Scientific Mode。step5: 运行项目⇧F10 (Shift + F10), 代码执行完毕,会有如下三个工具窗口显示:"SciView"工具窗口。它有两个选项卡, "data"选项卡中预览数据帧,在"plot"选项卡中预览 matplotlib 图表;"Documentation"工具窗口,显示编辑器内光标插入位置处对象的内联文档,比如光标停留在matplotlib 包导入的地方,就会显示 matplotlib 的相关信息;Python 控制台。执行完毕后,自动打开,程序中涉及变量详细值也在右侧边栏自动显示出来。

4.3 NioEventLoop 执行流程

上面讲解了 NioEventLoop 的初始化流程,那么它到底在什么时候开始执行的呢?源码入口:serverBootstrap.bind(80);第一步: 抽象类 AbstractBootstrappublic abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable { public ChannelFuture bind(int inetPort) { return this.bind(new InetSocketAddress(inetPort)); } public ChannelFuture bind(SocketAddress localAddress) { this.validate(); if (localAddress == null) { throw new NullPointerException("localAddress"); } else { //继续跟进 return this.doBind(localAddress); } } private ChannelFuture doBind(final SocketAddress localAddress) { //继续跟进 final ChannelFuture regFuture = this.initAndRegister(); } final ChannelFuture initAndRegister() { //继续跟进 this.init(channel); } //抽象方法 abstract void init(Channel var1) throws Exception;}第二步: 实现类 ServerBootstrappublic class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> { void init(Channel channel) throws Exception { //1.把 ChannelHandler 添加到 ChannelPipeline 里,组成一条双向业务链表 p.addLast(new ChannelHandler[]{new ChannelInitializer<Channel>() { public void initChannel(Channel ch) throws Exception { //1.1.管道 final ChannelPipeline pipeline = ch.pipeline(); //1.2.添加到管道 ChannelHandler handler = ServerBootstrap.this.config.handler(); if (handler != null) { pipeline.addLast(new ChannelHandler[]{handler}); } //1.3.执行线程池的 “execute()”,核心入口 ch.eventLoop().execute(new Runnable() { public void run() { pipeline.addLast( new ChannelHandler[]{ new ServerBootstrap.ServerBootstrapAcceptor( currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs) } ); } }); } }}); }}这里是在 init () 方法里面进行一序列的初始化工作,并且执行上面初始化好的 NioEventLoop 的 execute () 方法。第三步: 执行 SingleThreadEventExecutor 的 execute () 方法public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor { public void execute(Runnable task) { //是否是当前线程 boolean inEventLoop = this.inEventLoop(); if (inEventLoop) { //如果是当前线程,则添加任务到队列 this.addTask(task); } else { //如果不是当前线程,则先启动线程 this.startThread(); //把任务添加到任务队列 this.addTask(task); //如果线程已经关闭并且该任务已经被移除了 if (this.isShutdown() && this.removeTask(task)) { //执行拒绝策略 reject(); } } } private void startThread() { this.doStartThread(); } private void doStartThread() { this.executor.execute(new Runnable() { public void run() { //执行 run() 方法 SingleThreadEventExecutor.this.run(); } }); } //抽象方法 protected abstract void run();}第四步: 子类 NioEventLoop 实现抽象方法 run (),这里是 run () 方法是一个死循环,并且执行三个核心事件,分别是 “监听端口”、“处理端口事件”、“处理队列事件”。public final class NioEventLoop extends SingleThreadEventLoop { protected void run() { while(true) { //省略 } }}run () 方法里面核心执行了 this.processSelectedKeys() 和 this.runAllTasks()。

3. 常用方法

Optional<T>类提供了如下常用方法:booean isPresent():判断是否包换对象;void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传递给它;T get():如果调用对象包含值,返回该值,否则抛出异常;T orElse(T other):如果有值则将其返回,否则返回指定的other 对象;T orElseGet(Supplier<? extends T other>):如果有值则将其返回,否则返回由Supplier接口实现提供的对象;T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由Supplier接口实现提供的异常。知道了如何创建Optional对象和常用方法,我们下面结合具体实例来看一下,Optional类是如何避免空指针异常的。请查看如下实例,其在运行时会发生空指针异常:1252运行结果:Exception in thread "main" java.lang.NullPointerException at OptionalDemo2.getGoodsCategoryName(OptionalDemo2.java:73) at OptionalDemo2.main(OptionalDemo2.java:80)实例中,由于在实例化Goods类时,我们没有给其下面的Category类型的属性category赋值,它就为 null,在运行时, null.getName()就会抛出空指针异常。同理,如果goods实例为null,那么null.getCategory()也会抛出空指针异常。在没有使用Optional类的情况下,想要优化代码,就不得不改写getGoodsCategoryName()方法:static String getGoodsCategoryName(Goods goods) { if (goods != null) { Category category = goods.getCategory(); if (category != null) { return category.getName(); } } return "该商品无分类";}这也就是我们上面说的null检查逻辑代码,此处有两层if嵌套,如果有更深层次的级联属性,就要嵌套更多的层级。下面我们将Optional类引入实例代码:1253运行结果:默认分类实例中,我们使用Optional类的 ofNullable(T t)方法分别包装了goods对象及其级联属性category对象,允许对象为空,然后又调用了其ofElse(T t)方法保证了对象一定非空。这样,空指针异常就被我们优雅地规避掉了。

4. 重新调整速度

尺寸的问题是解决了,怎么速度又不对了?原来是因为雪碧图缩小了一半,所以现在的宽只有 1300 px了。可是我们定义的动画是 -2600 px,于是乎动画在相同的时间移动了 2 倍的距离,看起来就会导致速度加快。解决办法也很简单,把定义的动画距离也同比例缩小:/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { width: 130px; height: 130px; background: url(../img/rect.png); /* 动画: 动画名(loading) 时长(0.6秒) 运行方式(step-end) 动画次数(3次) 填充模式(双向) */ animation: loading .6s step-end 3 both, /* 动画可以定义多个,每个动画用逗号分隔。*/ /* 第二个动画的动画名(animate) 时长(0.8秒) 运行方式(step-end) 延时(1.8秒) 动画次数(无限) */ animate .8s steps(12) 1.8s infinite;}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ /* 修改最后一帧,以便动画结束后盒子就应用最后一帧的样式 */ to { /* 下一个动画的宽高 */ width: 108px; height: 150px; /* 下一个动画的雪碧图 */ background-image: url(../img/animate.png); /* 雪碧图的最短边(这里是高)刚好能够覆盖住盒子 */ background-size: cover; }}/* 定义动画:动画名(animate) */@keyframes animate { from { background-position: 0 } to { background-position: -1300px }}运行结果:911

1. 定义动画

想要运行一个动画,就要先去定义一个动画 —— 鲁迅。那么我们就先来看看矩形图要怎么定义动画:/* 清除浏览器默认边距 */* { padding: 0; margin: 0; }body { /* 这段代码是为了居中显示,不是重点,看不懂的话可以无视 */ height: 100vh; display: flex; align-items: center; justify-content: center; /* 添加背景图 */ background: url(../img/bg.jpg) center / cover;}.animate { background: url(../img/rect.png);}/* 定义动画:动画名(loading) */@keyframes loading { from { background-position: 0 0 } /* 第一个数字代表x轴坐标,第二个数字代表y轴坐标 */ 10% { background-position: -130px 0 } /* x坐标:-130 y坐标:0 */ 20% { background-position: -260px 0 } /* x坐标:-260 y坐标:0 */ 30% { background-position: -390px 0 } /* x坐标:-390 y坐标:0 */ 40% { background-position: -520px 0 } /* x坐标:-520 y坐标:0 */ 50% { background-position: 0 -130px } /* x坐标:0 y坐标:-130 */ 60% { background-position: -130px -130px } /* x坐标:-130 y坐标:-130 */ 70% { background-position: -260px -130px } /* x坐标:-260 y坐标:-130 */ 80% { background-position: -390px -130px } /* x坐标:-390 y坐标:-130 */ 90% { background-position: -520px -130px } /* x坐标:-520 y坐标:-130 */ to { background-position: 0 } /* 最后一帧不显示,可以随便写 */}定义一个名为 loading 的动画,雪碧图上一共有 10 个元素,所以在这里我们定义 11 帧(最后一帧看不到)。每一帧都要对准位置,整张雪碧图的尺寸是 680px * 260px,2 行 5 列。所以高260除以行2等于 130px、宽 680除以列 5还是等于 130px,所以我们的 div 宽高要设置成 130 * 130,第一帧到第五帧都是宽(130px)的倍数,第一帧是0 * 130px,第二帧是1 * 130px,依此类推。到了第五帧(40%)的时候,整个第一行已经都过了一遍,所以第六帧(50%)我们要换到第二行的行首。于是 y 坐标由之前的 0 变成了 -130px,刚好是一行的高度。有的同学可能会有一个疑问:为什么这些坐标都是负值呢?我们还是用图片去理解:小一点的方框代表我们的 div,大方块代表雪碧图,原点为左上角。如果是正值的话,就是雪碧图左上角距离 div 左上角右移。如果值为负的话,就是左移。y 轴同理,正值下移,负值上移。

1. Redis 介绍与安装

首先我们在 CentOS7.8 的系统上源码编译安装 redis-5.0 的最新版。按照下列步骤进行:安装 gcc 等编译工具,下载 redis 5 的最新源码并解压:[root@server2 shen]# wget http://download.redis.io/releases/redis-5.0.9.tar.gz--2020-06-27 13:10:23-- http://download.redis.io/releases/redis-5.0.9.tar.gzResolving download.redis.io (download.redis.io)... 109.74.203.151Connecting to download.redis.io (download.redis.io)|109.74.203.151|:80... connected.HTTP request sent, awaiting response... 200 OKLength: 1986574 (1.9M) [application/x-gzip]Saving to: ‘redis-5.0.9.tar.gz’100%[=========================================================================================================================================>] 1,986,574 12.9KB/s in 2m 11s 2020-06-27 13:12:35 (14.8 KB/s) - ‘redis-5.0.9.tar.gz’ saved [1986574/1986574][root@server2 shen]# tar xzf redis-5.0.9.tar.gz进入 redis 源码目录,直接安装:[root@server2 shen]# cd redis-5.0.9[root@server2 redis-5.0.9]# make MALLOC=libc PREFIX=/usr/local/redis install添加 redis 命令路径:[root@server2 redis-5.0.9]# cat /etc/profile...# 添加下面两行内容REDIS_HOME=/usr/local/redisexport PATH=$REDIS_HOME/bin:$PATH[root@server2 redis-5.0.9]# source /etc/profile[root@server2 redis-5.0.9]# which redis-cli/usr/local/redis/bin/redis-cli添加并修改 redis.conf 配置文件:[root@server2 redis-5.0.9]# mkdir /etc/redis[root@server2 redis-5.0.9]# cp redis.conf /etc/redis[root@server2 redis-5.0.9]# cat /etc/redis# ...# 修改第一处,改为后台守护进程方式启动daemonize yes# ...# 修改端口,比如改为6777port 6777# ...# 需要密码# requirepass foobaredrequirepass spyinx# ...# 允许通过公网访问该redis# bind 127.0.0.1bind 0.0.0.0加入 systemd 服务,统一管理:[root@server2 redis-5.0.9]# cat /etc/systemd/system/redis.service[Unit]Description=RedisAfter=network.target [Service]Type=forkingExecStart=/usr/local/redis/bin/redis-server /etc/redis/redis.confExecReload=/bin/kill -s HUP $MAINPIDExecStop=/usr/local/redis/bin/redis-cli -p 6777 shutdownPrivateTmp=true [Install]WantedBy=multi-user.target[root@server2 redis-5.0.9]# systemctl start redis[root@server2 redis-5.0.9]# systemctl status redis● redis.service - Redis Loaded: loaded (/etc/systemd/system/redis.service; disabled; vendor preset: disabled) Active: active (running) since Sat 2020-06-27 14:08:44 CST; 3s ago Process: 7080 ExecStart=/usr/local/redis/bin/redis-server /etc/redis/redis.conf (code=exited, status=0/SUCCESS) Main PID: 7081 (redis-server) CGroup: /system.slice/redis.service └─7081 /usr/local/redis/bin/redis-server 0.0.0.0:6777Jun 27 14:08:44 server2 systemd[1]: Starting Redis...Jun 27 14:08:44 server2 redis-server[7080]: 7080:C 27 Jun 2020 14:08:44.938 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0OoJun 27 14:08:44 server2 redis-server[7080]: 7080:C 27 Jun 2020 14:08:44.938 # Redis version=5.0.9, bits=64, commit=00000000, ...startedJun 27 14:08:44 server2 redis-server[7080]: 7080:C 27 Jun 2020 14:08:44.938 # Configuration loadedJun 27 14:08:44 server2 systemd[1]: Started Redis.Hint: Some lines were ellipsized, use -l to show in full.# 设置开机启动[root@server2 redis-5.0.9]# systemctl enable redisCreated symlink from /etc/systemd/system/multi-user.target.wants/redis.service to /etc/systemd/system/redis.service.最后测试下 redis 服务是否能正常工作:[root@server2 redis-5.0.9]# redis-cli -p 6777127.0.0.1:6777> auth spyinxOK127.0.0.1:6777> ping "hello, world""hello, world"127.0.0.1:6777> set hello worldOK127.0.0.1:6777> get hello"world"127.0.0.1:6777> 完成 redis 服务搭建后我们就可以开始 redis 服务的使用了。

2.1 Python 的历史

Python 的创始人为荷兰人 Guido van Rossum。1982年,Guido 从阿姆斯特丹大学(University of Amsterdam)获得了数学和计算机硕士学位。Python 的创始人 Guido van Rossum 在80年代,个人电脑的配置很低,比如早期的 Macintosh,只有 8MHz 的 CPU 主频和 128KB 的 RAM。为了增进程序的运行效率,程序语言也迫使程序员像计算机一样思考,以便能写出充分利用计算机性能的程序。Guido 使用 C 语言的过程中,感受到 C 语言的开发效率很低,需要耗费大量的时间编写 C 程序。他的另一个选择是 Shell。 Shell 是 UNIX 操作系统提供的脚本语言。UNIX 的管理员常常用 Shell 去写一些简单的脚本,以进行一些系统维护的工作,比如定期备份。Shell 可以像胶水一样,将 UNIX 下的许多功能连接在一起。许多 C 语言下上百行的程序,在 Shell 下只用几行就可以完成。Shell 的本质是调用命令来完成复杂的功能,它缺乏编程语言的若干重要特性。例如,Shell 缺乏复杂的数据结构:列表、字典、结构体,因此不适用于实现复杂的功能。Guido 希望有一种语言,这种语言能够像 C 语言那样,具备完整的编程语言特性,又可以像 Shell 那样,具有很高的开发效率。1989 年圣诞节期间,在阿姆斯特丹,Guido 为了打发圣诞节的无趣,决心开发一个新的程序语言 Python。Guido 将 Python(大蟒蛇)作为该编程语言的名字,是取自英国 20 世纪 70 年代首播的电视喜剧《蒙提.派森的飞行马戏团》(Monty Python’s Flying Circus),因此 Python 语言的 logo 是一条蟒蛇。Python 语言的 logo1991 年,第一个 Python 解释器诞生,它是用 C 语言实现的,又被称为 CPython。Python 从一开始就具有良好的可扩展性,可以用 C 语言编写模块,在 Python 程序中使用这些使用 C 语言开发的模块。1999 年,Guido 向 DARPA 提交了一条名为 “Computer Programming for Everybody” 的资金申请,并在后来说明了他对 Python 的目标:一门简单直观的语言并与主要竞争者一样强大开源,以便任何人都可以为它做贡献代码像纯英语那样容易理解适用于短期开发的日常任务这些想法中的基本都已经成为现实,Python 已经成为一门流行的编程语言。

2.1 HTTPS 请求流程

面试官提问: HTTPS 的请求流程和 HTTP 协议的请求流程有什么区别?题目解析:参考 HTTPS 的官方文档,我们将整个请求的流程简单抽象为以下几个步骤,抓住其中的核心步骤: (HTTPS 简化通信模型)步骤(1):客户端发送一个 HTTPS 请求,例如请求 https://imooc.com,连接到服务器端的 443 端口(和 HTTP 协议不同,HTTP 默认 80 端口)。步骤(2):服务器端收到握手信息,使用预先配置好的数字证书,即图中的公钥和私钥。如果是自己颁发的证书,那么需要客户端通过浏览器的弹窗验证,如果是组织申请获得,默认直接通过。步骤(3):传输证书给客户端,证书组装了多种信息,包含证书的颁发机构、证书有效时间、服务器端的公钥,证书签名等。步骤(4):客户端解析证书,也就是通过 TLS/SSL 协议,判定公钥是否有效,如果发现异常,会弹出警告框。如果校验没有问题,那么客户端会生成一个随机数,然后用上一步传输过来的公钥对随机数进行加密。步骤(5):客户端将上个步骤随机数加密后的内容传输给服务器端,这个随机数就是两端通信的核心。步骤(6):服务器端用自己的私钥进行解密,获取解密前的随机数。然后组装会话秘钥,这里私钥和客户端会话秘钥是相同的。步骤(7):将服务器端用私钥加密后的内容传输给客户端,在客户端用之前生成的随机数组装私钥还原。步骤(8):客户端用之前的私钥解密获取的信息,也就获取了通信内容。上述过程中,SSL 和 TLS 协议是核心模块,具体的证书交互流程相对复杂,面试场景基本不会涉及。我们需要关注的是为什么 HTTPS 同时使用非对称加密和对称加密,有两个原因:(1)对称加密流程两边需要使用相同的密钥,单纯使用对称加密,无法实现密钥交换。(2)非对称加密:满足安全要求,但是非对称加密的计算耗时高于对称加密的 2-3 个数量级(相同安全加密级别),对于实际的应用场景,例如电商网站,对网络交互高耗时容忍度是非常低的。所以 HTTPS 才先使用非对称交换密钥,之后再使用对称加密通信。

1. Scrapy Shell 介绍

Scrapy Shell 是一个交互终端,类似于 Python 交互式模式,它使我们可以在未启动 Scrapy 爬虫的情况下调试爬虫代码。在 Scrapy 的交互模式下,我们可以直接获取网页的 Response 结果,然后使用 XPath 或 CSS 表达式来获取网页元素,并以此测试我们获取网页数据的 Xpath 或者 CSS 表达式,确保后续执行时能正确得到数据。我们来看看如何进入 shell 模式,参考如下的视频:80在 Scrapy 框架中内置了 Selector 选择器,这个选择器是属于 parsel 模块的,而 parsel 模块是由 Scrapy 团队为解析网页而开发的,并且独立出来形成了一个第三方模块。这样我们可以在自己的爬虫程序中使用 parsel 模块而不必基于 Scrapy 框架 。我们从源码中来看看这个选择器类的定义。可以看到 Selector 类有我们熟悉的 xpath() 、css()、re() 以及 extract() 等方法,这些是我们解析网页的基础。Selector 类方法我们来思考一个问题,然后去源码中找到答案:(scrapy-test) [root@server ~]# scrapy shell https://gz.lianjia.com/ershoufang/ --nolog...>>> type(response)<class 'scrapy.http.response.html.HtmlResponse'>我们在 Scrapy Shell 中可以看到 response 是 HtmlResponse 的一个实例,它是怎么会有 Selector 的方法的?我们在前面的 Scrapy 初步的实例中看到过 response.xpath() 这样的用法,源码里面是怎么做的呢?这个问题比较简单,我们翻看一下源码就可以找到答案了。首先查看 HtmlResponse 类定义:# 源码位置:scrapy/http/response/html.pyfrom scrapy.http.response.text import TextResponseclass HtmlResponse(TextResponse): pass这个够不够简单?继续追看 TextResponse 的定义:# 源码位置:scrapy/http/response/text.py# ...class TextResponse(Response): # ... @property def selector(self): from scrapy.selector import Selector if self._cached_selector is None: self._cached_selector = Selector(self) return self._cached_selector # ... def xpath(self, query, **kwargs): return self.selector.xpath(query, **kwargs) def css(self, query): return self.selector.css(query) # ...是不是一下子就明白了?response.xpath() 正是调用的 scrapy.selector 下的Selector,继续看这个 Selector 的定义:# 源码位置:scrapy/selector/unified.pyfrom parsel import Selector as _ParselSelectorclass Selector(_ParselSelector, object_ref): # ...是不是最后又到了 parsel 下的 Selector?所以使用 response.xpath() 等价于使用 parsel 模块 下的 Selector 类中的 xpath 方法去定位网页元素。在给大家留一个更进一步的问题:在前面爬取互动出版网的 Scrapy 框架实例中,我们还用到了这样的表达式:book.xpath().extract()[0] 和 book.xpath().extract_first(),这样的代码执行过程又是怎样的呢(追踪到 parsel 这一层即可)?这个问题追踪的代码会比上面多一点,但也不复杂,这个问题会在下一节的 Reponse 类分析中给出相应的回答。

2. 利用 arcTo 方法绘制弧线

arcTo() 是利用两条相交切线来确定圆弧的位置,开始前我们先要搞懂切线的几个知识点。如何确定切线?我们都知道两点确定一条直线,这里两条直线相交处有一个交点,交点是两条线共用的一个点,所以我们只需要三个点就能确定两条切线。根据切线如何确定圆心?切线的性质有:(1)切线和圆只有一个公共点。(2)切线和圆心的距离等于圆的半径。(3)切线垂直于经过切点的半径。(4)经过圆心垂直于切线的直线必过切点。(5)经过切点垂直于切线的直线必过圆心。我们根据切线的垂直线必过圆心,即可确定圆心。我们来看一张图片:上图中,只要我们确定了 PA、PB 这两条切线和圆的半径 OA,即可确定 AB 这条弧线。上图中,我们沿着 OP 延长线移动 O 点的位置,即可得到半径不同的圆,也就得到了不同的 AB 弧线。到这里我们明白了:有三个点就可以确定两条切线。有圆的半径就可以确定切线间的一条弧线。arcTo 就是利用上面的原理来绘制弧线的。arcTo 方法有5个参数,前两个参数表示的是上图中 P 点的坐标,也就是切线的交点,第3个和第4个参数表示 PB 切线上的任意一个坐标点,第5个参数表示的是上图中 OA 的长度,也就是绘制圆的半径。特别注意:第3、4个参数表示的点不是切点!第3、4个参数表示的点不是切点!第3、4个参数表示的点不是切点!arcTo 方法的参数中只有两个点和一个半径,我们前面讲到要绘制弧线,必须是三个点,那第一个点哪儿去了呢?其实第一个点就是当前画布中笔触所在的位置,也就是当前画布中已经绘制的路径的终点。先看整体案例:1433运行结果:我们对上面的绘制弧线代码做拆分讲解:开始一个新路径。ctx.beginPath();确定第一个坐标点 A 点,A 点是当前已有路径的终点。ctx.moveTo(40,40);我换一个写法:ctx.moveTo(40,0);ctx.lineTo(80,40);这时候 A 点的位置就变成了 (80, 40)。 根据切线交点、第二条切线上的某个点和半径开始绘制弧线。ctx.arcTo(260,40, 260,200, 60); //调用了绘制圆的函数这里需要注意三点:(1) A 点和 PA 切线的切点会被自动连接起来,但是 PB 切线上的切点和 B点不会自动连接起来。 (2) A 点肯定在路径上,B 点不一定在路径上。(3) 切点由 canvas 自动计算。设置绘制样式以及开始描边。ctx.strokeStyle = "#456795";ctx.lineWidth = 4;ctx.stroke();我们从案例中可以看到,绘制一个圆形路径只需要调用一个函数即可,arc 方法和我们之前学过的 rect 绘制矩形的方法类似,也是绘制了一个路径,我们后续对路径的描边或者填充依然是需要调用 stroke 或者 fill 方法的。

2.3 地图布局

地理坐标系中,与地图布局相关的属性较多,包括:配置名类型默认值说明centerarray当前视图的中心点,用经纬度表示zoomnumber当前视图的缩放比例aspectScalenumber0.75地图长宽比boundingCoordsArray二维数组,定义定位的左上角、右下角对应的经纬度layoutCenterarray定义地图中心在屏幕中的位置layoutSizenumber|string定义地图大小leftnumber|string坐标系离容器左侧的距离topnumber|string坐标系离容器上方的距离rightnumber|string坐标系离容器右侧的距离bottomnumber|string坐标系离容器下方的距离widthnumber|string坐标系宽度heightnumber|string坐标系高度其中,left、top、right、bottom、width、height 是 echarts 中通用的定位手段,含义与配置方法都与其他组件一样。下面讨论列表中不太常见的属性。2.3.1 aspectScaleaspectScale 用于控制地图缩放的宽高比例。这个概念并不复杂,使用经纬度定义的地理信息本身带有宽高比例 aspect = width/height,那么渲染时若确定了地理坐标系的宽度为 x,则 y = x * aspect,形如:是不是跟我们平常看到的不一样?这是因为经纬坐标系是建立在地球的三维椭圆体上的,映射到二维平面时会产生一定的形变,所以绘制时需要在经纬度比例基础上加上椭圆形变,在 echarts 上则是通过 y = x * aspect * aspectScale 实现,通常保持默认值 0.75 即可:2.3.2 layoutCenter 与 layoutSizelayoutCenter、layoutSize 提供了另外一种布局方法,它们将地图坐标系调整为最长边等于 layoutSize`的盒子,并将盒子的中心点放置在 layoutCenter 位置上。基于 layoutCenter、layoutSize 的布局能够保持地图高宽比的情况下把地图放在某个盒形区域的正中间,并且保证不超出盒形的范围。使用上请注意:两者必须同时出现才有效;当配置了这两个属性后,left、top、right、bottom、width、height 均失效;layoutCenter、layoutSize 仅设定初始化时的布局效果,坐标系依然可以通过放大、移动变更位置和大小。例如:1319示例效果:2.3.3 center 与 boundingCoordscenter 用于定义当前视图的中心点,以经纬度坐标表示,形如: center: [-80, 30]; boundingCoords 则用于定义当前视图的左上角、右下角经纬度,以二维数组表示,形如:{ boundingCoords: [ // 定位左上角经纬度 [-90, 30], // 定位右下角经纬度 [-120, 50], ],}center 与 boundingCoords 互斥,当同时存在时,优先使用 center。从定义可以看出,center 定义的是地图所展示的中心区域的位置,配合 zoom 属性可以控制视图中展示的地图多寡,例如下例中:1320示例效果:而 boundingCoords 定义是地图坐标系所展现的整个内容区域的经纬度,使用时不需要考虑缩放比例,相对更简单。上例基础上,修改 geo:{ geo: { map: 'USA', roam: true, boundingCoords: [ // 定位左上角经纬度 [-90, 30], // 定位右下角经纬度 [-120, 50], ], },}示例效果:

7. 异常链

异常链是以一个异常对象为参数构造新的异常对象,新的异常对象将包含先前异常的信息。简单来说,就是将异常信息从底层传递给上层,逐层抛出,我们来看一个实例:public class ExceptionDemo5 { /** * 第一个自定义的静态内部异常类 */ static class FirstCustomException extends Exception { // 无参构造方法 public FirstCustomException() { super("第一个异常"); } } /** * 第二个自定义的静态内部异常类 */ static class SecondCustomException extends Exception { public SecondCustomException() { super("第二个异常"); } } /** * 第三个自定义的静态内部异常类 */ static class ThirdCustomException extends Exception { public ThirdCustomException() { super("第三个异常"); } } /** * 测试异常链静态方法1,直接抛出第一个自定义的静态内部异常类 * @throws FirstCustomException */ public static void f1() throws FirstCustomException { throw new FirstCustomException(); } /** * 测试异常链静态方法2,调用f1()方法,并抛出第二个自定义的静态内部异常类 * @throws SecondCustomException */ public static void f2() throws SecondCustomException { try { f1(); } catch (FirstCustomException e) { throw new SecondCustomException(); } } /** * 测试异常链静态方法3,调用f2()方法, 并抛出第三个自定义的静态内部异常类 * @throws ThirdCustomException */ public static void f3() throws ThirdCustomException { try { f2(); } catch (SecondCustomException e) { throw new ThirdCustomException(); } } public static void main(String[] args) throws ThirdCustomException { // 调用静态方法f3() f3(); }}运行结果:Exception in thread "main" ExceptionDemo5$ThirdCustomException: 第三个异常 at ExceptionDemo5.f3(ExceptionDemo5.java:46) at ExceptionDemo5.main(ExceptionDemo5.java:51)运行过程:通过运行结果,我们只获取到了静态方法 f3() 所抛出的异常堆栈信息,前面代码所抛出的异常并没有被显示。我们改写上面的代码,让异常信息以链条的方式 “连接” 起来。可以通过改写自定义异常的构造方法,来获取到之前异常的信息。实例如下:/** * @author colorful@TaleLin */public class ExceptionDemo6 { /** * 第一个自定义的静态内部异常类 */ static class FirstCustomException extends Exception { // 无参构造方法 public FirstCustomException() { super("第一个异常"); } } /** * 第二个自定义的静态内部异常类 */ static class SecondCustomException extends Exception { /** * 通过构造方法获取之前异常的信息 * @param cause 捕获到的异常对象 */ public SecondCustomException(Throwable cause) { super("第二个异常", cause); } } /** * 第三个自定义的静态内部异常类 */ static class ThirdCustomException extends Exception { /** * 通过构造方法获取之前异常的信息 * @param cause 捕获到的异常对象 */ public ThirdCustomException(Throwable cause) { super("第三个异常", cause); } } /** * 测试异常链静态方法1,直接抛出第一个自定义的静态内部异常类 * @throws FirstCustomException */ public static void f1() throws FirstCustomException { throw new FirstCustomException(); } /** * 测试异常链静态方法2,调用f1()方法,并抛出第二个自定义的静态内部异常类 * @throws SecondCustomException */ public static void f2() throws SecondCustomException { try { f1(); } catch (FirstCustomException e) { throw new SecondCustomException(e); } } /** * 测试异常链静态方法3,调用f2()方法, 并抛出第三个自定义的静态内部异常类 * @throws ThirdCustomException */ public static void f3() throws ThirdCustomException { try { f2(); } catch (SecondCustomException e) { throw new ThirdCustomException(e); } } public static void main(String[] args) throws ThirdCustomException { // 调用静态方法f3() f3(); }}运行结果:Exception in thread "main" ExceptionDemo6$ThirdCustomException: 第三个异常 at ExceptionDemo6.f3(ExceptionDemo6.java:74) at ExceptionDemo6.main(ExceptionDemo6.java:80)Caused by: ExceptionDemo6$SecondCustomException: 第二个异常 at ExceptionDemo6.f2(ExceptionDemo6.java:62) at ExceptionDemo6.f3(ExceptionDemo6.java:72) ... 1 moreCaused by: ExceptionDemo6$FirstCustomException: 第一个异常 at ExceptionDemo6.f1(ExceptionDemo6.java:51) at ExceptionDemo6.f2(ExceptionDemo6.java:60) ... 2 more运行过程:通过运行结果,我们看到,异常发生的整个过程都打印到了屏幕上,这就是一个异常链。

2. 常见的优化配置参数

首先 scrapy 框架有一个命令 (bench) 来帮助我们测试本地环境的效率,它会在本地创建一个 HTTP 服务器,并以最大可能的速度进行爬取,这个模拟的 Spider 只会做跟进连接操作,而不做其他处理。我们来实际看看这个命令的执行效果:(scrapy-test) [root@server qidian_yuepiao]# scrapy bench# ...2020-07-25 23:35:07 [scrapy.statscollectors] INFO: Dumping Scrapy stats:{'downloader/request_bytes': 127918, 'downloader/request_count': 278, 'downloader/request_method_count/GET': 278, 'downloader/response_bytes': 666962, 'downloader/response_count': 278, 'downloader/response_status_count/200': 278, 'elapsed_time_seconds': 11.300798, 'finish_reason': 'closespider_timeout', 'finish_time': datetime.datetime(2020, 7, 25, 15, 35, 7, 370135), 'log_count/INFO': 21, 'memusage/max': 48553984, 'memusage/startup': 48553984, 'request_depth_max': 12, 'response_received_count': 278, 'robotstxt/request_count': 1, 'robotstxt/response_count': 1, 'robotstxt/response_status_count/200': 1, 'scheduler/dequeued': 277, 'scheduler/dequeued/memory': 277, 'scheduler/enqueued': 5540, 'scheduler/enqueued/memory': 5540, 'start_time': datetime.datetime(2020, 7, 25, 15, 34, 56, 69337)}2020-07-25 23:35:07 [scrapy.core.engine] INFO: Spider closed (closespider_timeout)在上面的执行日志中,我们可以很清楚的看到该命令会搜索 settings.py 中的配置并打印项目的基本信息以及启用的扩展、下载中间件、Spider 中间件以及相应的 item pipelines。接下来是做的一些本地环境测试,测试显示的是每分钟平均能抓取1440个页面,当然实际的爬虫程序中需要有较多的处理,比如抽取页面数据、过滤、去重以及保存到数据库中,这些都是会消耗一定时间的。现在来介绍一下 settings.py 中比较常见的一个优化配置:并发控制:settings.py 中的 CONCURRENT_REQUESTS 参数用来确定请求的并发数,默认给的是16。而这个参数往往不适用于本地环境,我们需要进行调整。调整的方法是一开始设置一个比较大的值,比如100,然后进行测试,得到 Scrapy 的并发请求数与 CPU 使用率之间的关系,我们选择大概使得 CPU 使用率在 80%~90% 对应的并发数,这样能使得 Scrapy 爬虫充分利用 CPU 进行网页爬取;关闭 Cookie :这也是一个常见的优化策略。对于一些网站的请求,比如起点网、京东商城等, 不用登录都可以任意访问数据的,没有必要使用 Cookie,使用 Cookie 而会增加 Scrapy 爬虫的工作量。直接设置 COOKIES_ENABLED = False 即可关闭 Cookie;设置 Log 级别:将默认的 DEBUG 级别调整至 INFO 级别,减少不必要的日志打印;关闭重试:默认情况下 Scrapy 会对失败的请求进行重试,这种操作会减慢数据的爬取效率,因为对于海量的请求而言,丢失的数个甚至数百个请求都无关紧要;反而不必要的尝试会影响爬虫爬取效率;生产环境的做法最好是直接关闭,即 RETRY_ENABLED = False;减少下载超时时间:对于响应很慢的网站,在超时时间结束前,Scrapy 会持续等到响应返回,这样容易造成资源浪费。因此一个常见的优化策略是,减少超时时间,尽量让响应慢的请求释放资源。相应的参数设置示例如下:DOWNLOAD_TIMEOUT = 3关闭重定向:除非对重定向内容感兴趣,否则可以考虑关闭重定向。关闭操作 REDIRECT_ENABLED = False;自动调整爬虫负载:我们启用这个可以自动调节服务器负载的扩展程序,设置相关的参数,可以一定程度上优化爬虫的性能;AUTOTHROTTLE_ENABLED = False # 默认不启用,可以设置为True并调整下面相关参数AUTOTHROTTLE_DEBUG = FalseAUTOTHROTTLE_MAX_DELAY = 60.0AUTOTHROTTLE_START_DELAY = 5.0AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

2.3 read_csv() 函数的使用

我们这里自建了一个 pandasDataDemo.txt 数据文件,通过 Pandas 读取该文件数据,进行上述各项参数的详细讲解。1. 读取数据首先这里我们通过默认的文件读取类型,读取我们的 pandasDataDemo.txt 数据文件# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"data = pd.read_csv(data_path)print(data)# --- 输出结果 --- 书名 作者 出版日期 价格0 python从入门到实战 埃里克 2020 851 python数据分析 丹尼尔 2020 802 python爬虫技术 李宁 2020 793 疯狂python讲义 李刚 2019 1134 大数据处理 石宣化 2018 435 人工智能 史蒂芬 2018 976 深度学习 伊恩 2017 1527 人工智能算法 杰弗瑞 2020 538 人工智能简史 尼克 2017 24可以看到输出结果数据内容和我们 TXT 中的数据一样,在数据的格式上进行了行和列的解析,并自动生成了行索引 0-8,共 9 行数据。2. 参数 sepsep 参数的作用是指定数据的分隔符,默认是 “,”。我们首先将 pandasDataDemo.txt 中的数据列改成以 “=” 进行分割:书名=作者=出版日期=价格python从入门到实战=埃里克=2020=85python数据分析=丹尼尔=2020=80python爬虫技术=李宁=2020=79疯狂python讲义=李刚=2019=113大数据处理=石宣化=2018=43人工智能=史蒂芬=2018=97深度学习=伊恩=2017=152人工智能算法=杰弗瑞=2020=53人工智能简史=尼克=2017=24接下来我们通过 sep 设置解析列的分隔符:# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 sepdata = pd.read_csv(data_path,sep="=")print(data)# --- 输出结果 --- 书名 作者 出版日期 价格0 python从入门到实战 埃里克 2020 851 python数据分析 丹尼尔 2020 802 python爬虫技术 李宁 2020 793 疯狂python讲义 李刚 2019 1134 大数据处理 石宣化 2018 435 人工智能 史蒂芬 2018 976 深度学习 伊恩 2017 1527 人工智能算法 杰弗瑞 2020 538 人工智能简史 尼克 2017 24通过指定 sep 参数将数据以 “=” 进行各列的分割。3. 参数 header指定数据的解析,从哪一行开始,指定的这一行将默认的作为列索引。# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 header,指定从第4行开始解析data = pd.read_csv(data_path,sep="=",header=3)print(data)# --- 输出结果 --- python爬虫技术 李宁 2020 79 #该行数据在源文件中是 第4行0 疯狂python讲义 李刚 2019 1131 大数据处理 石宣化 2018 432 人工智能 史蒂芬 2018 973 深度学习 伊恩 2017 1524 人工智能算法 杰弗瑞 2020 535 人工智能简史 尼克 2017 24输出解析:这里可以看到,我们参数 header 传的是 3,输出结果的第一行为“python 爬虫技术 李宁 2020 79”,在源数据中是第4行数据,因为 Pandas 解析数据的行数是从0下标开始的,并且将第4行默认作为列索引的值。如果不使用数据中的某行作为列名,要声明 header=None ,Pandas 会默认以数字编号为各列名称。# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 header=Nonedata = pd.read_csv(data_path,sep="=",header=None)print(data)# --- 输出结果 --- 0 1 2 30 书名 作者 出版日期 价格1 python从入门到实战 埃里克 2020 852 python数据分析 丹尼尔 2020 803 python爬虫技术 李宁 2020 794 疯狂python讲义 李刚 2019 1135 大数据处理 石宣化 2018 436 人工智能 史蒂芬 2018 977 深度学习 伊恩 2017 1528 人工智能算法 杰弗瑞 2020 539 人工智能简史 尼克 2017 244. 参数 names通过该参数,我们可以为解析的数据,添加列索引值。# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 names,传入列名称data = pd.read_csv(data_path,sep="=",header=None,names=["AA","BB","CC","DD"])print(data)# --- 输出结果 --- AA BB CC DD0 书名 作者 出版日期 价格1 python从入门到实战 埃里克 2020 852 python数据分析 丹尼尔 2020 803 python爬虫技术 李宁 2020 794 疯狂python讲义 李刚 2019 1135 大数据处理 石宣化 2018 436 人工智能 史蒂芬 2018 977 深度学习 伊恩 2017 1528 人工智能算法 杰弗瑞 2020 539 人工智能简史 尼克 2017 24输出解析:通指定 names 的值,我们看到输出中列名称已经变成我们指定的 AA,BB,CC,DD了。5. 参数 nrows指定解析数据的行数:# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 nrowsdata = pd.read_csv(data_path,sep="=",nrows=3)print(data)# --- 输出结果 --- 书名 作者 出版日期 价格0 python从入门到实战 埃里克 2020 851 python数据分析 丹尼尔 2020 802 python爬虫技术 李宁 2020 79输出解析:这里可以看到输出结果,包含了数据文件中的四行,而不是 3 行数据,这是因为默认的数据的第一行解析为列索引后,然后再进行 3 行数据的解析。Tips:read_csv() 函数解析数据的逻辑为,根据里面参数设置,逐行的去解析文件中的数据,如果不指定 names,会先把第一行的数据解析为 列索引,然后再去根据条件,继续向下解析。下面我们通过设置 names 的值(或者设置 names=None),并传入解析的 nrows 数量,看一下输出结果:import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 nrows 和 namesdata = pd.read_csv(data_path,sep="=",nrows=3,names=["AA","BB","CC","DD"])print(data)# --- 输出结果 --- AA BB CC DD0 书名 作者 出版日期 价格1 python从入门到实战 埃里克 2020 852 python数据分析 丹尼尔 2020 80输出解析:可以看到这里的 3 行正是我们数据文件中的前三行数据。6. 参数 skiprows解析数据,从数据开始忽略多少行开始解析:# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 skiprowsdata = pd.read_csv(data_path,sep="=",skiprows=5)print(data)# --- 输出结果 --- 大数据处理 石宣化 2018 430 人工智能 史蒂芬 2018 971 深度学习 伊恩 2017 1522 人工智能算法 杰弗瑞 2020 533 人工智能简史 尼克 2017 24输出解析:这里可以看到数据在忽略了 5 行之后,将第 6 行数据解析为列名称,继续向下进行数据的解析。7. 参数 skipfooter ,encoding ,engineskipfooter 参数:解析数据,忽略从后向前说多少条的数据行;encoding 参数:制定解析的编码方式;engine 参数:指定解析的引擎类型;# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 skiprowsdata = pd.read_csv(data_path,sep="=",skipfooter=7)print(data)# --- 输出结果 --- 涔﹀悕 浣滆�� 鍑虹増鏃ユ湡 浠锋牸0 python浠庡叆闂ㄥ埌瀹炴垬 鍩冮噷鍏� 2020 851 python鏁版嵁鍒嗘瀽 涓瑰凹灏� 2020 80<ipython-input-7-44f2a64e80bb>:6: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support skipfooter; you can avoid this warning by specifying engine='python'. data = pd.read_csv(data_path,sep="=",skipfooter=7)输出解析:这里可以看到输出结果存在的问题,首先根据提示可以看出是 c 引擎不支持 skipfooter 的解析;其实是存在中文乱码的问题,为了解决这两个问题,我们通过 encoding 和 engine 分别制定编码方式和引擎类型。# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 skiprows,engine,encodingdata = pd.read_csv(data_path,sep="=",skipfooter=7, engine='python',encoding='utf-8')print(data)# --- 输出结果 --- 书名 作者 出版日期 价格0 python从入门到实战 埃里克 2020 851 python数据分析 丹尼尔 2020 80输出解析:这里我们通过指定编码 encoding=‘utf-8’ 和解析引擎 engine=‘python’ ,可以看到修复了上面存在的问题,并且看到结果是忽略了数据的后7行,只解析了前 3 行数据。Tips:所谓引擎,最通俗的理解就是动力的来源,我们这里提到的 C 引擎和 Python 引擎,主要是指在 Pandas 解析数据时,解析函数最底层主要运行程序的编写语言,在使用这两个解析器引擎时,C引擎的速度更快,但是 Python 引擎的功能更多更齐全。8. 参数 na_filter该参数可配置解析文件时是否检查(空字符串或者是空值),如果一个文件比较大的话,指定 na_filter 能有效的提高解析数据的速度:首先我们将源数据最后两行的作者这一列删掉内容,如下所示:书名=作者=出版日期=价格python从入门到实战=埃里克=2020=85python数据分析=丹尼尔=2020=80python爬虫技术=李宁=2020=79疯狂python讲义=李刚=2019=113大数据处理=石宣化=2018=43人工智能=史蒂芬=2018=97深度学习=伊恩=2017=152人工智能算法==2020=53人工智能简史==2017=24针对缺失值,read_csv() 函数默认是将缺失值解析后,展示为 NaN ,这里我们看一下解析出来的数据结果:# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"data = pd.read_csv(data_path,sep="=", engine='python',encoding='utf-8')print(data)# ---输出结果--- 书名 作者 出版日期 价格0 python从入门到实战 埃里克 2020 851 python数据分析 丹尼尔 2020 802 python爬虫技术 李宁 2020 793 疯狂python讲义 李刚 2019 1134 大数据处理 石宣化 2018 435 人工智能 史蒂芬 2018 976 深度学习 伊恩 2017 1527 人工智能算法 NaN 2020 538 人工智能简史 NaN 2017 24 # 这里看到最后良好的作者都解析为 NaN下面我们在 read_csv() 函数中,加入 na_filter=False属性:# 导入pandas包import pandas as pd# 指定导入的文件地址 默认是file,这里的路径中省略了 file:/data_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/pandasDataDemo.txt"# 这里我们传入参数 na_filterdata = pd.read_csv(data_path,sep="=",na_filter=False, engine='python',encoding='utf-8')print(data)# ---输出结果--- 书名 作者 出版日期 价格0 python从入门到实战 埃里克 2020 851 python数据分析 丹尼尔 2020 802 python爬虫技术 李宁 2020 793 疯狂python讲义 李刚 2019 1134 大数据处理 石宣化 2018 435 人工智能 史蒂芬 2018 976 深度学习 伊恩 2017 1527 人工智能算法 2020 538 人工智能简史 2017 24输出解析:这里可以看到,通过 na_filter=False 参数指定后,read_csv() 函数将不对缺失值进行解析。

2.2 分层定义以及常见协议

在画出了计算机网络的分层模型之后,我们还需要向面试官解释每一层的定义以及介绍常见的协议。2.2.1 应用层应用层(Application Layer)是 5 层协议的顶层,顾名思义,应用层的作用是通过操作系统中应用进程(例如电子邮件、浏览器文件传输)提供网络交互。应用层最常被问到的是 HTTP 协议和 DNS 域名解析协议(在之后的小节我们会详细讲解相关题目),其次还有一些后端开发过程中可能会接触的协议,例如支持文件传输的 FTP 协议(例如需要从 Windows 开发机传输文件到 Linux 服务器时使用),以及支持电子邮件的 SMTP 协议(例如需要开发电子邮件读写的相关爬虫时需要开放邮箱的 SMTP 协议)。2.2.2 传输层传输层(Transport Layer)主要是为了实现端口到端口(port to port)的通信,计算机的不同进程都会被分配不同的端口,例如域名默认的 80 端口。从接收和发送信息的角度可以分为两大功能:复用:把操作系统的多个进程利用一个传输层接口发送信息;分用:把收到的信息利用传输层接口分发到操作系统的不同进程。传输层涉及到两个常见的协议,几乎是面试必考协议:传输控制协议(TCP,Transmission Control Protocol):特点是面向连接,基于报文段传输,能够保证消息可靠交付的协议;用户数据包协议(UDP,User Datagram Protocol):特点是无连接,基于用户数据报传输,不保证消息可靠交付,只尽 "最大努力交付"。2.2.3 网络层计算机之间的通信可以分为位于同一个子网络(也就是局域网,Local Area Network)和位于不同的子网络(广域网,Wide Area Network),网络层协议解决的问题就是如何判断两台计算机是否属于同一个子网络中。网络层最常涉及的协议是 IP 协议 ,就是 TCP/IP 协议族中的 IP 网络协议,可见其重要性。此外,还有和 IP 协议相关的 ARP(Address Resolution Protocol,地址解析协议),以太网的数据传输最直接依赖的是 MAC 地址,ARP 协议的作用就是将 IP 地址转换为 MAC 地址。2.2.4 数据链路层数据链路层(Data-Link Layer)位于物理层和网络层之间,对于两个不同主机之间的数据传输,可能会经过多个路由器中转,中间的这条链路就是我们关注的重点,我们把两个主机抽象为两个点,链路层协议解决的问题就是 "点对点" 的数据传输。数据链路层将网络层交付的 IP 数据包封装成帧(Frame),其中每一帧包括了数据以及必要的控制信息(比如同步信息、寻址信息、差错控制信息),这种设计方案非常类似 TCP 协议中的控制位(由此也能看出计算机网络设计的互通性)。如果通过差错控制信息校验出了错误,那么就会在本层丢弃这个帧,纠正错误是通过网络层的 TCP 协议完成。PPP 协议(Point to Point Protocol):在两个点之间传输数据包的协议,因为本层涉及的协议在面试中考察甚少,基本可以只做简单了解。2.2.5 物理层物理层(Physical Layer)是 5 层协议模型中最底层的协议,就是通过物理手段(例如网线,电缆)将计算机连接起来,提供信息传输的物理媒介,数据由 0 和 1 二进制信号构成,传输单元是比特位。因为关于物理层的研究更偏向于通信相关的原理,我们只需要了解本层的定义即可。

直播
查看课程详情
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号