从零开始的 Go 爬虫框架编程实战 - 中篇
目录
这是一篇讲解如何编写 Go 爬虫框架的编程实战系列长文,是一个爬虫框架程序从无到有的成长进化史。
6. 增加通用获取属性的封装
在上一节的实战后,tidy
已经具备了基本爬虫框架,增加站点配置就可以支持新的站点爬虫。但是只支持文本查询是不够的,例如我们需要获取 <a href="value">
和 <img src="value">
中的 href
及 src
属性。我们在之前已经 Selector.Item.Attr(doc)
支持了属性的查找,需要将这个能力暴露给上次的配置清单。
首先在 Item
结构体中增加一个 attribute
的字段,用来保存清单配置的属性参数。为了结构或接口规范,0保持统一,我将 replacer
也提取进来。
type Item struct {
selector string
attribute string
replacer *strings.Replacer
}
通过如下方法配置这两个字段:
func (selector Item) replace(oldNew ...string) Item {
selector.replacer = strings.NewReplacer(oldNew...)
return selector
}
func (selector Item) attr(attr string) Item {
selector.attribute = attr
return selector
}
之后只需要通过 selector("a").attr("href")
方法配置,即可获取选择 a
标签并提取 href
属性的操作。
再新增一个 fc2.go 的站点,先看升级后最终的配置清单:
func Fc2(id string) Site {
return Site{
Url: fmt.Sprintf("https://adult.contents.fc2.com/article/%s/", id),
UserAgent: MobileUserAgent,
Selector: Selector{
Title: selector(".items_article_MainitemNameTitle"),
Actor: selector(".items_article_seller").replace("by ", ""),
Poster: selector("meta[property^=\"og:image\"]").attr("content"),
Producer: selector(".items_article_seller").replace("by ", ""),
Sample: selector(".main-video").attr("src"),
Series: selector(".items_article_seller").replace("by ", ""),
Release: selector(".items_article_Releasedate").replace("販売日 : ", ""),
Duration: selector(".items_article_MainitemThumb > p"),
Id: selector(".items_article_TagArea").attr("data-id"),
Label: selector("null"),
Genre: selector("null"),
Images: selector("li[data-img^=\"https://storage\"]").attr("data-img"),
},
}
}
可以从这里看到 Selector -> Item
结构体增加了 replace()
和 attr()
函数,有了文本替换和获取标签属性的能力。这是对选择器的第一次升级,可以在这里看到完成的 commits 记录。此时 Selector
已经有完整的扩展能力,为了结构明了,可以单独提取到 selector.go 中了。
继续增加一个 fc2club.go 的站点配置清单,可以看到爬虫代码无需做任何修改已经支持了匹配查询。
gb2312
euc-jp
等编码支持
7. 增加 当前爬虫爬取到的网站内容都是使用 uft-8
编码的,在抓取 Carib
站点信息时,出现了乱码的问题,联想到了之前使用 Python
写爬虫抓取另一个站点时遇到的 euc-jp
解析错误问题。有了之前 Python
编程经验,这一次很快就定位到问题,并通过 charset.NewReaderLabel(encoding, body)
函数解决:
func decodeHTMLBody(body io.Reader, encoding string) (io.ReadCloser, error) {
body, err := charset.NewReaderLabel(encoding, body)
if err != nil {
log.Fatal(err)
}
return ioutil.NopCloser(body), nil
}
这里可以通过自动侦测检查网站的编码,也可以在配置清单中指定编码类型。为了准确性,我选择了在配置清单中配置的方法。此时 Site
结构体经过升级,增加 Charset
字段:
type Site struct {
Url string
UserAgent string
Charset string
CssSelector
}
在解析前执行此函数即可完成内容转码:
// convert none utf-8 web page to utf-8
if site.Charset != "" {
body, err = decodeHTMLBody(resp.Body, site.Charset)
if err != nil {
log.Fatal(err)
}
}
// load the HTML document
doc, err := goquery.NewDocumentFromReader(body)
完整修改细节可以参考这里的 commit 记录。之后在配置清单 Site
初始化时增加 Charset: "euc-jp"
配置即可。
Selector
选择器,增加预设值支持
8. 扩展 再次增加一个 Carib.go 的站点配置清单,由于部分属性在爬取前就已经明确,可以直接预置,需要 Selector -> Item
增加一个预置参数字段 preset
来储存,并在查找时直接返回这个值。
type Item struct {
selector string
attribute string
replacer *strings.Replacer
preset string
}
func Preset(preset string) Item {
return Item{selector: "", attribute: "", replacer: strings.NewReplacer("", ""), preset: preset}
}
func (selector Item) Value(doc *goquery.Document) string {
if len(selector.preset) > 0 {
return selector.preset
}
selection := doc.Find(selector.selector).First()
return selector.textOrAttr(selection)
}
可以参考这里的 commit 修改记录。Carib.go
站点配置清单完整内容如下:
func Carib(id string) Site {
return Site{
Url: fmt.Sprintf("https://www.caribbeancom.com/moviepages/%s/index.html", id),
UserAgent: MobileUserAgent,
Charset: "euc-jp",
CssSelector: CssSelector{
Title: Selector("h1[itemprop=name]"),
Actor: Selector("a[itemprop=actor]"),
Poster: Preset(fmt.Sprintf("https://www.caribbeancom.com/moviepages/%s/images/l_l.jpg", id)),
Producer: Preset("Caribbean"),
Sample: Preset(fmt.Sprintf("https://smovie.caribbeancom.com/sample/movies/%s/480p.mp4", id)),
Series: Selector("a[onclick^=gaDetailEvent\\(\\'Series\\ Name\\']"),
Release: Selector("span[itemprop=datePublished]"),
Duration: Selector("span[itemprop=duration]"),
Id: Preset(id),
Label: Selector("null"),
Genre: Selector("a[itemprop=genre]"),
Images: Selector("a[data-is_sample='1']").Attribute("href").Replace("/movie", "https://www.caribbeancom.com/movie"),
},
}
}
可以看到这里有四个 Preset
的属性:Poster
Producer
Id
Sample
,此时 Selector
的函数已经变为大写字母开头的公开函数,是因为之前对选择器提取到了 selector
包中。增加 Preset
字段是选择期 Selector
的第二次升级。
再增加一个 caribpr.go 站点配置清单,可以看到已直接支持。
9. 支持 Cookies 配置
在增加 mgs.go 站点时,爬虫需要预置 cookies
才能正确抓取到网页,所以类似于编码格式 encoding
,需要对 Site
结构体进行升级。
首先升级 Site.get()
函数,在请求时增加 cookies
:
type Site struct {
...
Cookies []http.Cookie
...
}
func (site Site) get() (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", site.Url, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("User-Agent", site.UserAgent)
for _, cookie := range site.Cookies {
log.Println(cookie)
req.AddCookie(&cookie)
}
return client.Do(req)
}
再次增加配置清单,即可获取到正确的站点信息:
func Mgs(id string) Site {
return Site{
Url: fmt.Sprintf("https://sp.mgstage.com/product/product_detail/SP-%s/", id),
UserAgent: MobileUserAgent,
Cookies: []http.Cookie{{Name: "adc", Value: "1"}},
CssSelector: CssSelector{
Title: Selector(".sample-image-wrap.h1 > img").Attribute("alt"),
...
Images: Selector("a[class^=\"sample-image-wrap sample\"]").Attribute("href"),
},
}
}
完整内容参考 commit 修改记录。
从零开始的 Go 爬虫框架编程实战 - 中篇