Go 语言 爬虫实现 v0.1

学了一些 Go 的基本语法之后,深觉进一步的深入还是该靠实际项目来锻炼。小目标是逐步写完一个爬虫,以此来学习 Go 中的相关标准库以及 goroutine、channel 的使用。


1. http.Get()

Go 标准库 net/http 下的 http.Get() 方法定义如下

func Get(url string) (resp *Response, err error) {
    return DefaultClient.Get(url)
}

输入参数为 url 的字符串,返回一个 Response 结构体的实例的指针与错误信息,我们仔细研究一下 Response 结构体

1.1 Response 结构体

源码下的 Response 结构体定义

type Response struct {
    Status     string // e.g. "200 OK"
    StatusCode int    // e.g. 200
    Proto      string // e.g. "HTTP/1.0"
    ProtoMajor int    // e.g. 1
    ProtoMinor int    // e.g. 0
    Header Header
    Body io.ReadCloser
    ContentLength int64
    TransferEncoding []string
    Close bool
    Uncompressed bool
    Trailer Header
    Request *Request
    TLS *tls.ConnectionState
}

在我们所获得的返回值中,我们主要关心 Body 字段。Body 字段是一个 io.ReadCloser 接口,该接口定义如下:

type ReadCloser interface {
    Reader //也是一个接口,包含一个 Read 方法
    Closer  //也是一个接口,包含一个 Close 方法
}

看到这里有点疑惑,怎么才能获得 Body 包含的信息呢?通过官方文档看到,想要读取 Body 字段的内容,需要使用如下方法:

res, err := http.Get("http://www.google.com/robots.txt")
if err != nil {
    log.Fatal(err)
}
robots, err := ioutil.ReadAll(res.Body)

ioutil.ReadAll 方法返回 []byte,如果要可读还需转换为 string 类型。以下面的代码为例:

package main

import(
    "fmt"
    "net/http"
    "io/ioutil"
)

var netaddr string

func main(){
    netaddr = "https://www.jd.com/robots.txt"
    res, err := http.Get(netaddr)
    if err != nil{
        fmt.Println("Some Error")
        return
    }
    res_byte, err := ioutil.ReadAll(res.Body)
    defer res.Body.Close()
    fmt.Println("[]byte info:")
    fmt.Println(res_byte)
    fmt.Println("=============")
    fmt.Println("string info:")
    fmt.Println(string(res_byte))

}

以下是终端打印的信息

[]byte info:
[85 115 101 114 45 97 103 101 110 116 58 32 42 32 10 68 105 115 97 108 108 111 119 58 32 47 63 42 32 10 68 105 115 97 108 108 111 119 58 32 47 112 111 112 47 42 46 104 116 109 108 32 10 68 105 115 97 108 108 111 119 58 32 47 112 105 110 112 97 105 47 42 46 104 116 109 108 63 42 32 10 85 115 101 114 45 97 103 101 110 116 58 32 69 116 97 111 83 112 105 100 101 114 32 10 68 105 115 97 108 108 111 119 58 32 47 32 10 85 115 101 114 45 97 103 101 110 116 58 32 72 117 105 104 117 105 83 112 105 100 101 114 32 10 68 105 115 97 108 108 111 119 58 32 47 32 10 85 115 101 114 45 97 103 101 110 116 58 32 71 119 100 97 110 103 83 112 105 100 101 114 32 10 68 105 115 97 108 108 111 119 58 32 47 32 10 85 115 101 114 45 97 103 101 110 116 58 32 87 111 99 104 97 99 104 97 83 112 105 100 101 114 32 10 68 105 115 97 108 108 111 119 58 32 47 10]
=============
string info:
User-agent: * 
Disallow: /?* 
Disallow: /pop/*.html 
Disallow: /pinpai/*.html?* 
User-agent: EtaoSpider 
Disallow: / 
User-agent: HuihuiSpider 
Disallow: / 
User-agent: GwdangSpider 
Disallow: / 
User-agent: WochachaSpider 
Disallow: /

2. 使用 regexp 包实现正则匹配

我们通过 http.Get() 方法获取网页的相关内容后,需要使用正则表达式匹配出我们所感兴趣的信息。 可以使用 Go std 中的 regexp 包完成相关工作。下面通过 gobyexample 中给定示例说明 regexp 包的一些常用使用。

package main

import "bytes"
import "fmt"
import "regexp"

func main() {

    // MatchString 检查是否存在匹配的字符串,返回 bool 值与错误信息
    match, _ := regexp.MatchString("p([a-z]+)ch", "peach")
    fmt.Println(match)

    // 在上面我们直接使用了字符串进行匹配,但是
    // 在一些任务下,你可以使用 `Compile` 来
    // 优化 `Regexp` 结构体
    r, _ := regexp.Compile("p([a-z]+)ch")

    // 在 `Regexp` 结构体中有多重匹配的方法
    // 下面的例子和第一个一样,也是检查是否
    // 存在匹配的字符串
    fmt.Println(r.MatchString("peach"))

    // 找到 regexp 匹配的第一个字符串
    fmt.Println(r.FindString("peach punch"))

    // 该方法也是寻找符合匹配的第一个字符串,
    // 但是其返回的是所匹配的字符串在原字符串
    // 中的索引(返回类型为切片)
    fmt.Println(r.FindStringIndex("peach punch"))

    //  Submatch 不仅返回整个正则模式下匹配
    // 的字符串,也返回在子正则下所匹配到的
    // 字符串(返回类型为切片)
    fmt.Println(r.FindStringSubmatch("peach punch"))
    
    // 类似地,该方法会返回正则与子正则
    // 所匹配字符串的索引(返回类型为切片)
    fmt.Println(r.FindStringSubmatchIndex("peach punch"))

    // 返回所有匹配的字符串(返回类型为切片)
    fmt.Println(r.FindAllString("peach punch pinch", -1))

    // 返回所有匹配字符串的索引(返回类型为二维切片)
    fmt.Println(r.FindAllStringSubmatchIndex(
        "peach punch pinch", -1))

    // 若提供一个非负整数作为第二个参数,
    // 则该参数先限定了最大的匹配个数
    // (返回类型为切片)
    fmt.Println(r.FindAllString("peach punch pinch", 2))


    // 将上面所提到的方法的方法名中的 String 去掉
    // 便可以用来做 []byte 的匹配
    fmt.Println(r.Match([]byte("peach")))

    // MustCompile 强制将正则表达式转换为 regexp 类型
    // 与 Compile 的区别在于, MustCompile 的返回值
    // 只有一个(Compile 还有一个 err 返回值),若
    // 不能转换则会引发 panic。其实 MustCompile 也是
    // 调用了 Compile 来实现的
    r = regexp.MustCompile("p([a-z]+)ch")
    fmt.Println(r)

    // 若能匹配到,则使用第二个参数来替代匹配到
    // 的部分
    fmt.Println(r.ReplaceAllString("a peach", "<fruit>"))
    
    // Func 变量允许你将匹配到的部分
    // 利用所给的函数(第二个参数)来进行转换
    // (返回类型为 []byte)
    in := []byte("a peach")
    out := r.ReplaceAllFunc(in, bytes.ToUpper)
    fmt.Println(string(out))
}

以下是上述代码的终端输出:

true
true
peach
[0 5]
[peach ea]
[0 5 1 3]
[peach punch pinch]
[[0 5 1 3] [6 11 7 9] [12 17 13 15]]
[peach punch]
true
p([a-z]+)ch
a <fruit>
a PEACH

3. 文件的读写

获取网页与获取网页中的图片基本一致,都是通过 http.Get() 方法获取数据。我们先来粗略看看 Go 语言中的几种文件读取方式。

以关键字 “Go 语言 文件读写” 为关键字搜索相关是操作,会发现 Go 提供了好几个包来实现文件的读写,我们先来了解与区分一下各个包的使用。

3.1 文件读取

首先我们先在当前文件夹下新建一个 test_dat.txt 文件,其中的内容如下:

hello
go

同样,我们也是使用 gobyexample下的例子来大概说明一下。

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "os"
)

// Reading files requires checking most calls for errors.
// This helper will streamline our error checks below.
func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {

    // 将文件的所有内容读入内存,这是最常见的方式
    dat, err := ioutil.ReadFile("./test_dat.txt")
    check(err)
    fmt.Print(string(dat),"\n")

    //  也许你想控制怎样以及将文件哪一部分读取进来
    // 使用 os.Open 得到一个 os.File 类型的值,
    // 这相当于一个文件句柄
    f, err := os.Open("./test_dat.txt")
    check(err)

    // 读取文件开始的一些字节
    // 最多允许读入 5 个字节
    // 同时也要注意我们真正读了多少
    b1 := make([]byte, 5)
    n1, err := f.Read(b1) // os.File.Read 返回了读取的字节数与 err
    check(err)
    fmt.Printf("%d bytes: %s\n", n1, string(b1))

    // 可以使用 Seek 方法从文件
    // 获取一个新的 offset
    // 第二个参数分别可取 0, 1, 2 三个值
    // 0 :相对于文件起始
    // 1 :相对于当前 offset
    // 2 :相对于文件末尾
    o2, err := f.Seek(6, 0)
    check(err)
    b2 := make([]byte, 2)
    n2, err := f.Read(b2)
    check(err)
    fmt.Printf("%d bytes @ %d: %s\n", n2, o2, string(b2))

    // io 包下的 ReadAtLeast 方法,相比上面的
    // os.File.Read 方法更具鲁棒性
    // 具体见源码中对 err 返回值的解释
    o3, err := f.Seek(6, 0)
    check(err)
    b3 := make([]byte, 2)
    n3, err := io.ReadAtLeast(f, b3, 2)
    check(err)
    fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))

    // 在 Go 中没有内建的倒带(rewind)方法
    // 不过可以使用 Seek(0, 0)实现
    _, err = f.Seek(0, 0)
    check(err)

    // bufio 包实现了一个带缓存的 reader
    r4 := bufio.NewReader(f)
    b4, err := r4.Peek(5)
    check(err)
    fmt.Printf("5 bytes: %s\n", string(b4))


    // 关闭文件,一般使用 defer 关键字
    f.Close()

}

Go 下的 os、io/ioutil 等库下还有很多对文件读取的操作方法,具体可见官方文档。

注意,因为 Windows 下的回车换行符为 \r\n(如下图) :

Windows 下的回车换行符

所以如果直接在 Windows 下运行代码的话会得到如下输出:

hello
go
5 bytes: hello
2 bytes @ 6: 
g
2 bytes @ 6: 
g
5 bytes: hello

如果在 Windows 下使用该代码,则先用 Notepad++ 把文档处理成 UNIX 的格式(如下图):

UNIX 下回车换行符

得到输出如下:

hello
go
5 bytes: hello
2 bytes @ 6: go
2 bytes @ 6: go
5 bytes: hello

3.2 文件写入

同样使用 gobyexample 的例子来看看

// Writing files in Go follows similar patterns to the
// ones we saw earlier for reading.

package main

import (
    "bufio"
    "fmt"
    "io/ioutil"
    "os"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {

    // 直接写文件,不存在该文件则创建一个
    d1 := []byte("hello\ngo\n")
    err := ioutil.WriteFile("./tmp_dat1", d1, 0644)
    check(err)

    // 使用 os 包需要先创建文件
    f, err := os.Create("./tmp_dat2")
    check(err)

    // 记得关闭文件
    defer f.Close()

    // 写入 byte 切片
    d2 := []byte{115, 111, 109, 101, 10}
    n2, err := f.Write(d2)
    check(err)
    fmt.Printf("wrote %d bytes\n", n2)

    // 也可以用 WriteString 来写入字符串
    // 其实看了源代码会发现 WriteString
    // 也是调用了 Write 的
    n3, err := f.WriteString("writes\n")
    fmt.Printf("wrote %d bytes\n", n3)

    // Issue a `Sync` to flush writes to stable storage.
    f.Sync()

    // `bufio` provides buffered writers in addition
    // to the buffered readers we saw earlier.
    w := bufio.NewWriter(f)
    n4, err := w.WriteString("buffered\n")
    fmt.Printf("wrote %d bytes\n", n4)

    // Use `Flush` to ensure all buffered operations have
    // been applied to the underlying writer.
    w.Flush()

}

代码输出:

wrote 5 bytes
wrote 7 bytes
wrote 9 bytes

生成的两个文件如下:

tmp_dat1

tmp_dat2

最后推荐一篇文章(我都写了这么多才看到有这篇文章),详细的介绍了 Go 中文件读写的各种包:https://gocn.io/article/40


4. 实现第一个爬虫

很简单的实现一个爬去网站图片的爬虫,因为是 1.0 版本,所以十分简陋,之后再慢慢实现诸如自动登录,自动翻页,识别验证码,多线程运行等功能(如果我会的话)。

爬取的是2017年10月7日 www.zju.edu.cn 首页的图片,代码如下:

// version 1.0
// 1. 搞清楚 http 的用法,返回数据的格式
// 2. 搞清楚正则表达式匹配的实现
// 3. 搞清楚文件读写的方法

package main

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

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func main() {
    netaddr := "http://www.zju.edu.cn"
    res, err := http.Get(netaddr)
    check(err)
    res_byte, err := ioutil.ReadAll(res.Body)
    defer res.Body.Close()
    res_str := string(res_byte)
    // fmt.Println(res_str)
    match := regexp.MustCompile(`/_upload/article/images/(.+).jpg`)
    fmt.Println(match.MatchString(res_str))
    matched_str := match.FindAllString(res_str, -1)
    for i:= 0; i < len(matched_str); i++{
        image, err := http.Get("http://www.zju.edu.cn" + matched_str[i])
        check(err)
        image_byte, err := ioutil.ReadAll(image.Body)
        defer image.Body.Close()
        err = ioutil.WriteFile("./" + strconv.Itoa(i) + ".jpg", image_byte, 0644)
        check(err)
        fmt.Println("http://www.zju.edu.cn" + matched_str[i])
    }

}

最后可在当前文件夹下看到爬下来的图片,如下图:

爬取的图片

至此我们就完成了第一个爬虫啦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容