使用 go-colly 解析 HTML 并函数 returns 一个空切片

Parsing HTML with go-colly and function returns an empty slice

我正在使用 colly 框架解析网站,但出现错误。我有一个非常基本的功能 getweeks() 可以抓取和 return 一些东西,但我得到的是一个空切片。

func getWeeks(c *colly.Collector) []string {
    var wks []string
    c.OnHTML("div.ltbluediv", func(div *colly.HTMLElement) {
        weekName := div.DOM.Find("span").Text()  // a string Week 1, Week 2 etc 
        wks = append(wks, weekName)  // weekName has actual value is not empty
        // If `wks` printed here it shows correctly how the slice gets populated on each iteration
    })
    return wks  // returns []
}

func main() {
    c := colly.NewCollector(
    )

    w := getWeeks(c)
    fmt.Println(w)  // []

    c.OnRequest(func(r *colly.Request) {
        r.Headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64)")
    })

    c.Visit("target url")

}

tl;dr:切片 header 在 OnHTML 回调中更新,但您在 main 中打印的值是旧值切片 header。您应该改用 *[]string


首先,您传递给 c.OnHTML 的回调实际上只会在您调用 c.Visit 之后 运行,因此在 getWeeks 之后立即打印 w , 在任何情况下都会显示一个空切片。

然而,即使在 c.Visit 之后打印它也会是一个空切片,为什么?

Go 中的切片被实现为一种数据结构 — 称为切片 header(更多信息:1, 2)。

当您分配 getWeeks 的 return 值时,您实际上是在复制切片 header,包括其字段 DataLenCap。您可以通过使用 %p 动词打印切片的地址来在 this playground 中看到它(使用其他一些结构而不是 go-colly 来制作示例 self-contained):

func getWeeks(c *Foo) []string {
    var wks []string
    c.OnHTML("div.ltbluediv", func(text string) {
        weekName := text
        wks = append(wks, weekName)
    })
    fmt.Printf("%p\n", &wks)
    return wks
}

func main() {
    c := &Foo{}

    w := getWeeks(c)

    c.Visit("target url")
    fmt.Printf("%p\n", &w)

}

打印两个不同的内存地址:

0xc0000ac030
0xc0000ac018

现在,如果您继续在 Stack Overflow 上搜索 slice 和 append 行为,您可能会发现如果 slice 具有足够的容量 (1, 2, 3),则不会重新分配后备数组。

然而,即使您通过使用足够的容量初始化 wks 来确保后备数组相同,w 的值仍然是原始切片 header 的副本,因此 长度为 0。这在 this playground 中进行了演示,它打印出:

in getWeeks reflect.SliceHeader{Data:0xc0000121b0, Len:0, Cap:3}
in callback reflect.SliceHeader{Data:0xc0000121b0, Len:1, Cap:3}
in callback reflect.SliceHeader{Data:0xc0000121b0, Len:2, Cap:3}
in callback reflect.SliceHeader{Data:0xc0000121b0, Len:3, Cap:3}
[]
in main reflect.SliceHeader{Data:0xc0000121b0, Len:0, Cap:3}

您可以通过重新切片 (playground) 来调整 w 的长度:

c.Visit("target url")
w = w[0:3]
fmt.Println(w) // [foo bar baz]

但这意味着您需要事先知道不会导致重新分配的合理容量,以及重新分片的最终长度。

相反,return 一个指向切片的指针:

func getWeeks(c *colly.Collector) *[]string {
    wks := &[]string{}
    c.OnHTML("div.ltbluediv", func(div *colly.HTMLElement) {
        weekName := div.DOM.Find("span").Text()
        *wks = append(*wks, weekName) 
    })
    return wks
}

或者传递一个指针到getWeeks:

func getWeeks(c *colly.Collector, wks *[]string) {
    c.OnHTML("div.ltbluediv", func(div *colly.HTMLElement) {
        weekName := div.DOM.Find("span").Text()
        *wks = append(*wks, weekName)
    })
}

固定游乐场:https://go.dev/play/p/yhq8YYnkFsv