写在前面
静态网页也就是网页中没有程序代码,只有HTML,静态网页一旦写成,格式就不会变化。而动态网页是除了HTML,还有特定功能的程序代码。当我们通过浏览器与服务器进行交互的时候,服务器根据我们的请求动态的返回数据并加载网页,该过程也就是所谓的页面渲染。
因为服务器不会一次性的返回所有页面内容,所以很多数据并不在网页的源代码中,因此也就不能通过简单的http.Get()来爬取页面数据。
以我项目需要爬取的网站为例www.fifa.com/tournaments…
我们可以看到,在返回请求的Body里面是空值,并没有我们在网页源代码中所见的数据。因此我们需要动态爬取存放在服务器上的数据。抓取动态数据的方法一般有两种:(1)通过JavaScript逆向工程获取动态数据接口(真实的访问路径)——通过API爬取数据。(2)利用第三方库模拟真实浏览器,获取JavaScript渲染后的内容。本文主要讨论第二种方法:
方法一:Golang+chromedp+goquery
chromedp 本质上就是一个浏览器调度包,它与Selenium非常类似,但是使用起来更加方便,更易拓展,而且对golang更加友好
go get -u github.com/chromedp/chromedp //安装chromedp
使用chromedp(首先要有chrome浏览器),我们可以将需要浏览的网站的url传入,然后等待网页把全部的动态js执行完毕,生成最后的html文件,这时再获取其中的元素,便可以读取所有的元素.
func GetHttpHTML(url string, selector string) (tmp [][]string) {
options := []chromedp.ExecAllocatorOption{
chromedp.Flag("headless", true), //设置成无浏览器弹出模式
chromedp.Flag("blink-settings", "imageEnable=false"),
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"),
}
c, _ := chromedp.NewExecAllocator(context.Background(), options...)
chromeCtx, cancel := chromedp.NewContext(c, chromedp.WithLogf(log.Printf))
_ = chromedp.Run(chromeCtx, make([]chromedp.Action, 0, 1)...)
timeOutCtx, cancel := context.WithTimeout(chromeCtx, 60*time.Second)
defer cancel()
var htmlContent string
err := chromedp.Run(timeOutCtx,
chromedp.Navigate(url),
//需要爬取的网页的url
//chromedp.WaitVisible(`#content > div > section.fp-tournament-award-badge-carousel_awardBadgeCarouselSection__w_Ys5 > div > div > div.col-12.fp-tournament-award-badge-carousel_awardCarouselColumn__fQJLf.g-0 > div > div > div > div > div > div > div.slick-slide.slick-active.slick-current > div > div > div`),
chromedp.WaitVisible(selector),
//等待某个特定的元素出现
chromedp.OuterHTML(`document.querySelector("body")`, &htmlContent, chromedp.ByJSPath),
//生成最终的html文件并保存在htmlContent文件中
)
if err != nil {
log.Fatal(err)
}
log.Println(htmlContent)
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
awardTmp := [][]string{}
doc.Find(`div[class="fp-tournament-award-badge_awardContent__dUtoO"]`).Each(func(i int, selection *goquery.Selection) {
award := selection.Find(`p[class="fp-tournament-award-badge_awardName__JpsZZ"]`).Text()
//goquery通过Find()查找到我们选择的位置,Each()的功能与遍历相似返回所有的结果,Text()返回文本内容
name := selection.Find(`h4[class=" fp-tournament-award-badge_awardWinner__P_z2d"]`).Text()
country := selection.Find(`p[class="fp-tournament-award-badge_awardWinnerCountry__EmjVU"]`).Text()
awardTmp = append(awardTmp, []string{award, name, country})
})
return awardTmp
}
函数需要传入第一个值就是我们需要爬取的网页,而第二个值selector即为我们爬取的数据对应的选 html择器 , 通过谷歌浏览器进入网站,按 F12 -> 点击左上角的鼠标 -> 再点击我们需要爬取的数据 -> 就可以看到实际的html源码(目前看到的是通过 javascript 动态生成数据后的)
我需要爬取的是世界杯奖项对应的球员以及国家,选择此处位置作为我们的selector。
此时,htmlContext中就已经保存了经过数据渲染之后完整的页面信息。接下来就可以通过goquery选择出我们需要的数据,我将其保存在二维数组中方便下一步处理
大功告成~~
方法二:通过selenium来模拟浏览器发送请求
go get github.com/tebeka/selenium //安装selenium库
另外,我们还需要安装webdriver,通过chrome://version
可以查询到我们浏览器的版本
下载与之对应版本的chromedriver并存放到chrome的安装位置
opts := []selenium.ServiceOption{}
caps := selenium.Capabilities{
"browserName": "chrome",
}
imageCaps := map[string]interface{}{
"profile.managed_default_content_settings.images": 2,
}
chromeCaps := chrome.Capabilities{
Prefs: imageCaps,
Path: "",
Args: []string{
"--headless", //Chrome无头模式
"--no-sandbox",
"--User-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"--ignore-certificate-errors",
"--ignore -ssl-errors",
},
}
caps.AddChrome(chromeCaps)
//启动Chromedriver
service, err := selenium.NewChromeDriverService(
"path/to/chromedriver", 8080, opts...,
)
defer service.Stop()
if err != nil {
fmt.Println(err)
return "", err
}
//调起Chrome浏览器
webDriver, err := selenium.NewRemote(caps, fmt.Sprintf("http://localhost:%d/wd/hub", 8080))
if err != nil {
fmt.Println(err)
return "", err
}
defer webDriver.Quit()
//导航到目标网站
err = webDriver.Get(url)
if err != nil {
fmt.Println(err)
return "", err
}
//selenium提供了多种选择器(ByTagName, ByClassName, ByCSSSelector)帮助我们选择
outputDiv, err := webDriver.FindElement(selenium.ByTagName, "span")
结语
通过以上两种方法我们就可以爬取到需要动态加载的数据