- CLI开发框架
- cobra 集成库
- 目录规范
- 搭建框架
- 根命令
- 参数添加
- 子命令
- 帮助信息
- cobra 集成库
- 爬虫功能(趁热打铁)
- Goquery处理响应
- 编码处理
- 收集百度热搜榜
- 爬虫功能所有源码
CLI开发框架
师傅们久等了,为了加快进度,这章节添加了一个爬虫功能,也是后面写工具要用到的。
学习成果:能够集成一个爬虫功能到工具中
如下图所示
cobra 集成库
cobra 是一个cli程序脚手架,大是大了点,但是有规范模版,同时也很好用,代码分明(本人比较喜欢用这个)
以下仅仅是个人开发中常用到的,涉及比较浅,但是用来设计属于自己的小工具应该是足够的哈!依旧是修行靠个人。
- 下载
go get github.com/spf13/cobra/cobra
目录规范
虽然说这个库也可以随便创建来使用,但是我十分推荐下面这个模版,清晰而且显得专业。
▾ 项目/▾ cmd/cmd1.gocmd2.gocmd3.goroot.go▾其他(util)util.gosql.go...main.go
这里先讲一下目录结构:
- cmd:
就是你的命令放在该目录的root里面,子命令就是你创建的一些比如cmd1或者cmd2文件就是一些以后扩展的时候自行扩展,不是说一定要这些文件,主要文件需要一个就行,root也可以不叫root,只不过他是告诉你需要一个核心命令文件 - main.go是必须的,一般都叫main,你也可以重命名其他,只不过代码中
package main
的main要换名字(小知识点在这里补充了)
搭建框架
细节:
- 一般接收参数的变量都是放在全局
- 重复一遍:记得
go mod tidy
导入包,这是三方的,不是go默认自带,需要导入使用,就算你下载了也要导入。
根命令
root.go根命令
记住两点:
- 结构体实现:
&cobra.Command
Execute()
函数实现,这个是写给main函数调用的,所以你起什么名字都行,不一定按照我这个名字。
package cmdimport ( "fmt" "github.com/spf13/cobra")var rootCmd = &cobra.Command{ Use: "命令名字", Short: "短描述", Long: `长描述`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Hello, cobra!")
},
}func Execute() error {
return rootCmd.Execute()//执行命令
}
main.go
- 调用root中给的Execute函数即可
package mainimport (
"go_cobra/cmd"
)func main() {cmd.Execute()
}
参数添加
- 实现
init()
函数,这个就是库会自动调用,我们只要负责实现,该函数即可, 这个函数主要是用来配合rootCmd的,也就是说我们的根命令,init就是来给他添加东西,比如添加参数,添加子命令(后面会讲) - 全局与作用在单个命令中
单个:Flags
全局:PersistentFlags
单个即作用与某一个命令之下,比如root也算一个命令,但是他是根命令,假设我们有一个子命令,子命令下使用了这个Flags就是你该参数只能作用与这个子命令中
如果你写的是PersistentFlags作用是:你当前命令下的其他子命令也能使用
我这里添加两个参数,一个是本地的,意思是只有在root根命令才能使用,另一个是全局,当后面创建了子命令的时候,子命令也能使用我这个参数
变量如下:
var ( P string //打印参数 PP string //全局打印参数)
初始化函数init()
默认值为:nil
func init() {
rootCmd.Flags().StringVarP(&P, "print", "p", "nil", "打印") //添加参数
rootCmd.PersistentFlags().StringVarP(&PP, "Print", "P", "nil", "全局打印") //添加全局参数}
解释一下添加参数的函数,Flags为例子,PersistentFlags一样的。
这俩函数下有参数类型可以选择,所以说不仅仅是string可以作为参数值传入,当你的变量类型为bool的时候可以是:
rootCmd.Flags().BoolVarP
- 第一个参数:接收用户传入的参数值了,用变量接收,变量类型就是要看啊刚刚说的你用什么类型的函数了。
- 第二个参数:用户完整选项,也就是长选项
- 第三个参数:用户短选项
- 第四个参数:该选项的默认值
- 第五个参数“:该参数命令描述
运行结果
全局命令区别在下面的子命令中区分实现。
子命令
在cmd文件夹里创建一个version.go文件
- 结构体实现:
&cobra.Command
,和根命令以一样的类型,所以很多东西都是可以用的,添加子命令也时添加这个类型到根命令中。 - 依旧是实现
init()
函数,在函数里面将你的子命令添加进去即可。
这里就加一个版本命令,工具经常要写的一个子命令。
在init中使用:rootCmd.AddCommand(versionCmd)
,意思就是根命令中添加一个子命令versionCmd
在Run中同时也写了之前的全局参数PP
的操作:也就是说全局虽然说是全局,但是在代码里面并非真的自动调用,而是需要你手动写进去,他全局的意思是全局接受,负责操作的依旧是在你当前子命令中, 他不会因为是某个命令下的全局参数而直接在你这个子命令中自动调用哈!!
代码如下:
cmd/version.go
package cmdimport (
"fmt" "github.com/spf13/cobra"
)var versionCmd = &cobra.Command{
Use: "version",
Short: "显示版本",
Long: `显示xxxx版本`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("工具当前版本:v1.0.0")
if PP != "nil" { fmt.Println(PP) }
},
}func init() {
//将versionCmd添加到rootCmd
//这样在执行命令时,就可以使用version这个子命令了
rootCmd.AddCommand(versionCmd)
}
帮助信息
当然cobra这么优秀的框架肯定也有自动的帮助信息哈哈,我们只需要写好自己的功能代码即可。
最后我们的目录结构是:
爬虫功能(趁热打铁)
Goquery处理响应
Goquery爬虫必备包
下载
go get -u github.com/PuerkitoBio/goquery
使用
goquery.NewDocumentFromReader
负责解析文本内容返回*goquery.Document
对象*goquery.Document
对象根据你给的css选择器查找元素,这个查找的元素是将你这个css选择器在内容中所有元素都查找出来,所以不必担心只查找到一个而已。
随便爬一下百度的某个元素:
这里仅仅展示爬取一个元素,因为图片中教学的是复制完整的css选择器路径,更多css选择器自行去学习。
func testClimb() {
fmt.Println("测试爬取百度热点功能")
client := req.C()
data, _ := client.R().Get("https://top.baidu.com/board?top=realtime")
doc, _ := goquery.NewDocumentFromReader(data.Body) //解析网页
doc.Find("#sanRoot > main > div.hot-wrap_1nNog > div.theme-hot.category-item_1fzJW > div.list_1EDla > a:nth-child(7) > div.normal_1fQqB > div.content-wrap_1RisM > div > div").Each(func(i int, s *goquery.Selection) { //查找类名为c-single-text-ellipsis的元素
fmt.Println(strings.TrimSpace(s.Text())) //打印标题
})}
运行结果
编码处理
有一个单独处理某些编码的库,这个库有需要的自行学习句即可:
github.com/djimenez/iconv-go
这里仅仅展示一个比较通用的编码处理方式
go get -u golang.org/x/text
参考文章中作者已经写好了工具函数,拿来就用即可,我们写一些小工具来使用的话就不建议重复造轮子了,
使用方式:
utf8Body, err := DecodeHTMLBody(res.Body, "")
工具函数代码如下:
func detectContentCharset(body io.Reader) string {r := bufio.NewReader(body)if data, err := r.Peek(1024); err == nil {if _, name, _ := charset.DetermineEncoding(data, ""); len(name) != 0 {return name}}return "utf-8"
}func DecodeHTMLBody(body io.Reader, charset string) (io.Reader, error) {if charset == "" {charset = detectContentCharset(body)}e, err := htmlindex.Get(charset)if err != nil {return nil, err}if name, _ := htmlindex.Name(e); name != "utf-8" {body = e.NewDecoder().Reader(body)}return body, nil
}
参考文章的作者还找到一篇远古gbk编码的html网页来练习:
https://news.sina.com.cn/society/netsurvival/
charset.DetermineEncoding
会根据 HTML 页面中的 meta 元信息猜测网页编码。
由于我的终端编码类型是gb2312,不转换就能直接解析了,我转为utf8反而还乱码了,所以这里就不演示了
运行结果:(结尾会给出所有源码)
收集百度热搜榜
参考文章:https://darjun.github.io/2020/10/11/godailylib/goquery/
这里一样的功能,读取top榜单,但是我们读取的是榜单,所以不能直接复制单个元素了,要会一点选择器操作
三个class标签直接用.
符号来取至于子元素、后代元素用空格还是>
我还是简单说一下吧(忍不住)
空格:后代元素,即你孩子的孩子也能够匹配到
>
:子元素,仅仅代表你的子,即你生的下一代,不代表你下一代的下一代,所以只能取到下一层的元素。
我这里的结构其实用空格还是>
都行,因为结构比较简单
运行结果
函数功能如下:
func baiduHotspot() { // fmt.Println("爬取百度热点功能") client := req.C() data, _ := client.R().Get("https://top.baidu.com/board?top=realtime") doc, _ := goquery.NewDocumentFromReader(data.Body) //解析网页 doc.Find(".content-pos_1fT0H .name_2Px2N .c-single-text-ellipsis").Each(func(i int, s *goquery.Selection) { //查找类名为c-single-text-ellipsis的元素 title := s.Text() //获取文本内容 res := "\t" + strconv.Itoa(i) + ":" + strings.TrimSpace(title) fmt.Println(res) //打印标题 })}
爬虫功能所有源码
我把功能用到了cobra框架里
目录结构如下,记得创建文件
帮助命令
cmd/root.go文件
package cmdimport ( "bufio" "fmt" "io" "strconv" "strings" "github.com/PuerkitoBio/goquery" "github.com/imroc/req/v3" "github.com/spf13/cobra" "golang.org/x/net/html/charset" "golang.org/x/text/encoding/htmlindex")var ( P string //打印参数 PP string //全局打印参数 Climb bool //爬取百度热点功能 Testc bool //测试爬虫功能)func testClimb() { // fmt.Println("测试爬取百度热点功能") client := req.C() data, _ := client.R().Get("https://top.baidu.com/board?top=realtime") doc, _ := goquery.NewDocumentFromReader(data.Body) //解析网页 doc.Find("#sanRoot > main > div.hot-wrap_1nNog > div.theme-hot.category-item_1fzJW > div.list_1EDla > a:nth-child(7) > div.normal_1fQqB > div.content-wrap_1RisM > div > div").Each(func(i int, s *goquery.Selection) { //查找类名为c-single-text-ellipsis的元素 fmt.Println(strings.TrimSpace(s.Text())) //打印标题 })}func detectContentCharset(body io.Reader) string { r := bufio.NewReader(body) if data, err := r.Peek(1024); err == nil { if _, name, _ := charset.DetermineEncoding(data, ""); len(name) != 0 { return name } } return "utf-8"}func DecodeHTMLBody(body io.Reader, charset string) (io.Reader, error) { if charset == "" { charset = detectContentCharset(body) } e, err := htmlindex.Get(charset) if err != nil { return nil, err } if name, _ := htmlindex.Name(e); name != "utf-8" { body = e.NewDecoder().Reader(body) } return body, nil}func testClimb2() { client := req.C() data, _ := client.R().Get("https://news.sina.com.cn/society/netsurvival/") // 将 data.Body 转换为 io.Reader // decodedBody, _ := DecodeHTMLBody(bytes.NewReader(data.Bytes()), "") // 解码网页 doc, _ := goquery.NewDocumentFromReader(data.Body) // 解析网页 doc.Find(".title14 li").Each(func(i int, s *goquery.Selection) { // 微博72小时网络生存测试 fmt.Printf("%d:%s\n", i, strings.TrimSpace(s.Text())) // 打印标题 })}func baiduHotspot() { // fmt.Println("爬取百度热点功能") client := req.C() data, _ := client.R().Get("https://top.baidu.com/board?top=realtime") doc, _ := goquery.NewDocumentFromReader(data.Body) //解析网页 doc.Find(".content-pos_1fT0H > .name_2Px2N > .c-single-text-ellipsis").Each(func(i int, s *goquery.Selection) { //查找类名为c-single-text-ellipsis的元素 title := s.Text() //获取文本内容 res := "\t" + strconv.Itoa(i) + ":" + strings.TrimSpace(title) fmt.Println(res) //打印标题 })}var rootCmd = &cobra.Command{ Use: "命令名字", Short: "短描述", Long: `长描述`, Run: func(cmd *cobra.Command, args []string) { if P != "nil" { fmt.Println(P) } if PP != "nil" { fmt.Println(PP) } if Climb { fmt.Println("爬取百度热点功能") baiduHotspot() } if Testc { fmt.Println("测试爬虫功能") // testClimb() testClimb2() } },}func Execute() error { return rootCmd.Execute() //执行命令,这个是给main函数调用的}func init() { rootCmd.Flags().StringVarP(&P, "print", "p", "nil", "打印") //添加参数 rootCmd.PersistentFlags().StringVarP(&PP, "Print", "P", "nil", "全局打印") //添加全局参数 rootCmd.Flags().BoolVarP(&Climb, "climb", "c", false, "爬取百度热点功能") //添加爬取百度热点功能参数 rootCmd.Flags().BoolVarP(&Testc, "testc", "t", false, "测试爬虫功能") //添加测试爬虫功能}
cmd/version.go 文件
package cmdimport ( "fmt" "github.com/spf13/cobra")var versionCmd = &cobra.Command{ Use: "version", Short: "显示版本", Long: `显示xxxx版本`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("工具当前版本:v1.0.0") if PP != "nil" { fmt.Println(PP) } },}func init() { //将versionCmd添加到rootCmd //这样在执行命令时,就可以使用version这个子命令了 rootCmd.AddCommand(versionCmd)}
main.go 文件
package mainimport ( "go_cobra/cmd")func main() { cmd.Execute()}