简单go并发爬虫demo

爬虫

爬虫简介:
是一个程序,用来获取指定网站数据信息。

  • 明确 url 。确定爬取对象
  • 发送请求。获取服务器响应数据。
  • 解析数据,提取有用数据内容。
  • 保存、分析数据结果。

今天我们用go并发来简单写一个小Demo来爬取一下豆瓣评分网站的数据

首先来思路分析一下该怎么做:

  1. 明确 url。找出url之间的一些小规律,比如豆瓣的url规律如下
https://movie.douban.com/top250?start=0&filter=         1

https://movie.douban.com/top250?start=25&filter=        2

https://movie.douban.com/top250?start=50&filter=        3

https://movie.douban.com/top250?start=75&filter=        4

之间的规律很好找

  1. 待提取字符特性:

  2. 提示用户指定爬取起始、终止页

  3. 封装 doWork 函数, 按起始、终止页面循环爬取网页数据

  4. 组织每个网页的 url。 下一页 = +25

  5. 封装函数 HttpGetDB(url)result,err { http.Get(url), resp.Body.Read(buf), n==0 break, result+= string(buf[:n}) }

    爬取网页的所有数据 通过result 返回给调用者。

  6. 解析、编译正则表达式 —— 提取 “电影名称”fileNames 传出的是[][]string , 下标为【1】是不带匹配参考项。

  7. 解析、编译正则表达式 —— 提取 “评分”传出的是[][]string , 下标为【1】是不带匹配参考项。

  8. 解析、编译正则表达式 —— 提取 “评价人数”传出的是[][]string , 下标为【1】是不带匹配参考项。

  9. 封装函数,将上述内容写入文件。save2File( [][]string)

  10. 创建并发go程 提取所有网页数据。

  11. 创建阻止主go程提取退出的 channel , SpiderPageDB() 末尾处,写channel

  12. doWork 中,添加新 for ,读channel

代码实现:

首先可以封装一个函数来爬取多少页到多少页,并且每一页构造一个go程

func doWork(start, end int)  {
    page := make(chan int)

    // 循环创建多个goroutine,提高爬取效率
    for i:=start; i<=end; i++ {
        go SpiderPageDB(i, page)
    }
    // 循环读取 channel, 协调主、子go程调用顺序
    for i:=start; i<=end; i++ {
        fmt.Printf("第%d页爬取完成\n", <-page)
    }
}

获取每一页的数据函数

func HttpGetDB(url string) (result string, err error) {
    resp, err1 := http.Get(url)
    if err1 != nil {
        err = err1
        return
    }
    defer resp.Body.Close()
    buf := make([]byte, 4096)
    for {
        n, err2 := resp.Body.Read(buf)
        if n == 0 {
            break
        }
        if err2 != nil && err2 != io.EOF {
            err = err2
            return
        }
        result += string(buf[:n])
    }
    return
}

得到每一个数据然后分析数据解析数据

func SpiderPageDB(i int, page chan<- int)  {
    url := "https://movie.douban.com/top250?start=" + strconv.Itoa((i-1)*25) + "&filter="
    result, err := HttpGetDB(url)
    if err != nil {
        fmt.Println("HttpGetDB err:", err)
        return
    }
    // 编译、解析正则表达式 —— 电影名
    ret1 := regexp.MustCompile(`<img width="100" alt="(?s:(.*?))" src="` )
    // 提取有效信息
    fileNames := ret1.FindAllStringSubmatch(result, -1)

    // 编译、解析正则表达式 —— 分数
    pattern := `<span class="rating_num" property="v:average">(.*?)</span>`
    ret2 := regexp.MustCompile(pattern )
    // 提取有效信息
    fileScore := ret2.FindAllStringSubmatch(result, -1)
/*  for _, one := range fileScore {
        fmt.Println("fileName:", one[1])
    }*/
    // 编译、解析正则表达式 —— 评分人数
    ret3 := regexp.MustCompile(`<span>(\d*?)人评价</span>`)
    // 提取有效信息
    peopleNum := ret3.FindAllStringSubmatch(result, -1)

    // 写入到一个文件中
    save2file(i, fileNames, fileScore, peopleNum)

    page <- i   // 写入channel ,协调主go程与子go程调用顺序。
}

最后保存数据到文件中

func save2file(idx int, fileNames, fileScore, peopleNum [][]string)  {
    // 组织保存文件路径及名程
    path := "D:/ecec/第" + strconv.Itoa(idx) + "页.txt"

    f, err := os.Create(path)
    if err != nil {
        fmt.Println("Create err:", err)
        return
    }
    defer f.Close()
    // 获取 一个网页中的条目数 —— 25
    n := len(fileNames)

    // 写一行标题
    f.WriteString("电影名称" + "\t" + "评分" + "\t" + "评价人数" + "\n")

    // 依次按序写入电影相关条目。
    for i:=0; i<n; i++ {
        f.WriteString(fileNames[i][1] + "\t" + fileScore[i][1] + "\t" + peopleNum[i][1] + "\n")
    }
}

最后直接在main里面就可以调用

完整代码如下

package main

import (
    "fmt"
    "strconv"
    "net/http"
    "io"
    "regexp"
    "os"
)

func HttpGetDB(url string) (result string, err error) {
    resp, err1 := http.Get(url)
    if err1 != nil {
        err = err1
        return
    }
    defer resp.Body.Close()
    buf := make([]byte, 4096)
    for {
        n, err2 := resp.Body.Read(buf)
        if n == 0 {
            break
        }
        if err2 != nil && err2 != io.EOF {
            err = err2
            return
        }
        result += string(buf[:n])
    }
    return
}

func SpiderPageDB(i int, page chan<- int)  {
    url := "https://movie.douban.com/top250?start=" + strconv.Itoa((i-1)*25) + "&filter="
    result, err := HttpGetDB(url)
    if err != nil {
        fmt.Println("HttpGetDB err:", err)
        return
    }
    // 编译、解析正则表达式 —— 电影名
    ret1 := regexp.MustCompile(`<img width="100" alt="(?s:(.*?))" src="` )
    // 提取有效信息
    fileNames := ret1.FindAllStringSubmatch(result, -1)

    // 编译、解析正则表达式 —— 分数
    pattern := `<span class="rating_num" property="v:average">(.*?)</span>`
    ret2 := regexp.MustCompile(pattern )
    // 提取有效信息
    fileScore := ret2.FindAllStringSubmatch(result, -1)
/*  for _, one := range fileScore {
        fmt.Println("fileName:", one[1])
    }*/
    // 编译、解析正则表达式 —— 评分人数
    ret3 := regexp.MustCompile(`<span>(\d*?)人评价</span>`)
    // 提取有效信息
    peopleNum := ret3.FindAllStringSubmatch(result, -1)

    // 写入到一个文件中
    save2file(i, fileNames, fileScore, peopleNum)

    page <- i   // 写入channel ,协调主go程与子go程调用顺序。
}

func save2file(idx int, fileNames, fileScore, peopleNum [][]string)  {
    // 组织保存文件路径及名程
    path := "C:/exec/第" + strconv.Itoa(idx) + "页.txt"

    f, err := os.Create(path)
    if err != nil {
        fmt.Println("Create err:", err)
        return
    }
    defer f.Close()
    // 获取 一个网页中的条目数 —— 25
    n := len(fileNames)

    // 写一行标题
    f.WriteString("电影名称" + "\t" + "评分" + "\t" + "评价人数" + "\n")

    // 依次按序写入电影相关条目。
    for i:=0; i<n; i++ {
        f.WriteString(fileNames[i][1] + "\t" + fileScore[i][1] + "\t" + peopleNum[i][1] + "\n")
    }
}

func doWork(start, end int)  {
    page := make(chan int)

    // 循环创建多个goroutine,提高爬取效率
    for i:=start; i<=end; i++ {
        go SpiderPageDB(i, page)
    }
    // 循环读取 channel, 协调主、子go程调用顺序
    for i:=start; i<=end; i++ {
        fmt.Printf("第%d页爬取完成\n", <-page)
    }
}

func main()  {
    var start, end int
    fmt.Print("请输入爬取起始页面(>=1):")
    fmt.Scan(&start)

    fmt.Print("请输入爬取终止页面(>=start):")
    fmt.Scan(&end)

    doWork(start, end)
}

猜你喜欢

转载自blog.csdn.net/sgsgy5/article/details/82429534