golang爬取豆瓣Top250书籍信息

都说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

猜你喜欢

转载自blog.csdn.net/luckytanggu/article/details/79684470