都说python是爬虫的利器,有各种各样的第三方库。今天尝试了下golang,一字个爽 ~~
爬取豆瓣Top250书籍的整体思路是:书籍列表页(一个列表页一个goroutine处理)–> 书籍详情页(每本书一个goroutine处理) –> 获取信息 –> 入库
环境
- go version go1.10 linux/amd64
本文不会进行环境部署以及第三方库安装、数据库准备等讲解。
用到的golang知识
- golang对数据库操作(orm)
- goroutine
- 资源竞争问题
代码实现
// file douban.go
// Crawling 豆瓣Top250书籍信息
package main
import (
"fmt"
"log"
// "strconv"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
_ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
)
var wg sync.WaitGroup // 等待所有goroutine完成
var mutx sync.Mutex // 防止资源竞争,引入互斥锁
var count int = 0 // 计数器,统计爬取的书籍数
// 定义表字段
type Books struct {
Id int64 `xorm:"autoincr pk"`
Title string // 书名
Url string // url
PicUrl string // pic url
Auth string // 作者
// Fraction float64 // 豆瓣评分
Fraction string // 豆瓣评分
Press string // 出版社
OriginalTitle string // 原作名
Translators string // 译者
PublicationYear string // 出版年
// Page int64 // 页数
Page string // 页数
// Price float64 // 定价
Price string // 定价
Framed string // 装帧
Collection string // 丛书
ISBN string // ISBN
AuthSummary string `xorm:"text"` // 作者简介
ContentSummary string `xorm:"text"` // 内容简介
Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"`
}
// 获取Top250书籍详情页url
func Page(engine *xorm.Engine, url string) {
doc, err := goquery.NewDocument(url)
if err != nil {
log.Fatal(err)
}
doc.Find("div.indent").Find("div.pl2").Each(func(i int, s *goquery.Selection) {
href, _ := s.Find("a").Attr("href")
// title, _ := s.Find("a").Attr("title")
// span := s.Find("a").Find("span").Text()
// content := fmt.Sprintf("%s%s --> %s", title, span, href)
// fmt.Println(content)
go Book(engine, href) // 一个goroutine处理一本书信息抓取
time.Sleep(1 * time.Second)
})
}
// 获取书籍详情页具体内容
func Book(engine *xorm.Engine, url string) {
// 在函数退出时调用Done来通知main函数已经完成
defer wg.Done()
// 对已爬取的书籍数量计数器加锁
mutx.Lock()
{
count++
}
mutx.Unlock()
doc, err := goquery.NewDocument(url)
if err != nil {
log.Fatal(err)
}
data := &Books{}
// book name
title := doc.Find("h1 span").Text()
fmt.Printf("title: %s\n", title)
data.Title = title
// 书籍详情页url
bookurl := url
fmt.Printf("bookurl: %s\n", bookurl)
data.Url = url
// 书籍封面照片url
picUrl, _ := doc.Find("div#mainpic").Find("a").Attr("href")
fmt.Printf("picUrl: %s\n", picUrl)
data.PicUrl = picUrl
// 豆瓣评分
fraction := doc.Find("strong.rating_num").Text()
fmt.Printf("豆瓣评分: %s\n", fraction)
data.Fraction = fraction
// 书籍基本信息(一整块的信息)
info := doc.Find("div#info")
// 把全角/半角冒号统一替换为半角冒号, 因为发现有些书籍信息会用全角冒号,例如--> 译者:xxx
// 影响到下面一些字符的判断
lines := strings.Split(strings.Replace(info.Text(), ":|:", ":", -1), "\n")
for i, valA := range lines {
valA = strings.TrimSpace(valA)
if valA != "" && strings.ContainsAny(valA, ":") {
valA = fmt.Sprintf("%s ", valA)
for _, valB := range lines[i+1:] {
valB = strings.TrimSpace(valB)
if valB == "" {
continue
}
if strings.ContainsAny(valB, ":") {
break
} else {
valA = fmt.Sprintf("%s%s", valA, valB)
}
}
fmt.Println(valA)
val := strings.SplitN(valA, ":", 2)[1]
switch strings.SplitN(valA, ":", 2)[0] {
case "作者":
data.Auth = val
case "出版社":
data.Press = val
case "原作名":
data.OriginalTitle = val
case "译者":
data.Translators = val
case "出版年":
data.PublicationYear = val
case "页数":
data.Page = val
case "定价":
data.Price = val
case "装帧":
data.Framed = val
case "丛书":
data.Collection = val
case "ISBN":
data.ISBN = val
}
}
}
relatedInfo := doc.Find("div.related_info div.indent")
// 内容简介
contenSummary := ""
var content *goquery.Selection
if relatedInfo.Eq(0).Find("span").Nodes == nil {
content = relatedInfo.Eq(0).Find("div.intro")
} else {
// 有些简介需点击展开全部查看完整内容
content = relatedInfo.Eq(1).Find("div.intro").Eq(1)
}
content.Each(func(i int, s *goquery.Selection) {
text := strings.TrimSpace(s.Find("p").Text())
contenSummary = fmt.Sprintf("%s%s\n", contenSummary, text)
})
fmt.Printf("contentSummary: %s", contenSummary)
data.ContentSummary = contenSummary
// 作者简介
authSummary := ""
var auth *goquery.Selection
if relatedInfo.Eq(1).Find("span").Nodes == nil {
auth = relatedInfo.Eq(1).Find("div.intro")
} else {
auth = relatedInfo.Eq(1).Find("div.intro").Eq(1)
}
auth.Each(func(i int, s *goquery.Selection) {
text := strings.TrimSpace(s.Find("p").Text())
authSummary = fmt.Sprintf("%s%s\n", authSummary, text)
})
fmt.Printf("authSummary: %s\n", authSummary)
data.AuthSummary = authSummary
fmt.Printf("------------------------------\n")
// insert data
row, err := engine.Insert(data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("instert data in %d\n", row)
}
func main() {
starttime := time.Now()
// 计数加250,表示要等待250个goroutine(每本书我用一个goroutine处理,Top250 250本数)
wg.Add(250)
// connection mysql
// 需把数据库名称、用户、密码换成你自己的
engine, err := xorm.NewEngine("mysql", "root:123456@/test?charset=utf8")
if err != nil {
log.Fatal(err)
}
err = engine.Sync2(new(Books)) // 同步表结构
if err != nil {
log.Fatal(err)
}
baseurl := "https://book.douban.com/top250?"
// 豆瓣中一个列表页是25本书,一共10页,刚好250本书
// 至于列表页翻页的url变化规则,不通网站不一样,需要自己点击翻页,进行对比,发现其中的规则
for i := 0; i <= 225; i += 25 {
url := fmt.Sprintf("%sstart=%d", baseurl, i)
go Page(engine, url) // 一个列表页一个goroutine处理
time.Sleep(5 * time.Second) // 休眠5秒,防止单位时间内请求太频繁,被豆瓣禁止访问
}
wg.Wait() // 等待250个goroutine完成
elapsed := time.Since(starttime)
fmt.Printf("books num: %d\n", count) // 输出爬取的书本数量
fmt.Println("App elapsed: ", elapsed) // 输出程序运行时间
}
编译运行程序
$ go build -race douban.go // -race 可以检查程序是否存在资源竞争
$ ./douban
程序运行后在你指定连接的数据库中会生成一张名为books
的表,表结构跟我们定义Books
结构体一致
mysql> show create table books;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| books | CREATE TABLE `books` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
`pic_url` varchar(255) DEFAULT NULL,
`auth` varchar(255) DEFAULT NULL,
`fraction` varchar(255) DEFAULT NULL,
`press` varchar(255) DEFAULT NULL,
`original_title` varchar(255) DEFAULT NULL,
`translators` varchar(255) DEFAULT NULL,
`publication_year` varchar(255) DEFAULT NULL,
`page` varchar(255) DEFAULT NULL,
`price` varchar(255) DEFAULT NULL,
`framed` varchar(255) DEFAULT NULL,
`collection` varchar(255) DEFAULT NULL,
`i_s_b_n` varchar(255) DEFAULT NULL,
`auth_summary` text,
`content_summary` text,
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=251 DEFAULT CHARSET=utf8 |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
待程序跑完(我的机子用了1分钟多一点),就可以看到表中已经有250条记录了
mysql> select count(title) from books;
+--------------+
| count(title) |
+--------------+
| 250 |
+--------------+
1 row in set (0.00 sec)
这个爬虫有待优化点
- 应该加入代理,不然很容易豆瓣识别到是爬虫,会被封,访问不了豆瓣Top250书籍
- 现在程序是每获取一本书的信息就入库,250本书,对表进行了250次的操作(想想,如果数据量很大,多可怕的IO),应该是把250本书信息都获取了,再批量入库,仅需一次对表操作,减少IO