Go语言实战案例(中):在线词典
实践记录 · 2023/8/1 · 玉米哥
目录
猜数游戏
在线词典
SOCKS5代理
前言
在上一篇文章中,我总结了王克纯老师布置的第一个作业:猜数游戏,并详细介绍了Go语言中输入处理的一般准则,如果你还没有看这篇文章,请点击下方链接。
......
如此之多的函数,对输入的处理各有不同,什么时候保留\n,什么时候丢弃\n,读取字符串时什么规则,读取数字时又是什么规则。纷繁复杂,无法记忆。而且记了恐怕也难以用得上,最终只会为难自己。因此,我们可以制定一种读取输入的规范,来简化输入处理的流程。
......
本篇文章,咱们主要给小伙伴讲解第二个作业:在线词典。相信看过视频讲解的同学们都很清楚,这个程序与之前的示例有很大不同。最突出的地方就在于,这个程序断网了就没办法使用。
换句话说,这是个联网的程序。更进一步,程序的输入word
由用户给出,我们需要将输入发送到网络中的某个神秘的地方(其实就是一台电脑),这个地方提供服务,查询word
的含义,并将结果返回到我们的所在的地方,也就是我的电脑,然后我们再根据自己的需要,输出结果到控制台、文件、数据库等地方。
让我们即刻开始今天的学习吧!
以下内容主要总结于
- 字节跳动青训营后端入门 - Go语言原理与实践
- Go语言官方文档教程
- Go语言圣经
人类社会的信息沟通
飞鸽传书
玉米哥是古代进京赶考的一名考生,到了放榜这天,他惊喜的得知自己考中了状元。眉飞色舞之际,玉米哥想尽快告知女朋友这个好消息。无奈的是,他跋山涉水迢迢千里不远万里风雨无阻地来到了京城,可是回去不知又要几个月,这该如何是好?
好在还有信鸽可以使用,于是玉米哥赶快写好了信的内容。
橘子美女:
娘子端午安好,恩情倍加。自饮恩泽,得见君临高堂,日夜念念难忘。
今日欣闻风声传喜,崇光榜上,登峰造极。
状元之荣耀,叩谢天恩,谢妻之德,念昔日同携晨夕,共绘家园之幸福。
自吾云开雾散之辰,遵彼先贤之训,持素秉真,苦心经营,君孜孜不倦,瞻仰榜文,求学切求。
功底深厚,识见高妙,可谓学富五车,文采风华。凌霄之姿,乘风翱翔,焕发光华,气蒸云梦。
夫妻之间,相知相许,手足情深,义重于山。
吾谨记妻旧日育才之艰辛,不遗微末,不忘切身之情。
今竟脱颖而出,不胜欣喜之至,如沐春风,荣光满怀。
谨启
玉米哥
注意:情节需要,信件内容使用AI生成
就这样,信鸽携带着玉米哥写的信,飞回家报喜去了。
邮局寄信
一晃又过了几百年,转世之后的玉米哥,现在还保留着写信的习惯,只不过现在的人们不用鸽子传信了,而是通过邮局寄信。
不像鸽子一样认路,只要带上信的内容,就能把信送回家。邮局可不认识玉米哥家里的路。写信的时候,除了信件的内容,还需要再写一些附加的信息。
- 收信人:这封信要给谁
- 寄信人:谁寄的这封信
- 收信人地址:这封信要送到那里
- 寄信人地址:送不到的时候,原路退回
- 正文:信的内容
有了这些信息,玉米哥就能把好消息及时地告诉女朋友了。
使用HTTP协议发送信息
兜兜转转一大圈,终于来到了我们最关心的部分。玉米哥最近在学习英语六级,有个单词不认识,但是手头又没有英译汉词典。幸运的是,他知道有个网站可以查单词,叫做彩云小译。
玉米哥遵循以下的步骤发送消息。
- 打开彩云小译官网
- 输入需要查询的单词
- 点击翻译按钮
- 等待页面返回结果
就这样,玉米哥查到了单词的意思,并成功通过了六级考试。那么在这一系列的过程中,究竟发生了什么呢?
查单词是一种服务,词典可以提供这项服务,英语老师可以提供这项服务,电脑也可以提供这项服务。你只要给定单词,就能得到释义。只不过你给出单词的形式不同,得到释义的形式也各不相同。
提供查单词服务的电脑,我们就把它叫做服务器。我们通过自己的电脑查单词,我们的电脑叫做客户端,毕竟客户是上帝。
现在客户端和服务器想要通信,怎么让客户端把单词发到服务器呢?
茫茫网海中,有不止一台服务器提供服务,也有不止一台客户端在消费服务。我们必须明确,需要和哪一台服务器交流。
聪明的你大致可以想一下,为了发送这一封电子“信件”,我们需要提供哪些信息。
信件封面(可能是):彩云小译服务器在网络上的地址
信件内容(可能是):你好,彩云小译服务器,我想查一下pretty这个单词是什么意思,谢谢。
说到这里,再也偷偷藏不住了,这时候不用HTTP已经没办法交流了。
HTTP协议是什么
HTTP协议是一种约定、一种规范、一种协议。就像你在寄快递时需要填写这些内容。
内容 | 含义 | 格式 |
---|---|---|
姓名(发件人) | 你的姓名 | 有效的姓名 |
电话(发件人) | 你的电话 | 11位有效电话 |
地址(发件人) | 你的地址 | xx省xx市xx区xx门牌号 |
物品种类 | 食品、衣物、电子产品 | [食品]、[衣物]、[电子产品]中选一个 |
姓名(收件人) | 别人的姓名 | 有效的姓名 |
电话(收件人) | 别人的电话 | 11位有效电话 |
地址(收件人) | 别人的地址 | xx省xx市xx区xx门牌号 |
快递物品 |
---|
仔细观察表格,除了要快递的物品本身,我们还需要附加填写很多信息,并且这些信息都具有一定的格式和约束,归根结底,是为了促进快递的高效流通。那么HTTP协议中又有什么样的约定或者要求呢?
HTTP报文的内容
HTTP报文的结构由报文首部、空行、报文主体组成。
组成部分 | 作用 |
---|---|
报文首部 | 传递信息时必不可少的一些附加信息 |
空行 | 用于分割报文首部和报文主体 |
报文主体 | 信息的内容 |
报文首部类似于快递包裹表面的快递纸,报文主体类似于快递的物品本身,空行类似于包裹,将快递纸和物品隔离开。
与看得见摸得着的快递包裹不同,HTTP报文的本质是一串字节序列,在网络流中由不同设备进行处理,因此HTTP报文的每一部分都有自己的格式要求,便于设备能够读懂。
请求报文与响应报文
回到查单词的例子,我的客户端想给彩云小译的服务器发送pretty这个单词,就需要按照HTTP协议的规定,发送一个HTTP报文。彩云小译的服务器收到请求报文并处理后,也会返回一个报文当作结果。
客户端发送到服务器的报文,叫做请求报文。
服务器返回给客户端的报文,叫做响应报文。
先来看一个请求报文的例子。
请求报文
POST
/form/entry
HTTP/1.1
Host: hackr.jp
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 16
name=ueno&age=37
根据我们刚刚介绍的报文结构,可划分为以下结构。
部分 | 内容 |
---|---|
报文首部 | POST /form/entry HTTP/1.1 |
Host: hackr.jp | |
Connection: keep-alive | |
Content-Type: application/ | |
x-www-form-urlencoded | |
Content-Length: 16 | |
空行 | |
报文主体 | name=ueno&age=37 |
其中,报文首部又可以进一步划分。
请求报文的报文首部可划分为以下部分。
- 请求行
- 请求首部字段
- 通用首部字段
- 实体首部字段
- 其他
请求行
在上面的示例中,请求行就是POST
/form/entry
HTTP/1.1
,分别对应请求行的三个组成部分:方法
、URI
、HTTP版本
。
方法表示客户端访问服务器的类型,在本例中,方法是POST
,除了该方法,常用的还有GET
等,这些方法都是HTTP协议预先定义好的,只需要选择恰当的方法就可以。在本文中,我不对这些方法进行详细阐述,目前我们只需要暂时记住就可以。
URI表示需要访问的服务器上的资源地址,URI的全称是Uniform Resource Identifier
,统一资源标识符,用于在浩瀚如烟的互联网中标记资源的位置,你可以暂时理解为查询单词的服务器所在的地址。
HTTP版本,显而易见,表示我们正在使用的HTTP协议的版本。在HTTP发展的历程中,HTTP的版本也在不断发生变化,不同的HTTP版本,使用的规范也有所不同。我们也暂时只需记住,无需深究。
首部字段
首部字段相当于寄快递所需的附加信息,这些信息绝大部分都不需要我们自己提供,浏览器会自动填写这一切,拿Host: hackr.jp
举个例子,表明了HTTP报文前往的目的地,也就是域名为hackr.jp
的服务器。该字段和URI结合,便能够准确地将信息发送到我们想要的那台服务器。
报文主体
报文主体就是我们要发送的内容本身,等同于包裹里的物品,信件中的正文,这里不过多解释。
响应报文
再看一个响应报文的例子。
HTTP1.1
200
OK
Date: Tue, 10 Jul 2023 15:39:30 GMT
Content-Type: text/html
Content-Length: 362
<html>...</html>
响应报文同样可划分为以下结构。
部分 | 内容 |
---|---|
报文首部 | HTTP1.1 200 OK |
Date: Tue, 10 Jul 2023 15:39:30 GMT | |
Content-Type: text/html | |
Content-Length: 362 | |
空行 | |
报文主体 | <html>...</html> |
响应报文的报文首部可划分为以下部分。
- 状态行
- 响应首部字段
- 通用首部字段
- 实体首部字段
- 其他
状态行
状态行由协议版本、状态码(表示请求成功或失败的数字代码)、用以解释状态码的原因短语构成。
状态行开头的HTTP/1.1
表示服务器对应的HTTP的版本。紧接着的200 OK
表示请求的处理结果的状态码和原因短语。
首部字段
Date
表示响应创建的日期和时间,是首部字段内的一个属性。
Content-Type: text/html
表示响应报文的报文主体的内容格式,是文本类型中的html文本。除此之外,还可以是图片文件、视频文件、JSON、二进制类型等。
Content-Length: 362
表示报文主体的大小。
报文主体
服务器接收到请求报文并处理后,需要返回的内容就在报文主体中,发送给客户端处理。
使用Go语言进行HTTP通信
耐心的你看到这里,相信已经对HTTP原理有一个感性的认识了,懂得了原理,就可以大刀阔斧地实战了。现在我们开始编码部分,请小伙伴接下来,一边看,一边做,加深对知识的理解。
第一步:获取请求报文和响应报文的格式和内容
- 打开彩云小译官网。
- 按下
F12
(或者在任意空白处单击右键
,选择检查
),此时,会打开浏览器的开发者工具栏
。 - 在
开发者工具栏
的顶部区域,点击网络
(如果没有,点击>>
可展开更多选项卡,再点击网络
)。 - 在网页找到单词翻译框,输入
pretty
。 - 点击
翻译
按钮。 - 此时,
开发者工具栏
更新出许多文件,在筛选器
一栏,点击Fetch/XHR
,在剩下的文件中,选中dict
文件。
此时,我们就找到了请求报文所在的文件,可是在实践中,该如何判断这个文件就是我们需要的文件呢?
选中dict
文件后,在开发者工具栏的右侧区域,点击负载
,可以看到类似下面的内容。
trans_type: "en2zh"
{
"trans_type": "en2zh",
"source": "pretty"
}
这是一个JSON格式的数据,竟然看到了我们想查找的单词pretty
,那么这个文件很有可能就是请求报文。
再点击预览
,我们能看到另一个JSON格式的数据。
{
"rc": 0,
"wiki": {},
"dictionary": {
"prons": {
"en-us": "[ˈprɪtɪ]",
"en": "[ˈpriti]"
},
"explanations": [
"a.漂亮的,美丽的;温暖的;机灵的;优美的;好的;精致的",
"[俗]大的",
"ad.十分;很;颇;非常",
"vt.使漂亮;使可爱;予以美化"
],
"synonym": [
"attractive",
"lovely",
"beautiful",
"handsome",
"good-looking"
],
"antonym": [
"ugly",
"unattractive",
"unsightly",
"homely",
"plain"
],
"wqx_example": [
[
"a pretty penny",
"一大笔钱,相当可观的一笔钱"
],
[
"a pretty kettle of fish",
"一团糟,一塌糊涂"
],
[
"She is rather pretty . ",
"她相当秀丽。"
]
],
"entry": "pretty",
"type": "word",
"related": [],
"source": "wenquxing"
}
}
这个JSON文件竟然包含了音标、释义、反义词、近义词、例句等。看来这应该是响应报文的一部分了。
现在我们确信无疑,这个dict文件就是我们要找的文件。包含了响应报文和请求报文的信息。
第二步:构造请求报文和响应报文
在网页中查询单词,我们只需要输入单词,单击翻译就能看到结果了。
在这个过程中,浏览器帮我们做了下面的事情。
- 构造请求报文
- 将请求报文发送给服务器
- 接收服务器返回的响应报文
- 解析响应报文并将结果呈现在网页中
在我们自己的命令行程序中,步骤也很类似,只不过需要我们自己从命令行获取输入,构造请求报文,获取响应报文并将结果呈现在命令行中。
下面我们就来看看怎么做吧。
- 右键单击
dict
文件,选择复制
-复制为cURL(bash)
。 - 在
浏览器
的地址栏输入curlconverter.com/go/,转到该网站。 - 将步骤1复制的文本粘贴到该网站的输入框中,即可得到以下代码。
package main
import (
"fmt"
"io"
"log"
"net/http"
"strings"
)
func main() {
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"pretty"}`)
req, err := http.NewRequest("POST", "https://lingocloud.caiyunapp.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
req.Header.Set("Cookie", "_gcl_au=1.1.757739313.1690550639; _ga_65TZCJSDBD=GS1.1.1690550638.1.0.1690550639.0.0.0; _ga_R9YPR75N68=GS1.1.1690550638.1.0.1690550639.59.0.0; _ga=GA1.2.2096512254.1690550639; _gid=GA1.2.1395040821.1690550642")
req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "same-site")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36")
req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
req.Header.Set("app-name", "xy")
req.Header.Set("device-id", "02c95aaa9ec45900cfdd21cdaa76323b")
req.Header.Set("os-type", "web")
req.Header.Set("os-version", "")
req.Header.Set("sec-ch-ua", `"Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", bodyText)
}
这段代码虽然很长,我们来一段段解析。
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"pretty"}`)
req, err := http.NewRequest("POST", "https://lingocloud.caiyunapp.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
req
表示我们构造的请求报文,我们使用了三个参数来初始化这个请求报文。
POST
表明了请求方法,属于请求报文中报文首部的请求行。
https://lingocloud.caiyunapp.com/v1/dict
表明了请求URI。也属于请求报文中报文首部的请求行。
data
属于请求报文的报文主体。是一个reader
,输入源是字符串构造的JSON数据,里面包含了我们的单词pretty
。
如果你还不知道
reader
是什么,请阅读我的上一篇文章Go语言实战案例(上),我从猜数游戏拓展了Go语言中有关输入处理的方方面面。
通过上面的代码,我们就能构造出一个请求报文,下面的代码是对请求报文的进一步完善。
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("Accept-Language", "zh-CN,zhq=0.9")
req.Header.Set("Connection","keep-alive")
req.Header.Set("Content-Type","application/json;charset=UTF-8")
......(此处省略)......
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform",`"Windows"`)
每一行代码的开头都是req.Header.Set()
,过了英语六级的小伙伴,一定能看懂这是什么意思。这不就是设置请求报文的报文首部吗,和我文章中介绍HTTP部分的内容如出一辙,如果你认真看完了我文章的开头,一定会懂这是在做什么。
代码中设置的首部字段我不在这里讲解,感兴趣的小伙伴可以自己查阅,总之,这些字段就是为了客户端和服务器能够更好的通信,类似于快递包裹表面的快递纸包含的内容。
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", bodyText)
接下来的操作就很简单了,客户端将构造好的请求报文发送给服务器,并从服务器返回的响应报文中找到报文主体,将报文主体的内容全部读取到bodyText
中,然后以文本形式打印bodyText
。
这个例子很好地解释了
reader
的输入源不仅有键盘、文件、字符串,还有网络上的内容。我在文章Go语言实战案例(上)中详细解释了这几种不同类的输入源,想深入了解的小伙伴快去看看吧。
不出意外的话,下面是打印的内容。
{"rc":0,"wiki":{},"dictionary":{"prons":{"en-us":"[\u02c8pr\u026at\u026a]","en":"[\u02c8priti]"},"explanations":["a.\u6f02\u4eae\u7684,\u7f8e\u4e3d\u7684;\u6e29\u6696\u7684;\u673a\u7075\u7684;\u4f18\u7f8e\u7684;\u597d\u7684;\u7cbe\u81f4\u7684","[\u4fd7]\u5927\u7684","ad.\u5341\u5206;\u5f88;\u9887;\u975e\u5e38","vt.\u4f7f\u6f02\u4eae;\u4f7f\u53ef\u7231;\u4e88\u4ee5\u7f8e\u5316"],"synonym":["attractive","lovely","beautiful","handsome","good-looking"],"antonym":["ugly","unattractive","unsightly","homely","plain"],"wqx_example":[["a pretty penny","\u4e00\u5927\u7b14\u94b1,\u76f8\u5f53\u53ef\u89c2\u7684\u4e00\u7b14\u94b1"],["a pretty kettle of fish","\u4e00\u56e2\u7cdf,\u4e00\u584c\u7cca\u6d82"],["She is rather pretty . ","\u5979\u76f8\u5f53\u79c0\u4e3d\u3002"]],"entry":"pretty","type":"word","related":[],"source":"wenquxing"}}
看来这个响应报文的主体是JSON数据,但不幸的是,我们的黑框框目前还没有对数据做出美化,只能呈现原始的丑陋的数据。而浏览器则使用了HTML、CSS等工具呈现美观的数据。
第三步:特定于程序的改造
到这步,我们的任务已经完成了一大半了,在黑框框程序中上网,将想查询的单词发送给服务器,然后显示结果。
从用户获取输入
但是目前,要想修改查询的单词,我们只能在下面的代码中修改data
。
var data = strings.NewReader(`{"trans_type":"en2zh","source":"pretty"}`)
仔细分析,data
是一个从字符串获取输入的reader
,并且输入的内容是JSON结构的数据。而在Go语言中,struct
和JSON数据是可以相互转换的。
因此,我们可以定义一个struct
,在程序运行时由用户输入source
,然后将该struct
序列化为JSON数据,并读入data
中。
- 定义
struct
。
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
}
- 创建并初始化DictRequest对象。
request := DictRequest{TransType: "en2zh", Source: "pretty"}
小伙伴可能会有疑惑,
pretty
不还是在代码中吗,其实这是一个对象初始化语句,只需要先创建一个变量word
,然后使用word
初始化该对象中的Source
即可。
- 将对象序列化为JSON字节流
byteStream, err := json.Marshal(request)
如果你还不知道什么是序列化和反序列化,请阅读我的第二篇文章,Go语言入门指南:基础语法(下),这篇文章详细解释了序列化和反序列化的含义和来龙去脉。
- 将该JSON字节流读入
data
中。
var data = bytes.NewReader(byteStream)
上面的代码创建了一个reader
,并且指定了输入源是字节流。现在我们已经准备好data
了,后面的代码和示例就一摸一样了。
美观地呈现输出
上文提到,如果不做任何处理,那么服务器返回的JSON数据将会被原原本本的展现出来,很丑陋。那应该怎么办呢?
答案很简单,只要我们定义一个和JSON数据每一项都对应的struct
,将JSON数据反序列化到这个struct
中,然后操作这个struct
控制输出就可以了。
但是输出的这个JSON数据又很多项,手工敲代码又累又容易出错,我们可以使用克纯老师介绍的工具来自动生成struct
。
- 打开
浏览器
,输入oktools.net/json2go,跳转到该网站。 - 将服务器返回的JSON数据粘贴到网站的输入框,点击
转换-嵌套
。 - 点击
复制
,就得到了我们需要的struct
了。
type DictResponse struct {
Rc int `json:"rc"`
Wiki struct {
} `json:"wiki"`
Dictionary struct {
Prons struct {
EnUs string `json:"en-us"`
En string `json:"en"`
} `json:"prons"`
Explanations []string `json:"explanations"`
Synonym []string `json:"synonym"`
Antonym []string `json:"antonym"`
WqxExample [][]string `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
如果你阅读过我的第二篇文章Go语言入门指南:基础语法(下),你应该熟悉服务器返回的JSON数据是字节流,现在我们可以将得到的bodyText
字节流反序列化到新定义的DictResponse
中。
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
现在dictResponse
就包含了我们需要的内容,单词的音标、释义等,我们使用基本的循环控制流优雅地输出这些内容。
fmt.Println(dictResponse.Dictionary.Entry, "en-us", dictResponse.Dictionary.Prons.EnUs, "en", dictResponse.Dictionary.Prons.En)
for _, explanation := range dictResponse.Dictionary.Explanations {
fmt.Println(explanation)
}
输出代码根据对象的结构实现,可能不唯一
运行程序,可以看到下面的过程了。
输入
$ go run search.go pretty
输出
pretty en-us [ˈprɪtɪ] en [ˈpriti]
a.漂亮的,美丽的;温暖的;机灵的;优美的;好的;精致的
[俗]大的
ad.十分;很;颇;非常
vt.使漂亮;使可爱;予以美化
虽然依旧很丑陋,但是相比直接输出JSON数据,还是要优雅不少的,小伙伴,学会了吗?
小结
到目前为止,我们终于做出了一个在线查词的程序,能够构造请求报文,将报文发送给服务器,并以各种形式利用返回的响应报文,达成我们自己的目的,希望小伙伴看完后能有所收获。
下一步
呼,终于给文章收尾了,读完这一篇文章,希望你能够对互联网上数据的传输有更为深刻的认识,同时理解HTTP的基本原理,并能够用Go语言来完成客户端和服务器之间的通信。如果你想了解更多关于HTTP协议的内容,可以阅读《图解HTTP》,如果你对计算机网络感兴趣,可以阅读《网络是怎样连接的 》。想获取这两本书的电子版,小伙伴可以私聊我。
码字不易,如果您看到了这里,听我说谢谢你😀
如果您觉得本文还不错,请留下小小的赞😀
如果您有感而发,请留下宝贵的评论😀