使用 golang 来创建一个爬虫获取 <reddit.com> 图片。

比如 r/wallpaper,通过解析官方API http://www.reddit.com/r/wallpaper.json?limit=22&after=xxxxx 返回的 JSON 数据来分析和下载文件。

主要使用库

  • github.com/urfave/cli/v2 用来创建命令行。
  • github.com/gocolly/colly 是golang爬虫框架, 用来获取数据。
  • github.com/buger/jsonparser 来解析 reddit 的json数据。

下面以一步步创建爬虫, 创建命令,数据检索,并发处理下载。

graph TD
    A[命令行入口] --参数--> B(获取数据) 
    B --> B1(数据处理) 
    B1 --> C{limit}
    C --计数器+1--> B 
    C --等待下载完成-->  E(结束)
    B1 --计数器+1--> D(下载)

命令创建

创建命令 reddit-get 参数定义

  • -c --chanel 频道名称,默认 wallpaper
  • -d --dir 输出目录
  • -l --limit 处理数量限制

引入库 urfave/cli

import (
    "github.com/urfave/cli/v2"
)

创建命令代码

var nowPage int64   // 当前页
var limitPage int64 // 限制数量
var startPage int64 // 限制数量

func main() {
    app := &cli.App{
        Name:  "reddit-get",
        Usage: "download image!",
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name:    "chanel",
                Aliases: []string{"c"},
                Value:   "wallpaper",
                Usage:   "频道名称",
            },
            &cli.StringFlag{
                Name:    "dir",
                Aliases: []string{"d"},
                Value:   "output",
                Usage:   "输出目录",
            },
               &cli.Int64Flag{
                Name:        "start",
                Aliases:     []string{"s"},
                Value:       1,
                Usage:       "start page",
                Destination: &startPage,
            },
            &cli.Int64Flag{
                Name:        "limit",
                Aliases:     []string{"l"},
                Value:       10,
                Usage:       "limit page",
                Destination: &limitPage,
            },
        },
        Action: func(c *cli.Context) error {
            // 执行
            run(c.String("chanel"), c.String("dir"))
            return nil
        },
    }

    err := app.Run(os.Args)
    if err != nil {
        log.Fatal(err)
    }
}

数据检索执行

追加库

import(
    "path/filepath"
    "sync"
    "io"
    "log"
    "net/http"
    "os"
    "path"

    "github.com/buger/jsonparser"
    "github.com/gocolly/colly"
    "github.com/gocolly/colly/proxy"
)

代码


var wg sync.WaitGroup // 计数
var limit int64 = 24  // reddit 每页数量

func run(chanel, dir string) {
    log.Printf("--> %s/%s\n", dir, chanel)
    endPage := startPage + limitPage

    // 加载代理
    rp, err := proxy.RoundRobinProxySwitcher("socks5://127.0.0.1:8080")
    if err != nil {
        log.Fatal(err)
    }

    // 下载器
    downloadClient := http.DefaultClient
    downloadClient.Transport = &http.Transport{
        Proxy: rp,
    }

    // 创建默认
    c := colly.NewCollector(colly.AllowURLRevisit())
    c.SetProxyFunc(rp)

    // 记录
    c.OnRequest(func(r *colly.Request) {
        fmt.Println(nowPage, " --> 访问:", r.URL.String())
    })

    // 相应处理
    c.OnResponse(func(r *colly.Response) {
        // 后缀
        fileExtToDownload := map[string]bool{".jpg": true, ".png": true, ".gif": true}
        
        
        if nowPage >= startPage {
            // 解析
            jsonparser.ArrayEach(r.Body, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
                url, _, _, _ := jsonparser.Get(value, "data", "url")
                formatedURL := html.UnescapeString(string(url))
                _, filename := filepath.Split(formatedURL)
                if fileExtToDownload[filepath.Ext(filename)] {
                    f := DownloadFile{
                        Filename: filename,
                        Folder:   chanel,
                        URL:      formatedURL,
                        client:   downloadClient,
                    }
                    go f.Down(dir)
                }
            }, "data", "children")

            // 处理数量
            limit, err = jsonparser.GetInt(r.Body, "data", "dist")
            if err != nil {
                limit = 24
            }
        }

        // 处理数量
        limit, err := jsonparser.GetInt(r.Body, "data", "dist")
        if err != nil {
            limit = 24
        }
        limitCount += limit

        // 继续处理
        before, err := jsonparser.GetString(r.Body, "data", "after")
        if err == nil && nowPage < endPage {
            u := fmt.Sprintf("http://www.reddit.com/r/%s.json?limit=%d&after=%s", chanel, limit, before)
            wg.Add(1)
            go c.Visit(u)
        }

        // 结束 visit
        wg.Done()
    })

    u := fmt.Sprintf("http://www.reddit.com/r/%s.json", chanel)
    wg.Add(1) // +1
    go c.Visit(u)

    wg.Wait() // 等待清零
}


// DownloadFile 需要下载的文件
type DownloadFile struct {
    Filename string
    Folder   string
    URL      string
    client   *http.Client
}

// Down 开始下载
func (f *DownloadFile) Down(directory string) {
    wg.Add(1)
    defer wg.Done()

    // 实际路径
    p := path.Join(directory, f.Folder, f.Filename)

    if _, err := os.Stat(p); err == nil {
        log.Println("文件已存在:", f.URL)
        return
    }
    os.Mkdir(path.Join(directory, f.Folder), 0777)

    output, err := os.Create(p)
    defer output.Close()
    if err != nil {
        log.Println("创建失败: ", err)
    }

    response, err := f.client.Get(f.URL)
    if err != nil {
        log.Println("下载失败: ", err)
    }
    defer response.Body.Close()

    _, err = io.Copy(output, response.Body)
    if err != nil {
        log.Println("写入失败 ", err)
    }
    log.Printf("下载文件 %s/%s", f.Folder, f.Filename)

}

编译

go build

> ./reddit-get -h
NAME:
   reddit-get - download image!

USAGE:
   reddit-get [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --chanel value, -c value  频道名称 (default: "wallpaper")
   --dir value, -d value     输出目录 (default: "output")
   --start value, -s value   start page (default: 1)
   --limit value, -l value   limit page (default: 10)
   --help, -h                show help (default: false)

> ./reddit-get 
2020/05/09 16:59:23 --> output/wallpaper
访问: http://www.reddit.com/r/wallpaper.json
2020/05/09 16:59:28 下载文件 wallpaper/SI0qi3X.jpg
...

源码

项目位置: https://github.com/erasin/reddit-get