Abel'Blog

我干了什么?究竟拿了时间换了什么?

0%

go-爬虫

前言

colly

go colly 是一个开源的项目能用于做爬虫的。开一个文档来记录这个相关的知识。

chromedp

后续需要发现 colly 是无法支持爬取动态页面的,可能需要补充一下 chromedp 相关知识。

爬虫一般都做下面三个步骤的事情:

  1. 筛选网站;
  2. 抓取页面中的特定的内容,写入到pipeline中;
  3. pipeline提供各种实现,最终将数据写入到文件、数据库、redis之类;
  • PhantomJS 之前有人用这个虚拟的浏览器来跑爬虫;
  • selenium+chrome 这种方案是 java 来做的;

sdk文档

简介

安装

1
go get -u github.com/gocolly/colly/...

入门

先需要创建一个 [collector|chromedp] ,里面保存了连接信息,以及回调函数之类。

Collector-文档

通过回调函数来通知处理。

配置

可以创建的 collector 的时候制定,也能直接修改 struct 里面的成员变量。

也能通过修改环境变量来修改配置。环境变量能覆盖掉之前全部设置的配置,这样可以在没有编译的情况下,修改配置。

  • ALLOWED_DOMAINS (comma separated list of domains)
  • CACHE_DIR (string)
  • DETECT_CHARSET (y/n)
  • DISABLE_COOKIES (y/n)
  • DISALLOWED_DOMAINS (comma separated list of domains)
  • IGNORE_ROBOTSTXT (y/n)
  • MAX_BODY_SIZE (int)
  • MAX_DEPTH (int - 0 means infinite)
  • PARSE_HTTP_ERROR_RESPONSE (y/n)
  • USER_AGENT (string)

默认已经按照通用的golang-http服务器来实现的。也能通过下面这段代码修改成和 HTTP-Roundtripper

1
2
3
4
5
6
7
8
9
10
11
12
13
c := colly.NewCollector()
c.WithTransport(&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

最佳实践

调试

可以用这个来调试中间的问题。

1
c := colly.NewCollector(colly.Debugger(&debug.LogDebugger{}))

分布式抓取

分布式代理

可以通过设置多个代理地址,分布式抓取内容,默认是使用轮训机制来抓取,我们自己也能编写一个随机机制选择代理来抓取数据。这个主要是为了提高并发效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if p, err := proxy.RoundRobinProxySwitcher(
"socks5://127.0.0.1:1337",
"socks5://127.0.0.1:1338",
"http://127.0.0.1:8080",
); err == nil {
c.SetProxyFunc(p)
}
// 随机机制选择代理来抓取数据
var proxies []*url.URL = []*url.URL{
&url.URL{Host: "127.0.0.1:8080"},
&url.URL{Host: "127.0.0.1:8081"},
}

func randomProxySwitcher(_ *http.Request) (*url.URL, error) {
return proxies[random.Intn(len(proxies))], nil
}

// ...
c.SetProxyFunc(randomProxySwitcher)
分布式爬虫

要管理独立和分布式抓取工具,可以将抓取包装到一个服务里面。示例里面有个 scraper_server。可以直接阅读相关的资料。

分布式存储

实现了在内存中存储的数据。

存储后端

colly 有个内存后段来实现存储cookie和访问过的URL,它可以被任意实现这个接口的对象来覆盖,存储。

实现方式

默认为内存方式;

Redis 方式;

SQLite 方式;

MongoDB 方式;

使用选择器

如果任务足够复杂或具有不同类型的子任务,建议对一个抓取作业使用多个收集器。一个很好的例子是 coursera 课程抓取器,其中使用两个收集器 - 一个解析列表视图并处理分页,另一个收集课程详细信息。

这个相关的知识应该属于 css 相关。

基础知识

解析数据:使用正则表达式、XPath或CSS选择器等工具,解析网页中的数据。

基础知识

在解析 HTML 时,Go 语言提供了两个常用的库:htmlquery 和 goquery。虽然它们都可以用于解析 HTML,但它们之间有一些区别。

htmlquery:

  1. htmlquery 是一个基于 Go 语言的库,用于解析 HTML 并提取和操作数据。它使用 XPath 来选择和操作 HTML 中的元素。
  2. htmlquery 提供了方便的函数和方法,可以快速地选择和提取 HTML 中的元素。
  3. htmlquery 适用于简单的 HTML 解析任务,但可能无法处理一些复杂的 HTML 结构。

goquery:

  1. goquery 是另一个基于 Go 语言的库,用于解析和操作 HTML。它提供了一种类似于 jQuery 的语法来操作 HTML。
  2. goquery 使用 CSS 选择器来选择和操作 HTML 中的元素。它的语法与 jQuery 相似,因此对于熟悉 jQuery 的开发者来说更容易上手。
  3. goquery 具有更灵活的 HTML 解析能力,可以更好地处理复杂的 HTML 结构。

总结:

htmlquery 和 goquery 都是用于解析 HTML 的库,但它们使用不同的方法和语法。htmlquery 使用 XPath,而 goquery 使用 CSS 选择器。对于简单的 HTML 解析任务,htmlquery 可能是一个更方便的选择。而对于更复杂的 HTML 结构和操作需求,goquery 可能更适合。根据具体的需求和项目要求,可以选择合适的库进行 HTML 解析。

* goquery-css选择器

10种css-selector

飞雪无情

htmlquery-XPath

xpath-w3school

可以使用这个库来做一些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
"github.com/antchfx/htmlquery"

c.OnResponse(func(r *colly.Response) {
doc, err := htmlquery.Parse(strings.NewReader(string(r.Body)))
if err != nil {
log.Fatal(err)
}
nodes := htmlquery.Find(doc, `//*[@id="secondary"]/section[2]/ul//li`)
for _, node := range nodes {
a := htmlquery.FindOne(node, "./a[@href]")
fmt.Println(htmlquery.SelectAttr(a,"href"),htmlquery.InnerText(a))
}
})

使用 chromedp

安装

安装 Headless Chrome 的过程会因操作系统不同而有所不同。以下是在常见操作系统中安装 Headless Chrome 的基本步骤:

在 Ubuntu 或 Debian 上安装 Headless Chrome

添加 Chrome 的软件源:

1
2
3
4
5
apt-utils is not installed
gnupg, gnupg2 and gnupg1 do not seem to be installed

wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo apt update

更新软件源并安装 Chrome:

1
2
sudo apt update
sudo apt install google-chrome-stable

在 CentOS 或 RHEL 上安装 Headless Chrome

添加 Chrome 的 YUM 源:

1
2
3
4
5
6
7
8
sudo tee /etc/yum.repos.d/google-chrome.repo << RPMEOF
[google-chrome]
name=google-chrome
baseurl=http://dl.google.com/linux/chrome/rpm/stable/\$basearch
enabled=1
gpgcheck=1
gpgkey=https://dl.google.com/linux/linux_signing_key.pub
RPMEOF

安装 Chrome:

1
sudo yum install google-chrome-stable

在 macOS 上安装 Headless Chrome

下载并安装 Chrome:

1
brew install --cask google-chrome

在 Windows 上安装 Headless Chrome

下载 Chrome 安装程序并安装:

下载链接:https://www.google.com/chrome/

安装完毕后,可以通过命令行启动 Headless Chrome。在 Linux 或 macOS 上,可以使用 google-chrome —headless 命令;在 Windows 上,可以使用 chrome.exe —headless 命令。

要在 Golang 中使用 Headless Chrome,你需要安装 chromedp 库,然后按照之前的示例代码来使用。安装 chromedp 可以使用以下命令:

1
go get -u github.com/chromedp/chromedp

请注意,在一些情况下,可能需要根据特定操作系统的配置进行微调。如果出现任何问题,建议查阅相关文档或社区资源以获取更多帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()

var htmlContent string
for i := 0; i < 3; i++ { // 执行3次滑动操作
err := chromedp.Run(ctx,
chromedp.Navigate("https://www.example.com"), // 替换为目标网页的 URL
chromedp.Sleep(2*time.Second), // 等待页面加载完全
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight);`, &htmlContent),
)
if err != nil {
log.Fatal(err)
}

fmt.Println("Content after scroll:", htmlContent)

// 等待一段时间,以确保动态加载内容完全加载出来
time.Sleep(2 * time.Second)
}
}
在上述示例中,我们使用循环来执行滑动操作,每次滑动后等待一段时间以确保动态加载内容完全加载出来。你可以根据实际情况调整循环次数和等待时间,以获取所需的页面内容。

请注意,滑动操作可能会因页面结构和加载方式不同而有所不同。在编写代码时,你可能需要根据实际情况进行调整,以确保滑动操作能够正确地获取到页面的各个部分内容。

b站里面有个大佬写了一份代码,能模拟鼠标滑动的情况。

[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
package main

import (
"context"
"encoding/json"
"fmt"
"github.com/chromedp/cdproto/input"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"

"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)

var cnt = 0

func main() {
// 定义打开谷歌浏览器的一个临时数据文件夹
dir, err := ioutil.TempDir("", "chromedp-example")
if err != nil {
panic(err)
}
// defer函数代表当整个程序执行完之后会执行os.RemoveAll(dir),其实就是把这个临时数据文件夹删除
defer os.RemoveAll(dir)

// 配置一下等会程序运行打开的浏览器的一些参数
opts := append(chromedp.DefaultExecAllocatorOptions[:],
// 禁止GPU
chromedp.DisableGPU,
// 禁用默认的浏览器检查
chromedp.NoDefaultBrowserCheck,
// 一般我们调试的时候肯定是将这个值置为false,这样你就能看到程序在运行时打开浏览器,如果需要部署
// 到服务器,你希望无头打开浏览器,就得把这个值置为true
chromedp.Flag("headless", false),
// 忽略证书错误
chromedp.Flag("ignore-certificate-errors", true),
// 使用刚才创建的临时数据文件夹
chromedp.UserDataDir(dir),
)

allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
// 最后执行完之后肯定会关闭这个上下文
defer cancel()

// 使用log.Printf打印日志
taskCtx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
defer cancel()

// 检查浏览器进程是否启动
if err := chromedp.Run(taskCtx); err != nil {
panic(err)
}

// 监听网络事件
listenForNetworkEvent(taskCtx)
// actions就代表后面打开浏览器要执行的一系列操作
var actions []chromedp.Action

actions = append(actions,network.Enable())
// 指定要访问的地址
actions = append(actions,chromedp.Navigate(`https://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fm=result&fr=&sf=1&fmq=1631628760308_R&pv=&ic=&nc=1&z=&hd=&latest=&copyright=&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&sid=&word=%E7%BE%8E%E5%A5%B3%E5%A4%B4%E5%83%8F`))

// 模拟滚轮滚动50次,触发新的图片加载
for i:= 0; i < 20; i++{
actions = append(actions,chromedp.Sleep(1*time.Second))
actions = append(actions,chromedp.ActionFunc(func(ctx context.Context) error {
time.Sleep(1*time.Second)
// 在页面的(200,200)坐标的位置
p := input.DispatchMouseEvent(input.MouseWheel, 200, 200)
p = p.WithDeltaX(0)
// 滚轮向下滚动1000单位
p = p.WithDeltaY(float64(1000))
err = p.Do(ctx)
return err
}))
}

//执行这一列的操作
chromedp.Run(taskCtx,
actions...
)

}

//监听网络事件
func listenForNetworkEvent(ctx context.Context) {
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) {
// 是一个响应收到的事件
case *network.EventResponseReceived:
resp := ev.Response
if len(resp.Headers) != 0 {
//将这个resp转成json
response, _ := resp.MarshalJSON()
var res = &UrlResponse{}
json.Unmarshal(response, &res)
// 我们只关心是图片地址的url
if strings.Contains(res.Url,".jpg") || strings.Contains(res.Url, "f=JPEG"){
cnt++
// 去对每个图片地址下载图片
downloadImage(res.Url,"美女头像",cnt)
}
}
}
})
}

type UrlResponse struct {
Url string `json:"url"`
}
/**
根据图片的地址下载图片
*/
func downloadImage(imgUrl ,dir string, cnt int){
defer func() {
if r := recover(); r != nil {
fmt.Printf("发生异常,地址忽略: %s", imgUrl)
}
}()
//生成文件名
fileName := path.Base(strconv.Itoa(cnt)+".jpg")

// 设置请求地址和请求头参数
imgReq, err := http.NewRequest("GET", imgUrl, nil)
imgReq.Header.Add("Referer", "https://image.baidu.com/")
imgReq.Header.Add("Accept-Encoding", "gzip,deflate,br")
imgReq.Header.Add("Host", "image.baidu.com")
imgReq.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36")

client := &http.Client{}
// 执行请求
imgRes, err := client.Do(imgReq)

if err != nil {
log.Println("Get image error :", err)
return
}
defer imgRes.Body.Close()
if imgRes.ContentLength == 0{
return
}
// 这种时候dir文件夹必须存在,不然会报错
f, err := os.Create(dir + "/" + fileName )
if err != nil {
log.Println("Create image error:", err)
return
}
// 拷贝二进制流数据,保存成本地图片
io.Copy(f, imgRes.Body)
}

作者:菜牛冲鸭 出处:bilibili

selenium-python版本爬虫

ChromeDriver-dowload

dockerfile-for-go-and-chromedp

开启无头模式

1
2
3
4
5
6
7
8
chromedp.Flag("headless", true),
chromedp.DisableGPU,
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.Flag("no-default-browser-check", true),
chromedp.Flag("ignore-certificate-errors", true),
chromedp.WindowSize(1921, 1024),
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36"), //设置UserAgent

部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chrome failed to start:
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted
[1113/090000.130975:FATAL:zygote_host_impl_linux.cc(201)] Check failed: . : Operation not permitted (1)
[1113/090000.131458:ERROR:scoped_ptrace_attach.cc(27)] ptrace: Operation not permitted (1)
https://blog.csdn.net/chengly0129/article/details/72178806

docker run 时加上参数: --privileged

# 镜像打包
docker save -o <tarball name> <image name>
# 镜像加载
docker load -i <tarball name>
# 镜像删除
docker rmi <image name>
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
# 使用一个基础镜像
FROM ubuntu:latest

# 设置工作目录
WORKDIR /app
RUN mkdir -p /app/logs/

# 增加普通用户,google-chrome 需要用普通用户执行
RUN useradd -ms /bin/bash newuser

# 安装必要的软件,并且安装 google-chrome
RUN apt-get update;apt-get install wget -y;apt-get install -y apt-utils ; \
apt-get install -y curl; apt-get install -y gnupg2; \
apt-get install -y tzdata; \
sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -; \
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list';\
apt-get update;apt-get install google-chrome-stable vim -y

# 设置时区
# golang-时间函数会出现一些问题
ENV TZ=Asia/Shanghai

# 拷贝程序文件到容器中
COPY ./bin/config-release.json /app/config.json
COPY ./bin/spider /app/spider

# 授权
RUN chown -R newuser:newuser /app

# 使用普通用户执行
USER newuser

# 启动服务器
CMD ["/app/spider","-d"]

golang 由于时区问题造成崩溃

1
2
3
4
5
6
7
8
9
panic: time: missing Location in call to Date

goroutine 33 [running]:
time.Date(0xc000046f10?, 0xc000046f10?, 0xf09a46?, 0xc000046f0c?, 0x0?, 0x0?, 0xdca980?, 0x0?)
/usr/local/go/src/time/time.go:1469 +0x4c5
time.parse({0xf09a39, 0x12}, {0xc000046f00, 0x14}, 0xc000046f00?, 0x0)
/usr/local/go/src/time/format.go:1398 +0x2111
time.ParseInLocation({0xf09a39, 0x12}, {0xc000046f00, 0x14}, 0x0?)
/usr/local/go/src/time/format.go:1029 +0xe6

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12

# 设置时区
# https://serverfault.com/questions/949991/how-to-install-tzdata-on-a-ubuntu-docker-image
RUN DEBIAN_FRONTEND=noninteractive TZ=Asia/Shanghai apt-get -y install tzdata

# 设置时区
ENV TZ=Asia/Shanghai

RUN echo "${TZ}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
&& date

全量的配置

docker-compose

1
2
3
4
5
6
7
8
9
10
11
12
version: '3'

services:
spider:
build:
context: .
dockerfile: Dockerfile
hostname: spider
restart: always
volumes:
- /home/ubuntu/spider/logs:/data/logs

dockerfile

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
# 备注
FROM ubuntu:latest

RUN useradd -ms /bin/bash newuser

VOLUME [ "/data/logs" ]

# 设置工作目录
WORKDIR /data

# 安装必要的程序
RUN apt-get update;apt-get install wget -y;apt-get install -y apt-utils ; \
apt-get install -y curl; apt-get install -y gnupg2; \
apt install curl software-properties-common apt-transport-https ca-certificates -y ;\
curl -fSsL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor | tee /usr/share/keyrings/google-chrome.gpg > /dev/null ;\
echo deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main | tee /etc/apt/sources.list.d/google-chrome.list ;\
apt update ;\
apt install google-chrome-stable -y

# 设置时区
# https://serverfault.com/questions/949991/how-to-install-tzdata-on-a-ubuntu-docker-image
RUN DEBIAN_FRONTEND=noninteractive TZ=Asia/Shanghai apt-get -y install tzdata

# 设置时区
ENV TZ=Asia/Shanghai

RUN echo "${TZ}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
&& date

# 拷贝程序文件到容器中
COPY ./bin/config-release.json /data/config.json
COPY ./bin/spider /data/spider

RUN mkdir -p /data/logs

RUN chmod u+x /data/spider

# 授权用户
RUN chown -R newuser:newuser /data

# 使用用户来执行程序
USER newuser

# 启动服务器
CMD ["/data/spider","-d"]

编译dockerfile

docker-compose build

将docker部署出去;

docker-compose up -d

golang如何解析下面这样的json串:

  1. 有个json如下
    {
    “code”: 1,
    “data”: {
     "chainID": "111"
    
    }
    }
  2. 我在golang里面定义了一个struct先解析出来
    type RspCommand struct {
    Code int json:"code"
    Data interface{} json:"data"
    }
  3. 当我判断了 Code = 1 的时候,我需要将 data 解析成下面这个定义
    type RspRealData struct {
    ChainID string json:"chainID"
    }
    golang如何动态解析某个json字符串;
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
package main

import (
"encoding/json"
"fmt"
)

type RspCommand struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}

type RspRealData struct {
ChainID string `json:"chainID"`
}

func main() {
// 示例JSON字符串
jsonString := `{"code": 1, "data": {"chainID": "111"}}`

// 解析RspCommand
var rsp RspCommand
err := json.Unmarshal([]byte(jsonString), &rsp)
if err != nil {
fmt.Println("JSON解析错误:", err)
return
}

// 根据Code的值判断解析Data的结构体类型
switch rsp.Code {
case 1:
// 尝试断言Data字段为RspRealData类型
realData, ok := rsp.Data.(map[string]interface{})
if !ok {
fmt.Println("Data字段类型不匹配")
return
}

// 将map转为RspRealData
var realDataStruct RspRealData
err := mapToStruct(realData, &realDataStruct)
if err != nil {
fmt.Println("转换错误:", err)
return
}

fmt.Println("解析结果:", realDataStruct)
default:
fmt.Println("未知Code值")
}
}

// mapToStruct 将map转为指定结构体
func mapToStruct(m map[string]interface{}, result interface{}) error {
jstr, err := json.Marshal(m)
if err != nil {
return err
}
return json.Unmarshal(jstr, result)
}

引用

colly-github

colly-official-website

10-css-selector

xpath-w3school

github-chromedp