前期准备

本次为了方便解析html,快速提取登陆时需要的一些数据,类似python中的bs4,而不是采用正则表达式去匹配数据,所以会用到一个新的库 goqueryhttps://github.com/PuerkitoBio/goquery
安装:

  1. go get -v github.com/PuerkitoBio/goquery

goquery基础用法

[!NOTE]

大部分内容匹配规则和jquery类似

https://cloud.ctfd.io/login 页面为例

创建document实例

  1. req, _ := http.NewRequest("GET", "https://cloud.ctfd.io/login", nil)
  2. req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36")
  3. client := &http.Client{}
  4. resp, _ := client.Do(req)
  5. // 参数类型为 io.Reader
  6. dom, _ := goquery.NewDocumentFromReader(resp.Body)

标签选择器

这个比较简单,就是基于a,p等这些HTML的基本元素进行选择,这种直接使用Element名称作为选择器即可。比如dom.Find("div")
实例:找到所有的a标签,并循环获取值(不是属性,是标签中的值)

  1. dom.Find("a").Each(func(i int, selection *goquery.Selection) {
  2. fmt.Println(selection.Text())
  3. })

image-20211230103948054

  1. Features
  2. Pricing
  3. Store
  4. Contact
  5. Login
  6. Sign
  7. Up
  8. Don't have an account? Sign Up # 和截图匹配的这一条
  9. Forgot password?
  10. Exiting.

ID选择器

通过ID定位到元素,算是最常用的之一
实例:比如我们要定位到id="navbarResponsive"的元素,输出它的源码,不过需要注意的是,输出源码并不会输出当前的标签

  1. selection := dom.Find("#navbarResponsive")
  2. // selection := dom.Find("div#navbarResponsive") // 同时匹配标签和id,更准确
  3. fmt.Println(selection.Html())

image-20211230104545596

  1. <ul class="navbar-nav ml-auto">
  2. <li class="nav-item ">
  3. <a class="nav-link" href="https://ctfd.io/features">Features</a>
  4. </li>
  5. <li class="nav-item btn-group">
  6. <a class="nav-link" href="https://ctfd.io/hosting">Pricing</a>
  7. </li>
  8. <li class="nav-item ">
  9. <a class="nav-link" href="https://ctfd.io/store">Store</a>
  10. </li>
  11. <li class="nav-item btn-group">
  12. <a class="nav-link" href="https://ctfd.io/contact">Contact</a>
  13. </li>
  14. <li class="nav-item ">
  15. <a class="nav-link" href="/login">Login</a>
  16. </li>
  17. <li class="nav-item ">
  18. <a class="nav-link btn btn-outline-secondary" role="button" href="/signup">Sign
  19. Up</a>
  20. </li>
  21. </ul>
  22. <nil>

class选择器

它的用法和ID选择器类似,为Find(".class")
实例:寻找所有class="nav-link元素,并输出他们的值

  1. dom.Find(".nav-link").Each(func(i int, selection *goquery.Selection) {
  2. fmt.Println(selection.Text())
  3. })

[!TIP]

也可以组合,如 a.nav-link:寻找所有a标签且class为nav-link的元素

image-20211230105058155

  1. Features
  2. Pricing
  3. Store
  4. Contact
  5. Login
  6. Sign
  7. Up

属性选择器

上面3种有时候还不够用,这个时候就需要属性选择器来帮忙了
实例:获取input标签中属性namenonce的元素的value

[!Note]

就是获取红框中的值,这里放图了下面就不放了

image-20211230105447372

  1. res, exist := dom.Find("input[name=nonce]").Attr("value")
  2. if exist {
  3. fmt.Println(res) // YidceDA0XHhhZVx4YmE9XHgxNFx4MTVceDhlXHgxNC9ceGUyJw==
  4. }

补充:
除了完全相等,还有其他匹配方式,使用方式类似,这里统一列举下,不再举例

| 选择器 | 说明 | | —- | —- |

| Find(“div[lang]”) | 筛选含有lang属性的div元素 |

| Find(“div[lang=zh]”) | 筛选lang属性为zh的div元素 |

| Find(“div[lang!=zh]”) | 筛选lang属性不等于zh的div元素 |

| Find(“div[lang¦=zh]”) | 筛选lang属性为zh或者zh-开头的div元素 |

| Find(“div[lang*=zh]”) | 筛选lang属性包含zh这个字符串的div元素 |

| Find(“div[lang~=zh]”) | 筛选lang属性包含zh这个单词的div元素,单词以空格分开的 |

| Find(“div[lang$=zh]”) | 筛选lang属性以zh结尾的div元素,区分大小写 |

| Find(“div[lang^=zh]”) | 筛选lang属性以zh开头的div元素,区分大小写 |

以上是属性筛选器的用法,都是以一个属性筛选器为例,当然你也可以使用多个属性筛选器组合使用,比如: Find("div[id][lang=zh]"),用多个中括号连起来即可。当有多个属性筛选器的时候,要同时满足这些筛选器的元素才能被筛选出来

内容提取

获取到了标签,当然就像获取到里面的值了

parent>child子选择器

[!NOTE]

上面的基本都够用了,这里再列举一些可能会用到的筛选器

如果我们想筛选出某个元素下符合条件的子元素,我们就可以使用子元素筛选器,它的语法为Find("parent>child"),表示筛选parent这个父元素下,符合child这个条件的最直接(一级)的子元素。
实例:form标签下的input标签的属性value的值

  1. res, exist := dom.Find("form>input").Attr("value")
  2. if exist {
  3. fmt.Println(res) // YiJceGU0YTxceGY3alx4MGYnVVx4ZDdceGNlIg==
  4. }

image-20211230110237863

prev+next相邻选择器

假设我们要筛选的元素没有规律,但是该元素的上一个元素有规律,我们就可以使用这种下一个相邻选择器来进行选择。
实例:h2标签旁边的p标签的值

[!TIP]

如果class的值存在空格,那么可以用属性的格式来匹配,防止空格影响结果

  1. dom.Find("h2[class='block-title text-center']+p.text-center").Each(func(i int, selection *goquery.Selection) {
  2. fmt.Println(selection.Text()) // Don't have an account? Sign Up
  3. })

image-20211230111816587

prev~next兄弟选择器

有相邻就有兄弟,兄弟选择器就不一定要求相邻了,只要他们共有一个父元素就可以。
实例:获取lable标签的兄弟标签input

  1. dom.Find("label~input").Each(func(i int, selection *goquery.Selection) {
  2. fmt.Println(selection.Attr("name")) // email true
  3. })

image-20211230112542627

内容过滤器

有时候我们使用选择器选择出来后,希望再过滤一下,这时候就用到过滤器了
实例:获取包含内容Email的label标签

  1. dom.Find("label:contains(Email)").Each(func(i int, selection *goquery.Selection) {
  2. fmt.Println(selection.Text()) // Email Address
  3. })

image-20211230112929355
扩展:

  1. Find(":contains(text)")表示筛选出的元素要包含指定的文本
  2. Find(":empty")表示筛选出的元素都不能有子元素(包括文本元素),只筛选那些不包含任何子元素的元素
  3. Find(":has(selector)")contains差不多,只不过这个是包含的是元素节点

    :first-of-type过滤器

    :first-child选择器限制的比较死,必须得是第一个子元素,如果该元素前有其他在前面,就不能用:first-child了,这时候:first-of-type就派上用场了,它要求只要是这个类型的第一个就可以
    实例:输出第一个div标签的源码 ``` ret, _ := dom.Find(“div:first-of-type”).Html() fmt.Println(ret)
  1. **扩展:**<br />`:last-child` `:last-of-type`过滤器正好和上面的2歌过滤器相反,表示最后一个过滤器
  2. ### :nth-child(n) 过滤器系列
  3. 表示筛选出的元素是其父元素的第n个元素,n1开始。所以我们可以知道`:first-child``:nth-child(1)`是相等的。通过指定`n`,我们就很灵活的筛选出我们需要的元素<br />同样,`:nth-of-type(n)` `:nth-child(n)` 类似,只不过它表示的是同类型元素的第n个,所以`:nth-of-type(1)` `:first-of-type`是相等的<br />`nth-last-child(n)` `:nth-last-of-type(n)` 过滤器是倒序开始计算的,最后一个元素被当成了第一个
  4. > [!WARNING]
  5. > 都不举例了,没必要
  6. ### :only-child 过滤器系列
  7. `Find(":only-child")` 过滤器,从字面上看,可以猜测出来,它表示筛选的元素,在其父元素中,只有它自己,它的父元素没有其他子元素,才会被匹配筛选出来。<br />`:only-of-type` 过滤器和其他的类似,同类型元素只要只有一个,就可以被筛选出来
  8. ### 选择器或(|)运算
  9. 如果我们想同时筛选出`div`,`span`等元素怎么办?这时候可以采用多个选择器进行组合使用,并且以逗号(,)分割,`Find("selector1, selector2, selectorN")`表示,只要满足其中一个选择器就可以被筛选出来,也就是选择器的或(|)运算操作。<br />**实例:**筛选出所有的`meta`标签和`input`标签,并且获取到其属性`name`的值
  1. dom.Find("meta,input").Each(func(i int, selection *goquery.Selection) {
  2. val, exists := selection.Attr("name")
  3. if exists {
  4. fmt.Println(val)
  5. }
  6. })
  1. ![image-20211230130647349](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646986862729-d6de8644-6373-4f30-a0f3-b159bdc0691c.png)

viewport description author twitter:card twitter:site twitter:creator twitter:title twitter:description twitter:image email password nonce

  1. ### 补充说明
  2. 1、类似函数的位置操作
  3. |
  4. 方法
  5. | 说明
  6. |
  7. | --- | --- |
  8. |
  9. `Find(selection) *Selection`
  10. | 根据选择器查找节点集
  11. |
  12. |
  13. `Eq(index int) *Selection`
  14. | 根据索引获取某个节点集
  15. |
  16. |
  17. `First() *Selection`
  18. | 获取第一个子节点集
  19. |
  20. |
  21. `Last() *Selection`
  22. | 获取最后一个子节点集
  23. |
  24. |
  25. `Next() *Selection`
  26. | 获取下一个兄弟节点集
  27. |
  28. |
  29. `NextAll() *Selection`
  30. | 获取后面所有兄弟节点集
  31. |
  32. |
  33. `Prev() *Selection`
  34. | 前一个兄弟节点集
  35. |
  36. |
  37. `Get(index int) *html.Node`
  38. | 根据索引获取一个节点
  39. |
  40. |
  41. `Index() int`
  42. | 返回选择对象中第一个元素的位置
  43. |
  44. |
  45. `Slice(start, end int) *Selection`
  46. | 根据起始位置获取子节点集
  47. |
  48. 2、循环遍历选择的节点
  49. |
  50. 方法
  51. | 说明
  52. |
  53. | --- | --- |
  54. |
  55. `Each(f func(int, *Selection)) *Selection`
  56. | 遍历
  57. |
  58. |
  59. `EachWithBreak(f func(int, *Selection) bool) *Selection`
  60. | 可中断遍历
  61. |
  62. |
  63. `Map(f func(int, *Selection) string) (result []string)`
  64. | 返回字符串数组
  65. |
  66. 3、检测或获取节点属性值
  67. |
  68. 方法
  69. | 说明
  70. |
  71. | --- | --- |
  72. |
  73. `Attr(), RemoveAttr(), SetAttr()`
  74. | 获取,移除,设置属性的值
  75. |
  76. |
  77. `AddClass(), HasClass(), RemoveClass(), ToggleClass()`
  78. | 类相关
  79. |
  80. |
  81. `Html()`
  82. | 获取该节点的html
  83. |
  84. |
  85. `Length()`
  86. | 返回该Selection的元素个数
  87. |
  88. |
  89. `Text()`
  90. | 获取该节点的文本值
  91. |
  92. 4 在文档树之间来回跳转(常用的查找节点方法)
  93. |
  94. 方法
  95. | 说明
  96. |
  97. | --- | --- |
  98. |
  99. `Children()`
  100. | 返回selection中各个节点下的孩子节点
  101. |
  102. |
  103. `Contents()`
  104. | 获取当前节点下的所有节点
  105. |
  106. |
  107. `Find()`
  108. | 查找获取当前匹配的元素
  109. |
  110. |
  111. `Next()`
  112. | 下一个元素
  113. |
  114. |
  115. `Prev()`
  116. | 上一个元素
  117. |
  118. ## Cookie自动保存更新
  119. > [!NOTE]
  120. > 大家都知道,网站登陆后肯定有个用来鉴权的东西,而Cookietoken居多,这里我们讲一下用Cookie
  121. > [!DANGER]
  122. > 网上直接搜go模拟登陆,但是出来的代码都是登陆后手动设置reqcookie,而不会自动的更新cookie,既然python里面都有`request.Session`,那golang里面肯定也有类似的东西吧!
  123. 结合之前的经验,发现在创建客户端的时候,会传入一个`CookieJar`,这玩意儿根据经验肯定是用来存放Cookie的<br />![image-20211230132216424](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646986863954-8d2ac893-0768-499c-8a84-19814d6ecabb.png)<br />试一下便知<br />根据经验,第一次访问百度的时候,百度会给咱们分配cookie,那咱们就可以用它来试试能不能自动保存更新Cookie<br />![image-20211230132720546](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646986866724-26242fcf-20ff-4a17-9f1a-c6902cf33d44.png)<br />然后根据它的参数构造一个`CookieJar`出来

jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})

  1. 再创建个http客户端给它放进去,发起请求,查看结果

package main

import ( “fmt” “golang.org/x/net/publicsuffix” “net/http” “net/http/cookiejar” )

func main() { // 创建客户端 jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) client := http.Client{Jar: jar}

  1. fmt.Printf("访问前:\n")
  2. fmt.Println(client.Jar)
  3. client.Get("https://www.baidu.com")
  4. fmt.Printf("\n\n访问后:\n")
  5. fmt.Println(client.Jar)

}

  1. 可以看到我们的Cookie成功自动保存了,那就说明之前的猜想是对的<br />![image-20211230133024231](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646986868060-7773e0a8-be3b-45cc-881d-7a3902347b88.png)<br />后面对百度发起访问,就会自动带上Cookie了
  2. ## 实战
  3. 实战模拟登陆CTFD平台:[https://cloud.ctfd.io/login](https://cloud.ctfd.io/login)<br />主要分为3步:
  4. 1. 获取nonce
  5. 1. 登陆
  6. 1. 验证登陆是否成功
  7. **完整代码:**

package main

import ( “fmt” “github.com/PuerkitoBio/goquery” “golang.org/x/net/publicsuffix” “io/ioutil” “net/http” “net/http/cookiejar” “net/url” “strings” )

// 登陆账号密码 var Config = map[string]string{ “email”: “yeciyar420@zherben.com”, “passwd”: “xxxxx”, } // 登陆客户端 var Client http.Client

/

  1. 获取登陆需要的Nonce,同时初始化客户端 */ func getNonce() string { req, _ := http.NewRequest(“GET”, “https://cloud.ctfd.io/login“, nil) req.Header.Set(“User-Agent”, “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36”)

    // 初始化client jar, := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) Client = http.Client{Jar: jar} resp, := Client.Do(req)

    dom, := goquery.NewDocumentFromReader(resp.Body) val, := dom.Find(“input[name=nonce]”).Attr(“value”) return val }

/

  1. 登陆 */ func login(nonce string) (bool) { // 构造请求 param := url.Values{} param.Set(“email”, Config[“email”]) param.Set(“password”, Config[“passwd”]) param.Set(“nonce”, nonce) data := param.Encode() req, _ := http.NewRequest(“POST”, “https://cloud.ctfd.io/login“, strings.NewReader(data)) req.Header.Set(“authority”, “cloud.ctfd.io”) req.Header.Set(“content-type”, “application/x-www-form-urlencoded”) req.Header.Set(“user-agent”, “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36”) req.Header.Set(“referer”, “https://cloud.ctfd.io/admin“)

    // 发起请求 resp, := Client.Do(req) source, := ioutil.ReadAll(resp.Body) if strings.Contains(string(source), “Your password is wrong”){

    1. fmt.Println("账号或密码错误")
    2. return false

    } else {

    1. return true

    } }

/

  1. 验证是否登陆成功 */ func getInfo() { req, _ := http.NewRequest(“GET”, “https://cloud.ctfd.io/profile“, nil) req.Header.Set(“authority”, “cloud.ctfd.io”) req.Header.Set(“content-type”, “application/x-www-form-urlencoded”) req.Header.Set(“user-agent”, “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36”) req.Header.Set(“referer”, “https://cloud.ctfd.io/admin“)

    resp, _ := Client.Do(req) if resp.StatusCode == 200 {

    1. dom, _ := goquery.NewDocumentFromReader(resp.Body)
    2. val, exists := dom.Find("#name-input").Attr("value")
    3. if exists {
    4. fmt.Printf("Success, Login as %s\n", val)
    5. }

    } }

func main() { nonce := getNonce() fmt.Println(“Nonce: “, nonce)

  1. res := login(nonce)
  2. if res {
  3. fmt.Println("登陆成功,尝试获取个人信息...")
  4. getInfo()
  5. }

}

``` 效果:
image-20211230141529952