Golang中Colly回调函数无法从go func接收变量的问题

Golang中Colly回调函数无法从go func接收变量的问题 基本上,情况是这样的:

x := "It worked earlyer lol"
go func(x string){
  c.onHTML(...){
    print(x)
  }
  c.visit(https://www.site.com)
}

这就是问题的核心。当我打印变量时,它们是空白的,但在 go 函数外部它们是有定义的。以下是完整的父函数:

eventCollector.OnHTML(".rgMasterTable tr", func(h *colly.HTMLElement) {
    eventName := h.ChildText("td:nth-child(3) a")
    eventURL := h.ChildAttr("td:nth-child(3) a", "href")
    state := h.ChildText("td:nth-child(2)")
    wgFR.Add(1)             // 为每个 goroutine 增加 WaitGroup 计数器
    semaphore <- struct{}{} // 获取一个令牌

    go func(eventName, eventURL, state string) {
       defer wgFR.Done() // goroutine 退出时发出完成信号
       defer func() { <-semaphore }()
       contestCollector := eventCollector.Clone()
       var postedDateStr string
       contestCollector.OnHTML("#ctl00_ContentPlaceHolder1_FormView1_Report_2Label", func(d *colly.HTMLElement) {
          postedDateStr = d.Text
       })

       contestCollector.OnHTML(".rgMasterTable tr", func(c *colly.HTMLElement) { // 出问题的行

          contestName := c.ChildText("td:nth-child(1)")
          contestURL := c.ChildAttr("td:nth-child(3) a", "href")
          if contestURL == "" {
             contestURL = "FORMAT-ERROR"
          } // 文档样式结果的临时处理程序
          postedDate, timeErr := time.Parse("Jan 2, 2006", postedDateStr)
          if timeErr != nil {
             log.Printf("Error parsing time from %s", eventURL)
          }
          contest := Contest{
             EventName:   eventName,
             ContestName: contestName,
             PostedDate:  postedDate,
             ContestURL:  contestURL,
             State:       state,
             Present:     true,
          }
          fResults = append(fResults, contest)
       })
       err := contestCollector.Visit("https://www.judgingcard.com/Results/" + eventURL)
       if err != nil {
          log.Printf("Could not find event: %s -- %s", eventURL, eventName)
       }
       contestCollector.Wait() // 等待内部收集器完成
    }(eventName, eventURL, state)
})

在完整版本中,所有传递给 go 函数的变量在回调函数(contestCollector.OnHTML)内部都返回空白值。不幸的是,我不确定问题是在 goroutine 中,还是因为它是回调函数,或者是其他原因。 提前感谢!


更多关于Golang中Colly回调函数无法从go func接收变量的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

这看起来相当复杂。看起来你正在使用 github.com/gocolly/colly。为什么那个部分要放在一个 goroutine 里呢?我建议先尝试去掉它。看起来收集器本身已经在使用 goroutines 了(我只是猜测,因为它们有一个 Wait 函数;我没有查阅文档来确认这一点)。

这段代码看起来缺少一些内容(比如你在哪里等待 wgFRwgFR 甚至是在哪里定义的?),所以我的第一反应是,可能有些东西正在修改你外部函数中的字符串,而这可能是你没有预料到的。在有问题的 contestCollector.OnHTML 回调中,你没有将这些值传递给那个函数,所以它可能遇到了类似这样的问题:

func main() {
	myAwesomeValue := "awesome"
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		time.Sleep(time.Millisecond)
		fmt.Println("The value is", myAwesomeValue)
		wg.Done()
	}()
	myAwesomeValue = "not awesome"
	wg.Wait()
}

… 这段代码会打印出 The value is not awesome

更多关于Golang中Colly回调函数无法从go func接收变量的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢您,Colly框架确实有它自己的管理器,不需要定义新的等待组。根据我所学到的知识,我构建了这个测试脚本,它按预期工作。

package main

import (
	"fmt"
	"github.com/gocolly/colly"
)

type Event struct {
	event_date   string
	state        string
	event_name   string
	event_url    string
	contest_name string
	contest_url  string
}

func main() {
	var events []Event

	ECollector := colly.NewCollector(colly.Async(true))
	ECollector.Limit(&colly.LimitRule{DomainGlob: "*", Parallelism: 2})

	ECollector.OnHTML(".rgMasterTable tr", func(e *colly.HTMLElement) {
		if e.Index > 5 {
			return
		}

		date := e.ChildText("td:nth-child(1)")
		state := e.ChildText("td:nth-child(2)")
		name := e.ChildText("td:nth-child(3)")
		url := e.ChildAttr("td:nth-child(3) a", "href")

		event := Event{
			event_date: date,
			state:      state,
			event_name: name,
			event_url:  url,
		}
		events = append(events, event)
	})

	ECollector.Visit("https://www.judgingcard.com")
	ECollector.Wait()

	CCollector := ECollector.Clone()

	for _, event := range events {
		CCollector.OnHTML(".rgMasterTable tr", func(c *colly.HTMLElement) {
			//skip header line
			if c.Index == 0 || c.Request.URL.String() == "https://www.judgingcard.com/Results/default.aspx" {
				return
			}
			//More contests than events/build new struct
			contest_name := c.ChildText("td:nth-child(1)")
			contest_url := c.ChildAttr("td:nth-child(3) a", "href")
			
			fmt.Println(contest_name, contest_url)
		})
		CCollector.Visit("https://www.judgingcard.com" + event.event_url)
	}
	CCollector.Wait()
}

这是一个典型的闭包变量捕获问题。在Go中,当你在goroutine中使用闭包时,需要特别注意变量的作用域和生命周期。

问题在于你的内部contestCollector.OnHTML回调函数捕获的是外部变量,而不是goroutine参数。当goroutine执行时,外部变量可能已经被修改或重用。

以下是修复后的代码:

eventCollector.OnHTML(".rgMasterTable tr", func(h *colly.HTMLElement) {
    eventName := h.ChildText("td:nth-child(3) a")
    eventURL := h.ChildAttr("td:nth-child(3) a", "href")
    state := h.ChildText("td:nth-child(2)")
    wgFR.Add(1)
    semaphore <- struct{}{}

    go func(eventName, eventURL, state string) {
        defer wgFR.Done()
        defer func() { <-semaphore }()
        
        // 将参数复制到局部变量,确保闭包捕获的是正确的值
        localEventName := eventName
        localEventURL := eventURL
        localState := state
        
        contestCollector := eventCollector.Clone()
        var postedDateStr string
        
        contestCollector.OnHTML("#ctl00_ContentPlaceHolder1_FormView1_Report_2Label", func(d *colly.HTMLElement) {
            postedDateStr = d.Text
        })

        contestCollector.OnHTML(".rgMasterTable tr", func(c *colly.HTMLElement) {
            contestName := c.ChildText("td:nth-child(1)")
            contestURL := c.ChildAttr("td:nth-child(3) a", "href")
            if contestURL == "" {
                contestURL = "FORMAT-ERROR"
            }
            
            postedDate, timeErr := time.Parse("Jan 2, 2006", postedDateStr)
            if timeErr != nil {
                log.Printf("Error parsing time from %s", localEventURL)
            }
            
            contest := Contest{
                EventName:   localEventName,   // 使用局部变量
                ContestName: contestName,
                PostedDate:  postedDate,
                ContestURL:  contestURL,
                State:       localState,       // 使用局部变量
                Present:     true,
            }
            fResults = append(fResults, contest)
        })
        
        err := contestCollector.Visit("https://www.judgingcard.com/Results/" + localEventURL)
        if err != nil {
            log.Printf("Could not find event: %s -- %s", localEventURL, localEventName)
        }
        contestCollector.Wait()
    }(eventName, eventURL, state)
})

更简洁的写法是直接在闭包中使用参数:

contestCollector.OnHTML(".rgMasterTable tr", func(c *colly.HTMLElement) {
    contestName := c.ChildText("td:nth-child(1)")
    contestURL := c.ChildAttr("td:nth-child(3) a", "href")
    if contestURL == "" {
        contestURL = "FORMAT-ERROR"
    }
    
    postedDate, timeErr := time.Parse("Jan 2, 2006", postedDateStr)
    if timeErr != nil {
        log.Printf("Error parsing time from %s", eventURL)  // 直接使用参数
    }
    
    contest := Contest{
        EventName:   eventName,  // 直接使用参数
        ContestName: contestName,
        PostedDate:  postedDate,
        ContestURL:  contestURL,
        State:       state,      // 直接使用参数
        Present:     true,
    }
    fResults = append(fResults, contest)
})

问题的根本原因是:当外部eventCollector.OnHTML回调被多次调用时(每行调用一次),所有的goroutine共享相同的变量名,但实际捕获的是循环迭代中的变量引用。通过将参数复制到局部变量或直接在闭包中使用参数,可以确保每个goroutine捕获自己独立的值副本。

回到顶部