YMLiang

之前学习了一段时间go之后想写一点小的项目,上网查询了一些资料,做了一些小的demo,今天将我最近写的小项目分享给大家:利用go实现单任务版爬虫和并发爬虫,因为go对并发支持的很好,所以我们只需要提前将goroutine和channl学好,就很好理解了

1
2
3
4
5
项目结构:
1.带有简单的日志收集,打印的日志写到/logs目录下,可配置写到文件中还是输出到控制台,或者都输出
2.简单的正则匹配,上下文控制超时,chan控制并发数量,goroutine并发下载等知识点
3.简单的乱码处理,简单的随机数处理,图片下载,图片名称替换等
项目难度 ❤
  • regexp api:

    re := regexp.MustCompile(reStr) : reStr代表正则表达式 ,这个方法表示按照这个正则去匹配
    re.FindAllStringSubmatch(srcStr,-1)  : -1表示所有全查到    (1或2或3表示取1个2个3个)
    
  • 一些常用的正则符号

    () : 标识分组
    \w : 字母数字下划线
    \d : 数字
    \D :字母
    \s\S : 任意字符
    \s : 空白字符(小写s)
    \S : 非空白字符(大写S)
    re1 | re2 : re1 或 re2 所表示的片段
    regexp*+?  | regexp+? 分别表示1到多次 | 或0到多次   ?表示非贪婪,也就是匹配到?后面的东西为止
    {n,m}  |  {n,}  表示 n个到m个 |  或至少是n个
    [\w\.] 表示出现任意字母数字下划线或者 . 都是ok的
    
  • 举例:

    [\s\S]+?href   这段正则中: 
    [\s\S] 表示任意字符
    + 表示 0个或1个
    ?表示不会隔过 href
    
  • 正则案例

    这里以电话号为案例
    rePhone = `(1[3456789]\d)(\d.[a-z]{2,3})(\d{4})`
    匹配出是二维数组形式分四组,整体为第一组,有一个括号加1组
    
  • 快捷键

    Alt + Shift + M 变为函数形式
    Ctrl + Alt + L 格式化代码
    Ctrl + Alt + V 变量名补全

  • 示例代码:爬虫获取html界面内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func GetHtml(url string) string {
//利用context来控制超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
defer cancel()

var html string
//建立一个channel
done := make(chan int, 1)
go func() {
resp, err := http.Get(url)
if err != nil {
logs.Info("http get failed, err = ", err)
}
defer resp.Body.Close()

bytes, _ := ioutil.ReadAll(resp.Body)
html = string(bytes)
time.Sleep(1 * time.Second)
//向channel里面放数字(相当于一个占位符,没什么具体含义,代表这个channel里面有东西)
done <- 1
}()

select {
//如果done 这个 channel里面有元素,则代表爬取页面成功
case <-done:
fmt.Println("work done on time")
return html
//如果 context : ctx 超时,则爬取失败 ctx.Done()代表超时
case <-ctx.Done():
// timeout
fmt.Println("爬取超时:err= ", ctx.Err())
}

return html
}
  • 项目结构

项目结构

  • 源代码

完整代码可以fork 我的github :

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package main

import (
"context"
"crawler/config"
"crawler/log"
"crawler/utils"
"fmt"
"github.com/astaxie/beego/logs"
"io/ioutil"
"net/http"
"regexp"
"strings"
"sync"
"time"
)

//爬取的一些正则
var (
//分四组,整体为第一组,有一个括号加1组
rePhone = `(1[3456789]\d)(\d.[a-z]{2,3})(\d{4})`
//reEmail = `[1-9]\d{4,}@qq.com`
// \w : 字母数字下划线
reEmail = `\w+@\w+\.[a-z]{2,3}`
/**爬取超链接*/
reLink = `<a[\s\S]+?href="(http[\s\S]+?)"`
/**爬取网站 图片*/
rePic = `<img[\s\S]+?src="(http[\s\S]+?)"`
/**爬取标题*/
reTitle = `<a[\s\S]+?title="([\s\S]+?)"`
/*爬取图片名称*/
reName = `<img[\s\S]+?alt="([\s\S]+?)"`
/**爬取图片url和图片alt名称*/
rePicAlt = `<img.+?src="(http.+?)".+?alt="(.+?)".*?>`
/**Alt正则*/
reAlt = `alt="(.+?)"`
)

/**
定义一些全局变量
*/
var (
pushChan sync.WaitGroup
wg sync.WaitGroup
)


func main() {

//加载日志配置
fileName := "./conf/config.conf"
err := config.LoadConf("ini", fileName)
if err != nil {
logs.Error("LoadConf filed,err = ", err)
return
}

err = log.InitLogger(config.Conf.LogPath, config.Conf.LogLevel)
if err != nil {
fmt.Println("load conf error")
panic("load conf failed")
return
}

url := "https://www.163.com/"
//url := GetHtml("https://tieba.baidu.com/p/6118291659?pid=126751186930&cid=0&red_tag=1970653863#126751186930")
//url := GetHtml("https://www.766ju.com/")
//url := "https://www.766ju.com/vod/html2/20124.html"

fmt.Println(time.Now(),":图片爬取开始!")

/**----------------------下载图片-------------------*/
//elems := GetUrls(url,reName)
//elems := GetImgInfos(url, rePicAlt)
//for _, str := range elems {
// fmt.Printf("Name = %s \n, url= %s \n", str["url"], str["fileName"])
// //异步下载图片
//
//}
//wg.Wait()

pushChan.Add(1)
go func(url, rePicAlt string) {
elem2Chan(url, rePicAlt)
pushChan.Done()
}(url, rePicAlt)
pushChan.Wait()

close(midChan)

fmt.Println(time.Now(),"图片爬取完毕!")
/**----------------------下载图片-------------------*/

/**----------------------从管道中读并下载-------------------*/
fmt.Println(time.Now(),"图片开始下载!")
for elem := range midChan {
wg.Add(1)
go func(elem map[string]string) {
DownLoadPicAsync(elem["url"], elem["fileName"])
wg.Done()
}(elem)
}
wg.Wait()
fmt.Println(time.Now(),"图片下载完成!")
/**----------------------从管道中读并下载-------------------*/

/**----------------------打印信息-------------------*/
//PrintInfo(url,reName)
}

/**
建立中间管道,爬到的图片扔进管道中
*/
var (
midChan = make(chan map[string]string, 80)
picChan = make(chan int, 10) //信号量最多为10
)
//每个图片实例放入chan : midChan中
func elem2Chan(url, reg string) {
elem := GetImgInfos(url, reg)
for _, str := range elem {
midChan <- str
}
}

/**
同步下载图片
*/
func DownLoadPic(url, subFileName string) {

resp, _ := http.Get(url)
defer resp.Body.Close()
imageBytes, _ := ioutil.ReadAll(resp.Body)
fileName := ""
if strings.Contains(url, "gif") {
//fileName := `D:\goImg\imgs\` + strconv.Itoa(int(time.Now().UnixNano())) + ".jpg"
//fileName := `D:\goImg\imgs\` + GetRandomName() + ".jpg"
fileName = `D:\goImg\imgs\` + utils.ReplaceName(subFileName) + ".jpg"
err := ioutil.WriteFile(fileName, imageBytes, 0644)
if err != nil {
fmt.Println("下载失败,err = ", err)
}
} else {
//fileName := `D:\goImg\imgs\` + GetRandomName() + ".jpg"
fileName = `D:\goImg\imgs\` + utils.ReplaceName(subFileName) + ".jpg"
err := ioutil.WriteFile(fileName, imageBytes, 0644)
if err != nil {
fmt.Println("下载失败,err = ", err)
}
}
fmt.Printf("下载成功,fileName = {%s}\n", fileName)
}

/**
异步下载图片
*/

func DownLoadPicAsync(url, fileName string) {
picChan <- 123
DownLoadPic(url, fileName)
<-picChan
}

/**
获取爬取到的每张图片的url
*/
func GetUrls(url, reg string) []string {
html := GetHtml(url)
//爬取逻辑
re := regexp.MustCompile(reg)
// -1 代表匹配全部 ,写数字机几就取几个
allString := re.FindAllStringSubmatch(html, -1)
fmt.Println("捕获图片张数:", len(allString))
imgUrls := make([]string, 0)
for _, str := range allString {
imgUrl := str[1]
imgUrls = append(imgUrls, imgUrl)
}
return imgUrls
}

/*
获取img+alt信息 返回map 数组
map{key1:url value1:string ; key2:fileName value2:string}
*/
func GetImgInfos(url, reg string) []map[string]string {
//爬取html网页
html := GetHtml(url)
//爬取正则
re := regexp.MustCompile(reg)
// -1 代表匹配全部 ,写数字机几就取几个
allString := re.FindAllStringSubmatch(html, -1)
fmt.Println("捕获图片张数:", len(allString))
//新建一个map切片
imgInfos := make([]map[string]string, 0)
for _, str := range allString {
imgInfo := make(map[string]string, 0)
imgUrl := str[1]
imgInfo["url"] = imgUrl
imgInfo["fileName"] = GetImgNameFromTags(str[0])
imgInfos = append(imgInfos, imgInfo)
}
return imgInfos
}

/**
判断fileName是用alt命名还是随机数命名
*/
func GetImgNameFromTags(imgUrl string) string {
re := regexp.MustCompile(reAlt)
res := re.FindAllStringSubmatch(imgUrl, -1)
if len(res) > 0 {
//res[0][1]取的是第 1行 第二列的元素 即alt标签中的中文命名
return utils.GBK2UTF8(res[0][1])
} else {
return utils.GetRandomName()
}
}

//Alt + Shift + M 变为函数形式
/**
爬取html网页
*/
func GetHtml(url string) string {
//控制超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
defer cancel()
var html string
done := make(chan int, 1)
go func() {
resp, err := http.Get(url)
if err != nil {
logs.Info("http get failed, err = ", err)
}
defer resp.Body.Close()

bytes, _ := ioutil.ReadAll(resp.Body)
html = string(bytes)
time.Sleep(1 * time.Second)
done <- 1
}()
select {
case <-done:
fmt.Println("work done on time")
return html
case <-ctx.Done():
// timeout
fmt.Println("爬取超时:err= ", ctx.Err())
}
return html
}

 评论


博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

本站使用 Material X 作为主题 , 总访问量为 次 。
Copyright 2018-2019 YMLiang'BLOG   |   京ICP备 - 19039949  |  载入天数...载入时分秒...