都说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)
这个爬虫有待优化点