学了一些 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 下运行代码的话会得到如下输出:
hello
go
5 bytes: hello
2 bytes @ 6:
g
2 bytes @ 6:
g
5 bytes: hello
如果在 Windows 下使用该代码,则先用 Notepad++ 把文档处理成 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
生成的两个文件如下:
最后推荐一篇文章(我都写了这么多才看到有这篇文章),详细的介绍了 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])
}
}
最后可在当前文件夹下看到爬下来的图片,如下图: