从零开始的 Go 爬虫框架编程实战 - 中篇

发布于:

编程

目录

这是一篇讲解如何编写 Go 爬虫框架的编程实战系列长文,是一个爬虫框架程序从无到有的成长进化史。

6. 增加通用获取属性的封装

在上一节的实战后,tidy 已经具备了基本爬虫框架,增加站点配置就可以支持新的站点爬虫。但是只支持文本查询是不够的,例如我们需要获取 <a href="value"><img src="value"> 中的 hrefsrc 属性。我们在之前已经 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 的站点配置清单,可以看到爬虫代码无需做任何修改已经支持了匹配查询。

7. 增加 gb2312 euc-jp 等编码支持

当前爬虫爬取到的网站内容都是使用 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" 配置即可。

8. 扩展 Selector 选择器,增加预设值支持

再次增加一个 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 修改记录。