使用 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
...