2024年7月

0 引言

  • 在近期项目一场景中,一 Web API (响应内容:7MB - 40MB、数据项:5W-20W条)的网络传输耗时较大,短则 5s,长则高达25s,前端渲染又需要耗时 9s-60s。
  • 在这个场景中,前端的问题暂且不表。那么针对后端的问题,个人认为还是有较大的优化空间:
    • 1)启用HTTP 内容压缩策略 【最重要】
    • 2)调整数据结构(参考JDBC,
      fields:list<string>
      +
      values:list<list<object>>
      ,分2个独立的数据集,数据集内的object的属性值保证有序,减少了
      object
      内各字段名称的反复描述,以此降低网络带宽) 【次之】

  • 此处,主要探讨分析、实施
    HTTP 内容压缩策略
    中重点需要关注的 HTTP Response Header:


    • Content-Length :(如果启用压缩,压缩后的)内容长度
    • Transfer-Encoding : 传输编码
    • Content-Encoding : 内容编码
  • 正式讲之前,想讲讲结论、效果:

  • 响应内容 7119kb --> 633kb,缩减网络带宽约 90%
  • 响应耗时:6.12s --> 300ms,提升响应耗时约 95 %

1 概述篇

1.1 Transfer-Encoding

  • Transfer-Encoding
    ,是一个 HTTP 头部字段,字面意思是「
    传输编码
    」。
  • Transfer-Encoding 则是用来改变报文格式,它不但不会减少实体内容传输大小,甚至还会使传输变大,那它的作用是什么呢?本文接下来主要就是讲这个。我们先记住一点,Content-Encoding 和 Transfer-Encoding 二者是相辅相成的,对于一个 HTTP 报文,很可能同时进行了内容编码和传输编码。

1.2 Content-Encoding

  • 实际上,
    HTTP 协议
    中还有另外一个头部与编码有关:
    Content-Encoding(内容编码)
  • Content-Encoding
    通常用于对
    实体内容
    进行
    压缩编码
    ,目的是
    优化传输
    。例如,用 gzip 压缩文本文件,能大幅减小体积。
  • 内容编码
    通常是
    选择性的
    ,例如 jpg / png 这类文件一般不开启,因为图片格式已经是高度压缩过的,再压一遍没什么效果不说还浪费 CPU。
Content-Encoding
Transfer-Encoding 值
描述
gzip (推荐) 表明实体采用 GNU zip 编码
compress 表明实体采用 Unix 的文件压缩程序
deflate 表明实体是用 zlib 的格式压缩的
br (推荐) 指示响应数据采用Brotli压缩编码。
示例:Transfer-Encoding: br / Content-Encoding: br
identity 表明没有对实体进行编码。当没有 Content-Encoding 首部时,就默认为这种情况

HTTP 定义了一些标准的内容编码类型,并允许用扩展编码的形式增添更多的编码。
由互联网号码分配机构(IANA)对各种编码进行标准化,它给每个内容编码算法分配了唯一的代号。
Content-Encoding 首部就用这些标准化的代号来说明编码时使用的算法。

gzip、compress 以及 deflate 编码都是无损压缩算法,用于减少传输报文的大小,不 会导致信息损失。
这些算法中,gzip 通常是效率最高的,使用最为广泛。

1.3 Accept-Encoding : 客户端声明可接受的编码

  • Accept-Encoding
    字段包含用逗号分隔的支持编码的列表,下面是一些例子:
Accept-Encoding: compress, gzip
Accept-Encoding:
Accept-Encoding: *
Accept-Encoding: compress;q=0.5, gzip;q=1.0
Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0
  • 客户端可以给每种编码附带 Q(质量)值参数来说明编码的优先级。Q 值的范围从 0.0 到 1.0,0.0 说明客户端不想接受所说明的编码,1.0 则表明最希望使用的编码。

1.4 Persistent Connection(持久连接)

  • 暂时把
    Transfer-Encoding
    放一边,我们来看
    HTTP 协议
    中另外一个重要概念:
    Persistent Connection

    持久连接
    ,通俗说法
    长连接
    )。
  • 我们知道
    HTTP
    运行在
    TCP 连接
    之上,自然也有着跟 TCP 一样的
    三次握手

    慢启动
    等特性,为了尽可能的提高 HTTP 性能,使用
    持久连接
    就显得尤为重要了。为此,HTTP 协议引入了相应的机制。
    • HTTP/1.0

      持久连接机制
      是后来才引入的,通过
      Connection: keep-alive
      这个头部来实现,
      服务端

      客户端
      都可以使用它告诉对方在
      发送完数据之后不需要断开 TCP 连接,以备后用
    • HTTP/1.1
      则规定
      所有连接都必须是持久的
      ,除非显式地在头部加上
      Connection: close

      • 所以,实际上,
        HTTP/1.1

        Connection
        这个头部字段已经没有 keep-alive 这个取值了,但由于历史原因,很多 Web Server 和浏览器,还是保留着给
        HTTP/1.1
        长连接发送
        Connection: keep-alive
        的习惯。

1.5 Content-Length : 告诉浏览器(编码后)响应实体的长度

没有 Content-Length 时 :
pending

  • 浏览器重用已经打开的空闲持久连接,可以避开缓慢的
    三次握手
    ,还可以避免遇上
    TCP 慢启动的拥塞适应阶段
    ,听起来十分美妙。为了深入研究
    持久连接
    的特性,我决定用 Node 写一个最简单的 Web Server 用于测试,Node 提供了
    http
    模块用于快速创建 HTTP Web Server,但我需要更多的控制,所以用
    net
    模块创建了一个 TCP Server:
require('net')
	.createServer(function(sock) { 
		sock.on('data', function(data) { 
			sock.write('HTTP/1.1 200 OK\r\n'); 
			sock.write('\r\n'); 
			sock.write('hello world!'); 
			sock.destroy(); 
		}); 
	}).listen(9090, '127.0.0.1');

启动服务后,在浏览器里访问 127.0.0.1:9090,正确输出了指定内容,一切正常。去掉
sock.destroy()
这一行,让它变成
持久连接
,重启服务后再访问一下。这次的结果就有点奇怪了:迟迟看不到输出,通过 Network 查看请求状态,一直是
pending

这是因为,对于
非持久连接
,浏览器可以通过
连接
是否关闭来界定请求或响应实体的边界;而对于
持久连接
,这种方法显然不奏效。上例中,尽管我已经发送完所有数据,但浏览器并不知道这一点,它无法得知这个打开的连接上是否还会有新数据进来,只能傻傻地等了。

引入 Content-Length 后 :如实给浏览器反馈响应内容实体的长度

要解决上面这个问题,最容易想到的办法就是
计算内容实体长度
,并通过头部告诉对方。这就要用到
Content-Length
了,改造一下上面的例子:

require('net')
	.createServer(function(sock) { 
		sock.on('data', function(data) { 
			sock.write('HTTP/1.1 200 OK\r\n'); 
			sock.write('Content-Length: 12\r\n');
			sock.write('\r\n'); 
			sock.write('hello world!'); 
			sock.destroy(); 
		}); 
	}).listen(9090, '127.0.0.1');

可以看到,这次发送完数据、并没有关闭 TCP 连接,但浏览器能正常输出内容、并结束请求,因为浏览器可以通过
Content-Length
的长度信息,判断出
响应实体已结束
。那如果 Content-Length 和实体实际长度不一致会怎样?有兴趣的同学可以自己试试,通常如果
Content-Length
比实际长度短,会造成
内容被截断
;如果比实体内容长,会造成
pending

由于
Content-Length
字段必须
真实反映实体长度
,但
实际应用
中,有些时候实体长度并没那么好获得,例如实体来自于
网络文件
,或者由
动态程序
生成。这时候要想准确获取长度,只能开一个足够大的 buffer,等内容全部生成好再计算。但这样做一方面需要更大的
内存开销
,另一方面也可能会让客户端等更久。

1.6 Transfer-Encoding: chunked(传输编码 = 分块传输) :不依赖头部的长度信息,也能知道实体的边界

TTFB (Time To First Byte)

  • 我们在做
    WEB 性能优化
    时,有一个重要的指标叫
    TTFB

    Time To First Byte
    ),它代表的是
    从客户端发出请求到收到响应的第一个字节所花费的时间
  • 大部分浏览器自带的 Network 面板都可以看到这个指标(如
    Chrome - Network - a request - Timing - Waiting for server response
    ),越短的
    TTFB
    意味着用户可以越早看到页面内容,体验越好。可想而知,服务端为了计算响应实体长度而缓存所有内容,跟更短的 TTFB 理念背道而驰。
  • 但在 HTTP 报文中,实体一定要在头部之后,顺序不能颠倒,为此我们需要一个新的机制:
    不依赖头部的长度信息,也能知道实体的边界

Transfer-Encoding : 不依赖头部的长度信息,也能知道实体的边界

  • 本文主角终于再次出现了,
    Transfer-Encoding
    正是用来解决上面这个问题的。历史上
    Transfer-Encoding
    可以有多种取值,为此还引入了一个名为
    TE
    的头部用来协商采用何种传输编码。但是最新的 HTTP 规范里,只定义了一种传输编码:分块编码(chunked)。

  • 分块编码
    相当简单,在头部加入
    Transfer-Encoding: chunked
    之后,就代表这个报文采用了分块编码。这时,
    报文中的响应实体
    需要改为用一系列
    分块
    来传输。每个
    分块
    包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的
    CRLF
    (\r\n),也不包括
    分块数据
    结尾的
    CRLF
    。最后一个分块长度值必须为 0,对应的
    分块数据
    没有内容,表示
    实体结束

  • 按照这个格式改造下之前的代码:

require('net').createServer(function(sock) {
    sock.on('data', function(data) {
        sock.write('HTTP/1.1 200 OK\r\n');
        sock.write('Transfer-Encoding: chunked\r\n');
        sock.write('\r\n');

        sock.write('b\r\n');
        sock.write('01234567890\r\n');

        sock.write('5\r\n');
        sock.write('12345\r\n');

        sock.write('0\r\n');
        sock.write('\r\n');
    });
}).listen(9090, '127.0.0.1');

上面这个例子中,我在响应头中表明接下来的实体会采用分块编码,然后输出了 11 字节的分块,接着又输出了 5 字节的分块,最后用一个 0 长度的分块表明数据已经传完了。用浏览器访问这个服务,可以得到正确结果。可以看到,通过这种简单的分块策略,很好的解决了前面提出的问题。

前面说过
Content-Encoding

Transfer-Encoding
二者经常会结合来用,其实就是针对
Transfer-Encoding
的分块再进行
Content-Encoding

下面是用 telnet 请求测试页面得到的响应,就对分块内容进行了 gzip 编码:

> telnet 106.187.88.156 80

GET /test.php HTTP/1.1
Host: qgy18.qgy18.com
Accept-Encoding: gzip

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 03 May 2015 17:25:23 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Content-Encoding: gzip

1f
�H���W(�/�I�J

0

用 HTTP 抓包神器 Fiddler 也可以看到类似结果,有兴趣的同学可以自己试一下。

Transfer-Encoding: chunked 与 Content-Length

  • Transfer-Encoding: chunked

    Content-Length
    同为
    头部字段
    ,它们
    不会同时出现在头部中
  • 当使用
    分块传输
    时,头部将出现
    Transfer-Encoding: chunked
    ,而
    不再包含Content-Length字段
    ,即使强行设定该字段,也会被忽略。

在HTTP中,我们通常依赖 HttpCode/HttpStatus 来判断一个 HTTP 请求是否成功,如:

HTTP: Status 200 – 成功,服务器成功返回网页
HTTP: Status 304 – 成功,网页未修改
HTTP: Status 404 – 失败,请求的网页不存在
HTTP: Status 503 – 失败,服务不可用

… …

延伸:开发人员的程序发起HTTP请求时,判断 HTTP 请求是否成功场景 (可选读章节)

但开发人员有时候也会有令人意外的想象力。我们的一部分开发人员决定使用
Content-Length
来判断 HTTP 请求是否成功,当
Content-Length
的值小于等于0或者为 162 时,认为请求失败。


Content-Length
的值小于等于0时认为http请求失败还好理解,因为开发人员错误地以为 HTTP 响应头中一定会包含 Content-Length 字段。

为什么当
Content-Length
的值为162时,也认为请求失败呢。这是因为公司服务器的404页面的长度恰好是162。惊不惊喜,意不意外!

2 Web Server 配置篇

  • 启用压缩等配置

Nginx

gzip on;
gzip_min_length 1k;

gzip_buffers 4 16k;

#gzip_http_version 1.0;

gzip_comp_level 2;

gzip_types text/xml text/plain text/css text/js application/javascript application/json;

gzip_vary on;

gzip_disable "MSIE [1-6]\.";
  • 当在 Nginx 的配置文件
    nginx.conf

    location
    等位置配置以上内容时,Nginx 服务器将对指定的文件类型开启压缩 (gzip)以优化传输,减少传输量。

  • 分块传输
    可以将
    压缩对象
    分为多个部分,在这种情况下,资源整个进行压缩,压缩的输出分块传输。在压缩的情形中,
    分块传输
    有利于
    一边进行压缩一边发送数据

    而不是先完成压缩过程
    ,得知压缩后数据的大小之后再进行传输,从而使得用户能够更快地接受到数据,
    TTFB
    指标更好。

  • 对于开启了
    gzip
    的传输,报文的头部将增加
    Content-Encoding: gzip
    来标记传输内容的编码方式。同时,Nginx 服务器
    默认
    就会对
    压缩内容
    进行
    分块传输
    ,而无须显式开启
    chunked_transfer_encoding

  • Nginx 中如何
    关闭分块传输
    呢,在 Nginx 配置文件
    location
    段中加一行“
    chunked_transfer_encoding off;
    ”即可。

location / {
    chunked_transfer_encoding       off;
}

SpringBoot(Embed Tomcat)

  • SpringBoot 默认是
    不开启 gzip 压缩
    的,需要我们手动开启,在配置文件中添加两行
server: 
  compression: 
    enabled: true 
    mime-types: application/json,application/xml,text/html,text/plain,text/css,application/x-javascript
  • 注意:上面配置中的
    mime-types
    ,在 spring2.0+的版本中,默认值如下,所以一般我们不需要特意添加这个配置
// org.springframework.boot.web.server.Compression#mimeTypes
/**
 * Comma-separated list of MIME types that should be compressed.
 */
private String[] mimeTypes = new String[] { "text/html", "text/xml", "text/plain",
		"text/css", "text/javascript", "application/javascript", "application/json",
		"application/xml" };
  • 虽然加上了上面的配置,
    开启了 gzip 压缩
    ,但是需要注意
    并不是说所有的接口都会使用 gzip 压缩
    ,默认情况下,
    仅会压缩 2048 字节以上的内容
  • 如果我们需要修改这个值,通过修改配置即可
server:
  compression:
    min-response-size: 1024

Tomcat

  • Tomcat5.0以上。 修改
    %TOMCAT_HOME%/conf/server.xml
<Connector port="8080"
  protocol="HTTP/1.1"
  connectionTimeout="20000"
  redirectPort="8443"
  compression="on"
  compressionMinSize="2048"
  noCompressionUserAgents="gozilla, traviata"
  compressableMimeType="text/html,text/xml,text/javascript,
application/javascript,text/css,text/plain,text/json"/>
  • compression="on"
    开启压缩。可选值:"on"开启,"off"关闭,"force"任何情况都开启。
  • compressionMinSize="2048"
    大于2KB的文件才进行压缩。用于指定压缩的最小数据大小,单位B,默认2048B。注意此值的大小,如果配置不合理,产生的后果是小文件压缩后反而变大了,达不到预想的效果。
  • `noCompressionUserAgents="gozilla, traviata",对于这两种浏览器,不进行压缩(我也不知道这两种浏览器是啥,百度上没找到),其值为正则表达式,匹配的UA将不会被压缩,默认空。
  • compressableMimeType="text/html,text/xml,application/javascript,text/css,text/plain,text/json"
    会被压缩的MIME类型列表,多个逗号隔,表明支持html、xml、js、css、json等文件格式的压缩(plain为无格式的,但对于具体是什么,我比较概念模糊)。compressableMimeType很重要,它用来告知tomcat要对哪一种文件进行压缩,如果类型指定错误了,肯定是无法压缩的。那么,如何知道要压缩的文件类型呢?可以通过以下这种方法找到。

X 参考文献

  • 怀疑1:滑动窗持续收缩,导致后面接收效率急剧下降
  • 怀疑2:拥塞窗口cwnd,会不会是发送端因为乱序或超时导致服务器当前链路的cwnd下降而主动降速
  • 怀疑3:数据包乱序,或丢包
  • 怀疑4(结论):客户端的的问题(Web 前端)。

问题请求在浏览器除首页的其他场景、或着使用其他客户端直接请求下载速度都是正常的,出问题的那次请求又是预加载的请求(同时,还会有好几个请求会被一起发送),所以乍一看总会觉得是网络方面的问题,当然这个上文中的内容已经证明了,完全不是网络的问题

sd基础

工作原理&入门


输入提示词后 有文本编码器将提示词
编译成特征向量

vae编码器
将特征向量传入
潜空间内
,特征向量在潜空间内不断降噪,最后通过
vae解码器
将降噪之后的特征向量 解码成一个个像素组成的图片

一般选中默认vae模型

解码编码的模型


CLIP值越大,提前停止的越快,我们提示词被数字化的层数越少,提示词的相关性越小。反之越小越能丰富提示词
CLIP终止层数一般为2

其他功能

1.Hires.fix 高清修复
2.Refiner 当渲染到80% 切换另一个模型渲染
3.CFG 一般为5-10 越小ai越自由,越高越靠近提示词
4.随机种子 提示词一样,随机种子一样则可以在不同电脑生成相同图片
5.迭代步数越高 20-30 图片质量越高,步数过高则失真,且消耗更多时间,有时候还没有效果

ADetailer 修复人物的脸
采样方法 DPM++2M

网站,模型推荐

liblib NovelAI hugingface promlib civitai github
majicmix dreamshaper primemix architectrealmix

提示词 语法

英文和英文的标题符号
权重
数字越高权重越高,画面着重描述什么

[cat]=(cat:0.9)
(cat)=(cat:1.1)
{cat}=(cat:1.05)
[[cat]]=(cat:0.9x0.9)=(cat:0.81)
((cat))=(cat:1.1x1.1)=(cat:1.21)
ctrl + 上箭头 可以快捷调节  权重多低都行,过高则不行,会失真

短句与长句

一个一个词的拼写,而不是一句话呢
一个个词组会更准确,而且好调整权重
提示词控制在75个以内,正反向一样,不超过75

起手式

正向 4k masterpiece 会让图片更加精美
反向则用 text blur之类
有修饰词 

提示词顺序,越靠前权重越高

no.1 画质词/画风词
no.2 主题 one girl
no.3 环境/场景/构图
no.4 lora

提示词污染
1girl,blue dress,pink hair,green umbrella,
1girl,blue dress,red hair,puple umbrella,

防止提示词尤其是颜色相互渗透 使用break隔开

提示词融合

1girl,cat  猫在女孩身上
1girl And cat 猫娘 (1girl_cat 有同样效果)
[cat|dog] 也有融合效果
{forest:1girl:0.3} 在30%的时候结束画forest
{forest:1girl:0.7} 在70%的时候结束画forest

图生图&高清修复

通过图片加提示词生成结果


使用预设起手式,并添加进提示词


通过插件来快速选择自己想要的提示词

masterpiece,best quality,1girl,police,glamor,in summer,street,

将参考图拖入图生图并增加提示词
masterpiece,best quality,1girl,police,glamor,in summer,street,coat,
增加提示词coat,通过原图再次生图

图生图重绘幅度,不过高也不过低 0.3-0.5结果图与参考图之间差距不会太大
                            0.5-0.7赋予ai更多想象空间
                            低于0.3 或大于 0.7则扭曲变形

局部重绘,增加sunglasses提示词


upscale 二次元

GAN 4X Anime6B 适合动漫放大 (Gan生成式对抗网络简称)
重绘幅度 0.3-0.5安全区间 0.5-0.7ai自由领域
放大倍数x2 512 变为1024

文生图界面


文生图界面的放大需要锁定种子


再次点击小图标来到
图生图
,再次放大


无需锁定种子

再次放大 模型放大


从512缩放到1024

controlnet

风格转换 softedge

最开始能通过线条处理还原参考图的只有canny
canny 硬边缘 canny将参考图通过细线勾勒出来 架构基于相邻像素计算差值,死板,图片会出现莫名奇妙的元素

softedge
全能模型 将主体勾勒

开启插件controlnet

pidinet 与 hed
hed 保留图片更多细节,完整性好
建筑,场景
pidinet 能够更好保留主体,忽视细节
人物


使用真实系的模型,将二次元图片好好描述

线稿上色 Lineart

lineart多用于线稿上色动漫类图像处理
mj 生成线稿


点击爆炸按钮后,会有去下载插件,如控制台显示git网络失败,手动去下载到相应目录解压


最好宽高比 和原来图片一样 也就是宽度和高度

controlnet-2

openpose 姿态管理


姿态成功控制


dw_openpose_full效果最好
根据参考图图片高宽一致

depth [空间关系]

场景 比如教堂
softedge + depth
控制线条分布 + 深度

depth

预处理用了hedsafe
softedge
负面提示词这里因为是建筑,所以去除finger有关

可以通过提示词来改变教堂颜色

人物
softedge openpose depth ipadapter
线条分布 绑定骨骼 空间关系 面部特征迁移/风格一致性

tile

参考图模糊,再分区块重采样 结果图细节更丰富

可以将图片拖入controlnet的单张图片,再次tile。可以看到叶子的纹路

controlnet Ipadapter

ipadapter 可以去hugingface里下载,根据后缀放在指定目录
换脸
材质迁移
风格迁移

换脸


生成一张图片,为参考图


将参考图拖进ipadapter

写实换脸,上传了自己的图片

材质迁移

ipdapter

softedge

depth

midas也不错

材质很重要,如果用下图材质则使用crystal materials

Ipadapter-1

风格迁移

选择综合性强的大模型 dreamshaper

生成参考图


风格转换 style transfer


点击生成,


生成成功后,高清放大


直接更换风格
甚至可以根据动漫角色的画风,直接变换

ipadapter与openpose综合运用

controlnet unit1 ipadapter

生成骨骼图

controlnet unit2 openpose

写提示词增加lora

要和骨骼图一样比例


有lora则更靠近赫敏的图片

综合案例使用

室内设计
大模型选择 Architecturerealmix

unit 1 mlsd处理完只包含直线

unit 2

生成毛坯房
Unfinished, nothing, no furniture, rough, house interior

精装修
ModernRoomDesign,Interior design,modern simplicity,green,(masterpiece),(high quality),best quality,real,(realistic),super detailed,(full detail),(4k),modern,fashion,grand,vista,(high floor:1.2),


去到图生图


改变关键词然后图生图

如果成品出来还想改变,继续图生图
如果想要壁纸变成自己图片的风格,通过勾选上
传独立的控制图像
使用ipadapter,进行渲染
如果没有空间深度则继续使用depth

stable diffusion基础[liblib]

正向提示词

人物特征
1girl,solo,
suspender dress,headdress,delicate eyes,beautiful face,shallow smile,snow-white skin,elegant standing,
场景特点
outdoor,blue sky,white clouds,flowers,grass,
场景设定 [天气与光线与白天和黑夜]
day,night sunset,rain shrong rim light
movie light,light tracking,
场景形容词
beautifful ,happy
生图标准 [画质与风格]
8k,highest quality,high resolution
Comic Watercolor Realistic Abstract

1girl,solo,
suspender dress,headdress,delicate eyes,beautiful face,shallow smile,snow-white skin,elegant standing,
outdoor,blue sky,white clouds,flowers,grass,
movie light,light tracking,
beautiful,happy,
8k,highest quality,high resolution,
realistic,extreme detail

负向

low quality,blurry,bad proportions,cropped,watermark
ugly,bad body,missing fingers,extra feet
NSFW text logo

参数

eular a画笔选择,根据推荐来,其实都差不多
步数15-25,过高也不会有用

comfyui尝试

sd 挂lora+汉化工作流
图片有元数据
可以通过拉取图片来获得工作流

comfyui 大部分问题可以通过离线安装的方式解决
google可以解决。每当报错
通过日志去查看,工作流报红,缺模型就安装模型,缺插件安装插件,缺什么装什么

前言

今天复习了一些前端算法题,写到一两道比较有意思的题:重建二叉树、反向输出链表每个节点

题目

重建二叉树: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列 {1,2,4,7,3,5,6,8} 和中序遍历序列 {4,7,2,1,5,3,8,6},则重建二叉树并返回。

思路

前序遍历(根左右)和中序遍历(左根右)

思路就是使用递归把他分化为每个小的二叉树,然后都根据前序遍历(根左右)和中序遍历(左根右)的特性,前序的首元素就是根,然后再找到中序的根,根的左边就是左右边就是右,再进行递归,直到前序为null的时候就代表没有根节点了,那这个元素就是尾节点

一.

①[1,2,4,7,3,5,6,8],[4,7,2,1,5,3,8,6]-> val=>1 ->L([2,4,7],[4,7,2]) & R([3,5,6,8],[5,3,8,6]) 根节点 1 ,有左右节点

二.

①L([2,4,7],[4,7,2])-> val=>2 ->L([4,7],[4,7]) && R(null , null) 根节点2(属1的左节点) ,有左节点,无右节点

②R([3,5,6,8],[5,3,8,6])-> val=>3 ->L([5],[5]) && R([6,8],[6,8]) 根节点3(属1的右节点) ,有左右节点

三.

①L([4,7],[4,7]) ->val=>4 -> L(null , null) && R([7],[7]) 根节点4(属2的左节点) ,有右节点,无左节点

②R([6,8],[8,6]) -> val=>6 -> L([8] , [8]) && R(null , null) 根节点6(属3的右节点),有左节点,无右节点

③L([5],[5]) -> val=>5->(null,null)->终止 尾节点5(属3的左节点)

四.

①R([7],[7]) -> val=>7 ->终止 尾节点7(属4的右节点)

②L([8],[8]) -> val=>8 ->终止 尾节点8(属6的左节点)

代码实现

function rebuildBinaryTree(front, centre) {
    //判断是否为空节点
    if (!front || front.length == 0) {
        return null;
    }
    // 根节点
    var TreeNode = {
        val: front[0]
    };
    for (var i = 0; i < front.length; i++) {
        //找到中序遍历根节点位置
        if (centre[i] === front[0]) {
            //中序遍历(左根右)
            //根节点左边的节点为该节点的左边
            TreeNode.left = rebuildBinaryTree(front.slice(1, i + 1), centre.slice(0, i));
            //根节点右边的节点为该节点的右边
            TreeNode.right = rebuildBinaryTree(front.slice(i + 1), centre.slice(i + 1));
        }
    }
    return TreeNode;
}
let tree = rebuildBinaryTree([1, 2, 4, 7, 3, 5, 6, 8], [4, 7, 2, 1, 5, 3, 8, 6])
console.log(tree)

题目

从尾到头打印链表: 输入一个链表,从尾到头打印链表每个节点的值。

思路

由于链表是单向的,我们不能直接从头节点开始反向遍历。

所以可以使用数组来模拟栈。迭代遍历链表,将链表每个节点压入栈中,然后再依次从栈中弹出并打印。

代码实现

// 定义一个节点类,结构data表示节点数据、next表示下个节点的指针
class Node {
    constructor(data) {
        this.data = data
        this.next = null
    }
}

function printNode(node) {
    // 定义一个数组表示模拟栈
    let stack = new Array()
    // 初始化当前节点为传入的节点
    let NodeNextElm = node
    //判断下个节点指针是否为空
    while (NodeNextElm !== null) {
        //压栈
        stack.push(NodeNextElm.data)
        //存储下个节点的指针
        NodeNextElm = NodeNextElm.next
    }
    while (stock.length > 0) {
        //当栈不为空时,循环弹出栈顶元素并打印
        console.log(stack.pop())
    }
}
//初始化链表
//新建链表节点
const node1 = new Node(1)
const node2 = new Node(2)
const node3 = new Node(3)
//手动存储下个节点的指针
node1.next = node2
node2.next = node3
//调用
printNode(node1)

过程解析

一. 进入,此时的NodeNextElm:{
    "data": 1,
    "next": {
        "data": 2,
        "next": {
            "data": 3,
            "next": null
        }
    }
}

此时进入while循环:

①第一次循环:

栈stack:[1]

NodeNextElm:{
	"data": 2,
	"next": {
		"data": 3,
		"next": null
	}
}

②第二次循环:

栈stack:[1,2]

NodeNextElm:{
	"data": 3,
	"next": null
}

③第三次循环:

栈stack:[1,2,3]

NodeNextElm:null

循环结束

pop()弹出栈并打印:3,2,1

上述为个人整理内容,水平有限,如有错误之处,望各位园友不吝赐教!如果觉得不错,请点个赞和关注支持一下!谢谢~๑•́₃•̀๑ [鲜花][鲜花][鲜花]

全网最适合入门的面向对象编程教程:27 类和对象的 Python 实现-Python 中异常层级与自定义异常类的实现

image

摘要:

本文主要介绍了在使用 Python 进行面向对象编程时,异常的层级和如何使用继承关系完成自定义自己项目中异常类,并以传感器数据采集为例进行讲解。

原文链接:

FreakStudio的博客

往期推荐:

学嵌入式的你,还不会面向对象??!

全网最适合入门的面向对象编程教程:00 面向对象设计方法导论

全网最适合入门的面向对象编程教程:01 面向对象编程的基本概念

全网最适合入门的面向对象编程教程:02 类和对象的 Python 实现-使用 Python 创建类

全网最适合入门的面向对象编程教程:03 类和对象的 Python 实现-为自定义类添加属性

全网最适合入门的面向对象编程教程:04 类和对象的Python实现-为自定义类添加方法

全网最适合入门的面向对象编程教程:05 类和对象的Python实现-PyCharm代码标签

全网最适合入门的面向对象编程教程:06 类和对象的Python实现-自定义类的数据封装

全网最适合入门的面向对象编程教程:07 类和对象的Python实现-类型注解

全网最适合入门的面向对象编程教程:08 类和对象的Python实现-@property装饰器

全网最适合入门的面向对象编程教程:09 类和对象的Python实现-类之间的关系

全网最适合入门的面向对象编程教程:10 类和对象的Python实现-类的继承和里氏替换原则

全网最适合入门的面向对象编程教程:11 类和对象的Python实现-子类调用父类方法

全网最适合入门的面向对象编程教程:12 类和对象的Python实现-Python使用logging模块输出程序运行日志

全网最适合入门的面向对象编程教程:13 类和对象的Python实现-可视化阅读代码神器Sourcetrail的安装使用

全网最适合入门的面向对象编程教程:全网最适合入门的面向对象编程教程:14 类和对象的Python实现-类的静态方法和类方法

全网最适合入门的面向对象编程教程:15 类和对象的 Python 实现-__slots__魔法方法

全网最适合入门的面向对象编程教程:16 类和对象的Python实现-多态、方法重写与开闭原则

全网最适合入门的面向对象编程教程:17 类和对象的Python实现-鸭子类型与“file-like object“

全网最适合入门的面向对象编程教程:18 类和对象的Python实现-多重继承与PyQtGraph串口数据绘制曲线图

全网最适合入门的面向对象编程教程:19 类和对象的 Python 实现-使用 PyCharm 自动生成文件注释和函数注释

全网最适合入门的面向对象编程教程:20 类和对象的Python实现-组合关系的实现与CSV文件保存

全网最适合入门的面向对象编程教程:21 类和对象的Python实现-多文件的组织:模块module和包package

全网最适合入门的面向对象编程教程:22 类和对象的Python实现-异常和语法错误

全网最适合入门的面向对象编程教程:23 类和对象的Python实现-抛出异常

全网最适合入门的面向对象编程教程:24 类和对象的Python实现-异常的捕获与处理

全网最适合入门的面向对象编程教程:25 类和对象的Python实现-Python判断输入数据类型

全网最适合入门的面向对象编程教程:26 类和对象的Python实现-上下文管理器和with语句

更多精彩内容可看:

给你的 Python 加加速:一文速通 Python 并行计算

一文搞懂 CM3 单片机调试原理

肝了半个月,嵌入式技术栈大汇总出炉

电子计算机类比赛的“武林秘籍”

一个MicroPython的开源项目集锦:awesome-micropython,包含各个方面的Micropython工具库

文档和代码获取:

可访问如下链接进行对文档下载:

https://github.com/leezisheng/Doc

image

本文档主要介绍如何使用 Python 进行面向对象编程,需要读者对 Python 语法和单片机开发具有基本了解。相比其他讲解 Python 面向对象编程的博客或书籍而言,本文档更加详细、侧重于嵌入式上位机应用,以上位机和下位机的常见串口数据收发、数据处理、动态图绘制等为应用实例,同时使用 Sourcetrail 代码软件对代码进行可视化阅读便于读者理解。

相关示例代码获取链接如下:
https://github.com/leezisheng/Python-OOP-Demo

正文

异常层级

大部分异常都是 Exception 类的子类,但并非所有异常都是。Exception 类本身实际上继承自 BaseException。事实上,所有异常必须继承 Base Exception 类或是其子类。

image

image

有两个关键的异常,SystemExit 和 KeyboardInterrupt,它们直接继承自 BaseException 而不是 Exception:

  • SystemExit 异常在程序自然退出时抛出,通常是因为我们在代码中的某处调用了 sys.exit 函数(例如,当用户选择了菜单选项中的退出,或者是单击了窗口中的“关闭”按钮,或者是输入指令关闭服务器)。
    设计这个异常的目的是,在程序最终退出之前完成清理工作,而不需要显式地处理(因为清理代码发生在 finally 语句中)。
  • KeyboardInterrupt 异常常见于命令行程序。当用户执行依赖于系统的按键组合(通常是 Ctrl+C)中断程序时会抛出这个异常。这是用户故意中断一个正在运行的程序的标准方法,与 SystemExit 类似,应该以结束程序作为对它的响应。同时,与 SystemExit 类似,处理它应该在 finally 块中完成清理任务。

内置异常的类层级结构如下:

image

当我们仅用 except:从句而不添加任何类型的异常时,将会捕获所有

BaseException 的子类;也就是说将捕获所有异常,包括上述的两个特殊的异常对象。如果你想要捕获所有除了 SystemExit 和 KeyboardInterrupt 之外的其他异常,你应该明确指明捕获 Exception。更进一步,如果你真的想要捕获所有异常,我建议你使用 except BaseException:语法而不是 except:。这样可以告诉其他人你是故意这样做以处理特殊异常的。

自定义异常类

在通常情况下,当我们需要在特定情境下触发一个异常时,我们可能会发现并无现成的内置异常对象能够满足我们的需求。然而,值得庆幸的是,创建我们自己的异常类是相当简便的。异常类的命名通常旨在传达发生了何种错误,同时,我们也可以在初始化函数中添加任何所需的参数,以提供更详细的信息。

不论是以直接还是间接的方式,异常都应从 Exception 类派生。异常类可以被定义成能做其他类所能做的任何事,但通常应当保持简单,它往往只提供一些属性,允许相应的异常处理程序提取有关错误的信息。大多数异常命名都以 “Error” 结尾,类似标准异常的命名。

这里,我们定义一个 InvalidIDError 表示传入的传感器 ID 号非法。

同时,在 try 语句块中,用户自定义的异常后执行 except 块语句,变量 e 是用于创建 InvalidIDError 类的实例。示例代码如下:

_# 定义一个ID号非法的异常_
class InvalidIDError(Exception):
    pass

class SensorClass(SerialClass):
    _# 类变量:_
    _#   RESPOND_MODE -响应模式-0_
    _#   LOOP_MODE    -循环模式-1_
    RESPOND_MODE,LOOP_MODE = (0,1)
    _# 类变量:_
    _#   START_CMD       - 开启命令      -0_
    _#   STOP_CMD        - 关闭命令      -1_
    _#   SENDID_CMD      - 发送ID命令    -2_
    _#   SENDVALUE_CMD   - 发送数据命令   -3_
    START_CMD,STOP_CMD,SENDID_CMD,SENDVALUE_CMD = (0,1,2,3)

    _# 类的初始化_
    def __init__(self,port:str = "COM11",id:int = 0,state:int = RESPOND_MODE):
        try:
            _# 判断输入端口号是否为str类型_
            if type(port) is not str:
                raise TypeError("InvalidPortError:",port)
            _# 判断ID号是否在0~99之间_
            if id <= 0 or id >= 99:
                _# 触发异常后,后面的代码就不会再执行_
                _# 当传递给函数或方法的参数类型不正确或者参数的值不合法时,会引发此异常。_
                raise InvalidIDError("InvalidIDError:",id)
            _# 调用父类的初始化方法,super() 函数将父类和子类连接_
            super().__init__(port)
            self.sensorvalue = 0
            self.sensorid    = id
            self.sensorstate = state
            print("Sensor Init")
            logging.info("Sensor Init")
        except TypeError:
            _# 当发生异常时,输出如下语句,提醒用户重新输入端口号_
            print("Input error com, Please try new com number")
        except InvalidIDError as e:
            _# 当发生异常时,输出如下语句,提醒用户重新输入ID号_
            print("Input error ID, Please try id : 0~99")
            print(e.args)

运行代码结果如下:

image

对于 InvalidIDError 类而言,
我们可以给异常传入任意数量的参数,通常用一个字符串信息,但是任何可能用于后面异常处理的对象都是可以的。

Exception.__init__方法设计成接收任意参数并将它们作为元组保存在一个名为 args 的属性中。这使得我们可以更容易地定义新的异常,而不需要重写 init 方法。

当然,如需自定义初始化函数,这是完全可行的做法,我们可以这么做。

举个例子,在接收传感器数据中,有时传感器数据会离实际数据偏差过大。我们在 MasterClass 主机类的 RecvSensorValue 接收传感器数据方法中,可以设计一个 InvalidSensorValueError 异常类,在该异常类的初始化函数中接收当前获取的传感器数值和设定的传感器数据阈值为参数。此外,它还有一个方法用于计算这次传感器数值与设定阈值间差值的方法。示例代码如下:

_# 表示传感器数据过高的异常_
class InvalidSensorValueError(Exception):
    def __init__(self,recvvalue,setvalue):
        super().__init__("Receive Sensor Value is too high")
        self.recvvalue = recvvalue
        self.setvalue = setvalue
    _# 计算接收数据和设定数据的误差值_
    def cal_offset(self):
        offset = self.setvalue - self.recvvalue
        return offset

class MasterClass(SerialClass,PlotClass):
    ...
    _# 接收传感器数据_
    def RecvSensorValue(self):

        try:
            _# 设定的阈值_
            setvalue = 99
            data = super().ReadSerial()

            _# 如果接收的传感器数据大于阈值_
            if data >= setvalue:
                raise InvalidSensorValueError(data,setvalue)

            print("MASTER RECIEVE DATA : " + str(data))
            logging.info("MASTER RECIEVE DATA : " + str(data))
            self.valuequeue.put(data)
        except InvalidSensorValueError as e:
            print("invalid sensor value",e.args)
            print("value offset is : ",e.cal_offset())
        return data 
    ...
if __name__ == "__main__":
    _# 创建主机类_
    m = MasterClass()
    m.StartMaster()
    m.RecvSensorValue()

运行代码,结果如下:

image

有许多原因值得我们考虑如何定义自己的异常。
这样做有助于向异常中添加信息或以其他形式记录日志。然而,自定义异常的真正优势在于,它们能够创建出供他人使用的框架、库或 API。
在此过程中,我们必须注意确保我们编写的代码所抛出的异常对于客户端程序员来说是合理的。他们应该能够轻松地处理这些异常,并清楚地描述当前的情况。同时,客户端程序员应能轻松地看出如何修复这些错误(如果这导致他们的代码中出现错误)或者处理这些异常(如果是他们需要了解的情况)。这不仅要求我们确保异常处理的清晰度和一致性,同时也需考虑到程序的整体可读性和维护性。

image

.NET Aspire
用于云原生应用开发,提供用于构建、测试和部署分布式应用的框架,这些应用通常利用微服务、容器、无服务器体系结构等云构造。2024年7月23日宣布的新 8.1 版本是该平台自 5 月正式发布以来的第一次重大更新,Microsoft 对 .NET Aspire 的第一个重大更新Aspire 8.1解决了容器镜像的构建和 Python 代码的编排以及一系列新功能和增强功能的问题。

Aspire 框架本身可以称为分布式框架,因为它位于 NuGet 包的集合中,可用于在 Visual Studio、Visual Studio Code 或命令行中构建应用。

image

Microsoft的Mitch Denny在7月23日的.NET Aspire 8.1
公告
中说:“这个版本包括一些新功能和生活质量改进,这些反馈来自在生产应用程序中使用.NET Aspire的开发人员的反馈。他强调了此更新的两个具体功能:支持使用 AddDockerfile(...)
构建容器镜像
,以及使用 AddPythonProject(...)
编排 Python 代码
.


容器镜像
使用在应用程序主机运行时自动构建 Docker 文件的方法
AddDockerfile(...)
,该方法
WithDockerfile(...)
还可以帮助开发人员通过允许轻松编辑和与现有资源集成来自动化 Dockerfile 构建和定制。因此,前者非常适合创建新的容器资源,而后者则用于修改现有的容器镜像。

这两种方法都支持构建参数和密钥,使开发人员能够将参数和敏感信息安全地传递给 Docker 构建过程,同时避免在应用程序清单中意外泄露。

Denny 说:“这意味着您可以快速编辑 Dockerfile,并依赖 .NET Aspire 来构建它们,而无需自己手动构建。


编排 Python 代码


Denny 强调的第二个主要特性是通过代码编排方法增强了对多语言微服务架构的支持。在 .NET Aspire 中,业务流程主要侧重于通过简化云原生应用的配置和互连的管理来增强本地开发体验。

该方法由 Willem Meints 贡献,允许开发人员启动基于 Python 的服务。要使用它,开发人员需要安装 Python 托管包并将 Python 资源添加到他们的应用程序模型中。该方法
AddPythonProject(...)
利用 Python 的虚拟环境 (venv) 工具,需要手动安装文件中指定的依赖项。此外,如果包含依赖项,它将在 .NET Aspire 仪表板中启用遥测。但是,由于 OpenTelemetry 库的限制,必须在环境变量设置为 的情况下运行应用程序。
requirements.txt
opentelemetry-distro[otlp]
ASPIRE_ALLOW_UNSECURED_TRANSPORT
true

Python 支持加入了对 Node.js 应用的现有支持。


Denny的文章还提供了有关以下内容的详细信息:

  • 容器化扩展中提供的新资源类型和组件,范围从 Kafka UI 到 Azure Web PubSub
  • 测试改进(更易于编写需要等待资源初始化的测试用例)
  • 指标示例(聚合数据的示例数据点)
  • 跨度链接(在跨度之间创建关系)
  • 改进了实例 ID 名称(友好的实例 ID 而不是难以阅读的 GUID)

有关详细信息,请参阅 Microsoft 的
.NET Aspire 中的新增功能
文档。