學(xué)了一些 Go 的基本語法之后,深覺進(jìn)一步的深入還是該靠實(shí)際項(xiàng)目來鍛煉。小目標(biāo)是逐步寫完一個(gè)爬蟲,以此來學(xué)習(xí) Go 中的相關(guān)標(biāo)準(zhǔn)庫以及 goroutine、channel 的使用。
1. http.Get()
Go 標(biāo)準(zhǔn)庫 net/http 下的 http.Get() 方法定義如下
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
輸入?yún)?shù)為 url 的字符串,返回一個(gè) Response 結(jié)構(gòu)體的實(shí)例的指針與錯(cuò)誤信息,我們仔細(xì)研究一下 Response 結(jié)構(gòu)體
1.1 Response 結(jié)構(gòu)體
源碼下的 Response 結(jié)構(gòu)體定義
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
}
在我們所獲得的返回值中,我們主要關(guān)心 Body 字段。Body 字段是一個(gè) io.ReadCloser 接口,該接口定義如下:
type ReadCloser interface {
Reader //也是一個(gè)接口,包含一個(gè) Read 方法
Closer //也是一個(gè)接口,包含一個(gè) Close 方法
}
看到這里有點(diǎn)疑惑,怎么才能獲得 Body 包含的信息呢?通過官方文檔看到,想要讀取 Body 字段的內(nèi)容,需要使用如下方法:
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,如果要可讀還需轉(zhuǎn)換為 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 包實(shí)現(xiàn)正則匹配
我們通過 http.Get() 方法獲取網(wǎng)頁的相關(guān)內(nèi)容后,需要使用正則表達(dá)式匹配出我們所感興趣的信息。 可以使用 Go std 中的 regexp 包完成相關(guān)工作。下面通過 gobyexample 中給定示例說明 regexp 包的一些常用使用。
package main
import "bytes"
import "fmt"
import "regexp"
func main() {
// MatchString 檢查是否存在匹配的字符串,返回 bool 值與錯(cuò)誤信息
match, _ := regexp.MatchString("p([a-z]+)ch", "peach")
fmt.Println(match)
// 在上面我們直接使用了字符串進(jìn)行匹配,但是
// 在一些任務(wù)下,你可以使用 `Compile` 來
// 優(yōu)化 `Regexp` 結(jié)構(gòu)體
r, _ := regexp.Compile("p([a-z]+)ch")
// 在 `Regexp` 結(jié)構(gòu)體中有多重匹配的方法
// 下面的例子和第一個(gè)一樣,也是檢查是否
// 存在匹配的字符串
fmt.Println(r.MatchString("peach"))
// 找到 regexp 匹配的第一個(gè)字符串
fmt.Println(r.FindString("peach punch"))
// 該方法也是尋找符合匹配的第一個(gè)字符串,
// 但是其返回的是所匹配的字符串在原字符串
// 中的索引(返回類型為切片)
fmt.Println(r.FindStringIndex("peach punch"))
// Submatch 不僅返回整個(gè)正則模式下匹配
// 的字符串,也返回在子正則下所匹配到的
// 字符串(返回類型為切片)
fmt.Println(r.FindStringSubmatch("peach punch"))
// 類似地,該方法會(huì)返回正則與子正則
// 所匹配字符串的索引(返回類型為切片)
fmt.Println(r.FindStringSubmatchIndex("peach punch"))
// 返回所有匹配的字符串(返回類型為切片)
fmt.Println(r.FindAllString("peach punch pinch", -1))
// 返回所有匹配字符串的索引(返回類型為二維切片)
fmt.Println(r.FindAllStringSubmatchIndex(
"peach punch pinch", -1))
// 若提供一個(gè)非負(fù)整數(shù)作為第二個(gè)參數(shù),
// 則該參數(shù)先限定了最大的匹配個(gè)數(shù)
// (返回類型為切片)
fmt.Println(r.FindAllString("peach punch pinch", 2))
// 將上面所提到的方法的方法名中的 String 去掉
// 便可以用來做 []byte 的匹配
fmt.Println(r.Match([]byte("peach")))
// MustCompile 強(qiáng)制將正則表達(dá)式轉(zhuǎn)換為 regexp 類型
// 與 Compile 的區(qū)別在于, MustCompile 的返回值
// 只有一個(gè)(Compile 還有一個(gè) err 返回值),若
// 不能轉(zhuǎn)換則會(huì)引發(fā) panic。其實(shí) MustCompile 也是
// 調(diào)用了 Compile 來實(shí)現(xiàn)的
r = regexp.MustCompile("p([a-z]+)ch")
fmt.Println(r)
// 若能匹配到,則使用第二個(gè)參數(shù)來替代匹配到
// 的部分
fmt.Println(r.ReplaceAllString("a peach", "<fruit>"))
// Func 變量允許你將匹配到的部分
// 利用所給的函數(shù)(第二個(gè)參數(shù))來進(jìn)行轉(zhuǎn)換
// (返回類型為 []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. 文件的讀寫
獲取網(wǎng)頁與獲取網(wǎng)頁中的圖片基本一致,都是通過 http.Get() 方法獲取數(shù)據(jù)。我們先來粗略看看 Go 語言中的幾種文件讀取方式。
以關(guān)鍵字 “Go 語言 文件讀寫” 為關(guān)鍵字搜索相關(guān)是操作,會(huì)發(fā)現(xiàn) Go 提供了好幾個(gè)包來實(shí)現(xiàn)文件的讀寫,我們先來了解與區(qū)分一下各個(gè)包的使用。
3.1 文件讀取
首先我們先在當(dāng)前文件夾下新建一個(gè) test_dat.txt 文件,其中的內(nèi)容如下:
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() {
// 將文件的所有內(nèi)容讀入內(nèi)存,這是最常見的方式
dat, err := ioutil.ReadFile("./test_dat.txt")
check(err)
fmt.Print(string(dat),"\n")
// 也許你想控制怎樣以及將文件哪一部分讀取進(jìn)來
// 使用 os.Open 得到一個(gè) os.File 類型的值,
// 這相當(dāng)于一個(gè)文件句柄
f, err := os.Open("./test_dat.txt")
check(err)
// 讀取文件開始的一些字節(jié)
// 最多允許讀入 5 個(gè)字節(jié)
// 同時(shí)也要注意我們真正讀了多少
b1 := make([]byte, 5)
n1, err := f.Read(b1) // os.File.Read 返回了讀取的字節(jié)數(shù)與 err
check(err)
fmt.Printf("%d bytes: %s\n", n1, string(b1))
// 可以使用 Seek 方法從文件
// 獲取一個(gè)新的 offset
// 第二個(gè)參數(shù)分別可取 0, 1, 2 三個(gè)值
// 0 :相對于文件起始
// 1 :相對于當(dāng)前 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 中沒有內(nèi)建的倒帶(rewind)方法
// 不過可以使用 Seek(0, 0)實(shí)現(xiàn)
_, err = f.Seek(0, 0)
check(err)
// bufio 包實(shí)現(xiàn)了一個(gè)帶緩存的 reader
r4 := bufio.NewReader(f)
b4, err := r4.Peek(5)
check(err)
fmt.Printf("5 bytes: %s\n", string(b4))
// 關(guān)閉文件,一般使用 defer 關(guān)鍵字
f.Close()
}
Go 下的 os、io/ioutil 等庫下還有很多對文件讀取的操作方法,具體可見官方文檔。
注意,因?yàn)?Windows 下的回車換行符為 \r\n(如下圖) :

所以如果直接在 Windows 下運(yùn)行代碼的話會(huì)得到如下輸出:
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() {
// 直接寫文件,不存在該文件則創(chuàng)建一個(gè)
d1 := []byte("hello\ngo\n")
err := ioutil.WriteFile("./tmp_dat1", d1, 0644)
check(err)
// 使用 os 包需要先創(chuàng)建文件
f, err := os.Create("./tmp_dat2")
check(err)
// 記得關(guān)閉文件
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 來寫入字符串
// 其實(shí)看了源代碼會(huì)發(fā)現(xiàn) WriteString
// 也是調(diào)用了 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
生成的兩個(gè)文件如下:

tmp_dat2

最后推薦一篇文章(我都寫了這么多才看到有這篇文章),詳細(xì)的介紹了 Go 中文件讀寫的各種包:https://gocn.io/article/40
4. 實(shí)現(xiàn)第一個(gè)爬蟲
很簡單的實(shí)現(xiàn)一個(gè)爬去網(wǎng)站圖片的爬蟲,因?yàn)槭?1.0 版本,所以十分簡陋,之后再慢慢實(shí)現(xiàn)諸如自動(dòng)登錄,自動(dòng)翻頁,識別驗(yàn)證碼,多線程運(yùn)行等功能(如果我會(huì)的話)。
爬取的是2017年10月7日 www.zju.edu.cn 首頁的圖片,代碼如下:
// version 1.0
// 1. 搞清楚 http 的用法,返回?cái)?shù)據(jù)的格式
// 2. 搞清楚正則表達(dá)式匹配的實(shí)現(xiàn)
// 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])
}
}
最后可在當(dāng)前文件夾下看到爬下來的圖片,如下圖:
