關於golang專案之爬蟲 單機版
爬蟲專案的應用範圍很廣泛
最近總結了一下爬蟲的專案 並記錄下來
爬蟲的最終版為 分散式併發處理爬蟲 但是我們分為三部分記錄 首先是單任務版的爬蟲記錄
此次我們爬取的是珍愛網的公開內容 由於其他網址有可能涉及私密資訊 所以選擇相親網站
宣告 本人爬取的內容只供自己練習爬蟲使用 不會以此牟利
首先看一下我們要爬取的網頁介面
我們先從最終版本捋順出單任務版本爬蟲需要實現的功能
.獲取並列印所有城市第一頁使用者的詳細資訊
這是我們需要或許的內容之一 各個城市的名稱
先打印出網頁的所有原始碼
func main(){ //試探網頁能否正常開啟 若能則將網頁內容以結構體指標方式返回 resp , err :=http.Get("http://www.zhenai.com/zhenghun") if err != nil{ panic(err) } //程式結束時實現結構體指標關閉 defer resp.Body.Close() //判斷頭部內容是否正確 if resp.StatusCode != http.StatusOK{ fmt.Println("Error StatusCode:", resp.StatusCode) return } //獲取字串 s, err := ioutil.ReadAll(resp.Body) if err != nil{ panic(err) } fmt.Printf("%s", s) }
列印內容後我們的結果上面中文都是亂碼 我們解決一下
func main(){ //試探網頁能否正常開啟 若能則將網頁內容以結構體指標方式返回 resp , err :=http.Get("http://www.zhenai.com/zhenghun") if err != nil{ panic(err) } //程式結束時實現結構體指標關閉 defer resp.Body.Close() //判斷頭部內容是否正確 if resp.StatusCode != http.StatusOK{ fmt.Println("Error StatusCode:", resp.StatusCode) return } //自動判斷字元格式函式 詳細講解在函式體內 e := DetermineEncoding(resp.Body) //將從網頁中獲取的結構體放入函式並告訴函式結構體內的字元格式 返回utf8格式 utf8Reader := transform.NewReader(resp.Body,e.NewDecoder()) //將結構體內容返回為byte s, err := ioutil.ReadAll(utf8Reader) if err != nil{ panic(err) } //列印utf8格式字元 fmt.Printf("%s", s) } //判斷字元格式並返回 func DetermineEncoding(r io.Reader) encoding.Encoding{ //提取結構體內的前1024個字元 byte , err := bufio.NewReader(r).Peek(1024) if err != nil{ panic(err) } //比較提取出來的字元進行判斷 e, _, _:= charset.DetermineEncoding(byte,"") //返回盤短值 return e }
需要注意的是如果沒有安裝指定庫 上面程式碼可能有的無法實現 我們安裝庫
一共是安裝兩個庫
gopm get -g -v golang.org/x/text 是進行字元格式轉換
gopm get -g -v golang.org/x/net/html 是字元型別自動判斷
安裝完成後指定路徑會出現庫檔案gbk.go
現在我們來列印結果
現在我們正確的打印出網頁原始碼了
有了原始碼之後 我們要篩選出對自己有用的資訊
獲取城市名稱和連線的方法有
.使用css選擇器
.使用xpath (與css類似)
.使用正則表示式
這裡我用到的是正則表示式 在正式進行專案之前 我們回顧一下正則表示式
const text = `
My email is [email protected]
email1 is [email protected]
email2 is [email protected]
email3 is [email protected]
`
func main() {
//確定要尋找的目標及返回需要的字元段
re := regexp.MustCompile(`([a-zA-Z0-9]+)@([a-zA-Z0-9]+)(\.[a-zA-Z0-9.]+)`)
//返回二維陣列 函式的作用是得到字元段並按要求返回需要的單個字串
match := re.FindAllStringSubmatch(text,-1)
//迴圈列印每一段字元
for _, m := range match{
fmt.Println(m)
}
}
列印結果如下
regexp.MustCompile函式內的引數含義為 要用什麼條件進行查詢
[a-zA-Z0-9]括號內代表要查詢的內容 大小寫字母及數字 都是我們要查詢的內容
([a-zA-Z0-9]) 中括號外面一層小括號代表 被小括號包括的所有內容都重新分配一段內容作為返回值
.*為查詢全部內容包括空字元
.+為查詢全部內容不包括空字元 字元為空則不列印
\.為轉義字元 需要注意
回顧結束 下面我們在專案中使用
func main(){
//試探網頁能否正常開啟 若能則將網頁內容以結構體指標方式返回
resp , err :=http.Get("http://www.zhenai.com/zhenghun")
if err != nil{
panic(err)
}
//程式結束時實現結構體指標關閉
defer resp.Body.Close()
//判斷頭部內容是否正確
if resp.StatusCode != http.StatusOK{
fmt.Println("Error StatusCode:", resp.StatusCode)
return
}
//自動判斷字元格式函式 詳細講解在函式體內
e := DetermineEncoding(resp.Body)
//將從網頁中獲取的結構體放入函式並告訴函式結構體內的字元格式 返回utf8格式
utf8Reader := transform.NewReader(resp.Body,e.NewDecoder())
//將結構體內容返回為byte
s, err := ioutil.ReadAll(utf8Reader)
if err != nil{
panic(err)
}
//----------------------------------------------------------------------------------
//獲取所需內容函式 |
printCityList(s) |
//----------------------------------------------------------------------------------
}
//判斷字元格式並返回
func DetermineEncoding(r io.Reader) encoding.Encoding{
//提取結構體內的前1024個字元
byte , err := bufio.NewReader(r).Peek(1024)
if err != nil{
panic(err)
}
//比較提取出來的字元進行判斷
e, _, _:= charset.DetermineEncoding(byte,"")
//返回盤短值
return e
}
//----------------------------------------------------------------------------------
//提取所需內容 如網頁地址 城市名稱 |
func printCityList(contents []byte){ |
//設定被提取者所需要的條件 |
re :=regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`) |
//從網頁檔案中提取所需檔案 |
matches := re.FindAllSubmatch(contents, -1) |
//列印所需內容 |
for _, m := range matches{ |
fmt.Printf("City: %s URL: %s \n", m[2], m[1]) |
}
//列印總城市數量 |
fmt.Printf("Matches found: %d\n", len(matches))
} |
//----------------------------------------------------------------------------------
增加及改變的位置被我標記出來了
增加一個小技巧知識點 [^>] 在不知道具體內容是字母陣列還是符號是 我們可以用 ^加上停止點 其中>為停止點是第一次遇到>
由於我們需要找的是帶有地址及城市名稱的欄位
所以我們的格式是
<a href="http://www.zhenai.com/zhenghun/anqing"
class="">安慶</a>
列印結果如下
這裡我們需要注意的是
re.FindAllSubmatch(contents, -1) 返回值是 [][][]tybe
這裡 我們可以把[]tybe當做是string
剩下的可以理解為[][]string 這樣二維陣列好理解多了吧
然後我們用range把二維陣列分割成一維陣列
每個一維陣列是有三個內容塊 類似於 var a []int = {1,2,3}
然後我們列印真正需要的內容 “"a[2] a[1]"” a[0]為不需要值
城市和網址篩選成功後我們就可以從每一個城市中爬取第一頁的資料了
下面我們來看單任務版爬蟲的架構
既然架構確定了 我們按照架構圖把程式碼敲出來 註釋都在程式碼的每一行
先看我們的引擎部分
package engine
import (
"awesomeProject1/crawler/fetcher"
"log"
"fmt"
)
//引擎 控制整個程式的流程
func Run(seeds ...Request){
var requests []Request
//接收main函式傳過來的值
for _, r := range seeds{
requests = append(requests,r)
}
//利用傳過來的值進行 解析 及 提取
for len(requests) > 0 {
//獲取第一個值
r := requests[0]
//進行切片 把已經提取的內容篩選出去
requests = requests[1:]
//第一次列印為main函式傳入地址 然後每次列印是從r.ParserFunc函式中提取出的城市地址
log.Printf("Fetching %s\n", r.Url)
//將不同URL傳輸進去 返回不同的頁面原始碼
body, err := fetcher.Fetch(r.Url)
//判斷URL是否正確 如果不正確 跳過此次迴圈
if err != nil{
log.Printf("Fetcher: error fetching url %s:%v",r.Url,err)
continue
}
//注意關鍵的地方 r為Request結構體變數 在main函式中 我們設定Request結構體變數中的ParserFunc值為parser.PrintCityList
//所以當第一次迴圈時r.ParserFunc(body)相當於parser.PrintCityList(body) 再一次體會到go語言的函數語言程式設計魅力
//r.ParserFunc(body)得到的結構體組分為城市名稱及要執行的函式 仔細揣摩結構體Request的ParserFunc值
//第一次迴圈成功後 parser.PrintCityList(body)被我們的engine.Nilparser代替 當然現在engine.Nilparser為空沒有任何返回值
ParseResult := r.ParserFunc(body)
//requests被填滿 requests又得到新的URL和運算函式 被抓取資訊只要足夠就可以一直執行下去
requests = append(requests,ParseResult.Requests...)
//列印所有在PrintCityList函式返回的Item值 Item值是任何型別可以使城市名也可以是使用者資訊
for _, item := range ParseResult.Items{
fmt.Printf("Got item %v\n", item)
}
}
}
引擎部分不容易理解的地方在於r.ParserFunc(body) 我已經著重註釋出來了
這段程式碼最重要的是函數語言程式設計基本功紮實
下面我們看提取
package fetcher
import (
"net/http"
"fmt"
"golang.org/x/text/transform"
"io/ioutil"
"io"
"golang.org/x/text/encoding"
"bufio"
"golang.org/x/net/html/charset"
)
func Fetch(url string)([]byte, error){
//試探網頁能否正常開啟 若能則將網頁內容以結構體指標方式返回
resp , err :=http.Get(url)
if err != nil{
return nil, err
}
//程式結束時實現結構體指標關閉
defer resp.Body.Close()
//判斷頭部內容是否正確
if resp.StatusCode != http.StatusOK{
return nil, fmt.Errorf("Error Statuscode: %d", resp.StatusCode)
}
//自動判斷字元格式函式 詳細講解在函式體內
e := DetermineEncoding(resp.Body)
//將從網頁中獲取的結構體放入函式並告訴函式結構體內的字元格式 返回utf8格式
utf8Reader := transform.NewReader(resp.Body,e.NewDecoder())
//將結構體內容返回
return ioutil.ReadAll(utf8Reader)
}
//判斷字元格式並返回
func DetermineEncoding(r io.Reader) encoding.Encoding{
//提取結構體內的前1024個字元
byte , err := bufio.NewReader(r).Peek(1024)
if err != nil{
panic(err)
}
//比較提取出來的字元進行判斷
e, _, _:= charset.DetermineEncoding(byte,"")
//返回判斷值
return e
}
由於提取基本上就是拷貝貼上過來的 就不多講了 唯一需要注意的是返回值型別
我們再看返回型別
package engine
//資訊存放位置 每一個資訊具有單獨不連續的記憶體
type Request struct {
Url string //存放地址
ParserFunc func([]byte) ParseResult // 存放函式型別
}
//資訊大的集合 注意結構體中的型別都為陣列 這是一個很關鍵的設定
type ParseResult struct{
Requests []Request //存放一個或多個Request供程式使用
Items []interface{} //存放多個引數 基本列印就靠他
}
//暫時設定為空讓程式跑起來 開始收集使用者資訊時 它會被替換掉
func Nilparser([] byte) ParseResult {
return ParseResult{}
}
返回型別是一個類似於 樹 的方式 一個節點套幾個節點 搞明白他們的轉換方式很重要
下面我們繼續看解析器
package parser
import (
"regexp"
"awesomeProject1/crawler/engine"
)
const citylistRe = `<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`
//提取所需內容 如網頁地址 城市名稱
func PrintCityList(contents []byte)engine.ParseResult{
//設定被提取者所需要的條件
re :=regexp.MustCompile(citylistRe)
//從網頁檔案中提取所需檔案
matches := re.FindAllSubmatch(contents, -1)
//提取出來的內容需要一個接受者 result定義型別
result := engine.ParseResult{}
for _, m := range matches{
//將提取出來的資訊按照順序放入變數result中
result.Items = append(result.Items,string(m[2]))
result.Requests = append(result.Requests,engine.Request{
string(m[1]),
engine.Nilparser,
})
}
//返回result result內部是陣列
return result
}
解析器也是我們之前接觸的 這裡需要注意的點也是返回值
最後看一下main函式
package main
import (
"awesomeProject1/crawler/engine"
"awesomeProject1/crawler/zhenai/parser"
)
func main() {
//執行爬蟲的起始條件 當內部迴圈一次結束後 裡面資訊屬於無效
engine.Run(engine.Request{
"http://www.zhenai.com/zhenghun",
parser.PrintCityList,
})
}
整個單任務版爬蟲的架構就出來了
下面我們再看一下列印結果
架構很成功
下面我們要測試一下citylist函式對不對
這裡的測試不需要太複雜
package parser
import (
"testing"
"io/ioutil"
)
func TestParseCityList(t *testing.T){
//原本應該是開啟網頁的 不過測試有可能主機不能聯網 網頁原始碼放到html檔案中 然後開啟html檔案
contents, err :=ioutil.ReadFile("citylist_test_data.html")
if err != nil{
panic(err)
}
//提取目標元素
result := PrintCityList(contents)
//之前獲取資訊 測試是否正確
const resultSize = 470
//之前獲取資訊 測試是否正確
expectedUrls := []string{
"http://www.zhenai.com/zhenghun/aba",
"http://www.zhenai.com/zhenghun/akesu",
"http://www.zhenai.com/zhenghun/alashanmeng",
}
//之前獲取資訊 測試是否正確
expectedCities := []string{
"阿壩","阿克蘇","阿拉善盟",
}
//正常測試
if len(result.Requests) != resultSize{
t.Errorf("result should have %d requests; but had %d", resultSize, len(result.Requests))
}
for i, url := range expectedUrls{
if result.Requests[i].Url != url{
t.Errorf("expected url #%d: %s ; but was %s \n",i, url, result.Requests[i].Url)
}
}
for i, city := range expectedCities{
if result.Items[i].(string) != city{
t.Errorf("expected url #%d: %s ; but was %s \n",i, city, result.Items[i].(string))
}
}
if len(result.Items) != resultSize{
t.Errorf("result should have %d requests; but had %d", resultSize, len(result.Items))
}
}
測試結果完全正確
既然我們獲得了城市名稱及每個城市的地址
那我們接下來獲取每個城市的第一頁使用者
既然需要獲取使用者資訊 我們先建立一個結構體用來儲存使用者資訊
package model
type Profile struct{
Name string //暱稱
Gender string //性別
Age int //年齡
Height int //身高
Weight int //體重
Income string //收入
Marriage string //婚姻
Education string //教育
Occupation string //職業
Hokou string //戶口
Xingzuo string //星座
House string //房子
Car string //車子
}
既然要獲取使用者的資訊 我們需要先找出使用者的URL 下面的函式幫助我們尋找使用者的URL
package parser
import (
"awesomeProject1/crawler/engine"
"regexp"
)
//獲取使用者資訊格式
const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)" [^>]*>([^<]+)</a>`
//contents為城市頁面地址 從每個城市第一頁中篩選資訊
func ParseCity(contents []byte) engine.ParseResult{
//確定要查詢的格式
re := regexp.MustCompile(cityRe)
//搜尋全部與格式相同的資訊
matches := re.FindAllSubmatch(contents, -1)
//建立結構體進行存放
result := engine.ParseResult{}
//把第一張頁面中的使用者名稱及地址取出
for _, m := range matches{
//m[2]為使用者名稱
name := string(m[2])
//在結構體中存入所有暱稱名字 並標識為User
result.Items = append(result.Items, "User "+name)
//這個函式中最關鍵的點
//將函式ParseProfile作為返回值 即確定了暱稱 由沒有改動結構體
result.Requests = append(result.Requests, engine.Request{
string(m[1]), //使用者頁面地址
func(c []byte) engine.ParseResult{ //使用者資訊
return ParseProfile(c, name)
},
})
}
return result
}
將爬取使用者詳細資訊的函式ParseProfile作為結構體返回值 返回
package parser
import (
"awesomeProject1/crawler/engine"
"regexp"
"strconv"
"awesomeProject1/crawler/model"
)
//獲取正則表示式條件 並且在全域性變數中定義
var Gender = regexp.MustCompile(`<td><span class="label">性別:</span><span field="">([^<]+)</span></td>`)
var ageRe = regexp.MustCompile(`<td><span class="label">年齡:</span>([\d]+)歲</td>`)
var Height = regexp.MustCompile(`<td><span class="label">身高:</span>([\d]+)CM</td>`)
var Weight = regexp.MustCompile(`<td><span class="label">體重:</span><span field="">([\d]+)KG</span></td>`)
var Income = regexp.MustCompile(`<td><span class="label">月收入:</span>([^<]+)</td>`)
var Marriage = regexp.MustCompile(`<td><span class="label">婚況:</span><span field=""> ([^<]+)</span></td>`)
var Education = regexp.MustCompile(`<td><span class="label">學歷:</span>([^<]+)</td>`)
var Occupation = regexp.MustCompile(`<td><span class="label">職業: </span>([^<]+)</td>`)
var Hokou = regexp.MustCompile(`<td><span class="label">籍貫:</span>([^<]+)</td>`)
var Xingzuo = regexp.MustCompile(`<td><span class="label">星座:</span><span field="">([^<]+)</span></td>`)
var House = regexp.MustCompile(`<td><span class="label">住房條件:</span><span field="">([^<]+)</span></td>`)
var Car = regexp.MustCompile(`<td><span class="label">是否購車:</span><span field="">([^<]+)</span></td>`)
//執行每一個條件 獲取每一個內容
func ParseProfile(contents []byte,name string) engine.ParseResult{
profile := model.Profile{}
age, _ := strconv.Atoi(extractString(contents,ageRe))
profile.Age = age
height, _ := strconv.Atoi(extractString(contents,Height))
profile.Height = height
weight, _ := strconv.Atoi(extractString(contents,Weight))
profile.Weight = weight
profile.Name = name
profile.Gender = extractString(contents,Gender)
profile.Income = extractString(contents,Income)
profile.Marriage = extractString(contents,Marriage)
profile.Education = extractString(contents,Education)
profile.Occupation = extractString(contents,Occupation)
profile.Hokou = extractString(contents,Hokou)
profile.Xingzuo = extractString(contents,Xingzuo)
profile.House = extractString(contents,House)
profile.Car = extractString(contents,Car)
//只需要傳入內容
result := engine.ParseResult{
Items: []interface{}{profile},
}
return result
}
//將正則表示式的篩選值輸出
func extractString(contents []byte, re *regexp.Regexp) string{
match := re.FindSubmatch(contents)
if len(match) >= 2{
return string(match[1])
}else{
return ""
}
}
正則表示式在使用者頁面中找到並複製下來修改
獲得使用者詳細資訊
先看我們單任務版的最終結構
我們再看執行結果
結果正確 至此我們的單任務版爬蟲成功
貼一下整段程式碼 可以複製下來自己測試
package main
import (
"awesomeProject1/crawler/engine"
"awesomeProject1/crawler/zhenai/parser"
)
func main() {
//執行爬蟲的起始條件 當內部迴圈一次結束後 裡面資訊屬於無效
engine.Run(engine.Request{
"http://www.zhenai.com/zhenghun",
parser.PrintCityList,
})
}
package engine
import (
"awesomeProject1/crawler/fetcher"
"log"
"fmt"
"time"
)
//引擎 控制整個程式的流程
func Run(seeds ...Request){
var requests []Request
//接收main函式傳過來的值
for _, r := range seeds{
requests = append(requests,r)
}
//利用傳過來的值進行 解析 及 提取
for len(requests) > 0 {
//獲取第一個值
r := requests[0]
//進行切片 把已經提取的內容篩選出去
requests = requests[1:]
//第一次列印為main函式傳入地址 然後每次列印是從r.ParserFunc函式中提取出的城市地址
log.Printf("Fetching %s\n", r.Url)
//將不同URL傳輸進去 返回不同的頁面原始碼
body, err:= fetcher.Fetch(r.Url)
//判斷URL是否正確 如果不正確 跳過此次迴圈
if err != nil{
log.Printf("Fetcher: error fetching url %s:%v",r.Url,err)
continue
}
//注意關鍵的地方 r為Request結構體變數 在main函式中 我們設定Request結構體變數中的ParserFunc值為parser.PrintCityList
//所以當第一次迴圈時r.ParserFunc(body)相當於parser.PrintCityList(body) 再一次體會到go語言的函數語言程式設計魅力
//r.ParserFunc(body)得到的結構體組分為城市名稱及要執行的函式 仔細揣摩結構體Request的ParserFunc值
//第一次迴圈成功後 parser.PrintCityList(body)被我們的engine.Nilparser代替 當然現在engine.Nilparser為空沒有任何返回值
ParseResult := r.ParserFunc(body)
//requests被填滿 requests又得到新的URL和運算函式 被抓取資訊只要足夠就可以一直執行下去
requests = append(requests,ParseResult.Requests...)
//列印所有在PrintCityList函式返回的Item值 Item值是任何型別可以使城市名也可以是使用者資訊
for _, item := range ParseResult.Items{
fmt.Printf("Got item %v\n", item)
}
time.Sleep(time.Millisecond)
}
}
package engine
//資訊存放位置 每一個資訊具有單獨不連續的記憶體
type Request struct {
Url string //存放地址
ParserFunc func([]byte) ParseResult // 存放函式型別
}
//資訊大的集合 注意結構體中的型別都為陣列 這是一個很關鍵的設定
type ParseResult struct{
Requests []Request //存放一個或多個Request供程式使用
Items []interface{} //存放多個引數 基本列印就靠他
}
//暫時設定為空讓程式跑起來 開始收集使用者資訊時 它會被替換掉
func Nilparser([] byte) ParseResult {
return ParseResult{}
}
package fetcher
import (
"net/http"
"fmt"
"golang.org/x/text/transform"
"io/ioutil"
"golang.org/x/text/encoding"
"bufio"
"golang.org/x/net/html/charset"
)
func Fetch(url string)([]byte, error){
//試探網頁能否正常開啟 若能則將網頁內容以結構體指標方式返回
resp , err :=http.Get(url)
if err != nil{
return nil, err
}
//程式結束時實現結構體指標關閉
defer resp.Body.Close()
//判斷頭部內容是否正確
if resp.StatusCode != http.StatusOK{
return nil, fmt.Errorf("Error Statuscode: %d", resp.StatusCode)
}
//自動判斷字元格式函式 詳細講解在函式體內
bodyReader := bufio.NewReader(resp.Body)
e := DetermineEncoding(bodyReader)
//將從網頁中獲取的結構體放入函式並告訴函式結構體內的字元格式 返回utf8格式
utf8Reader := transform.NewReader(bodyReader,e.NewDecoder())
//將結構體內容返回
return ioutil.ReadAll(utf8Reader)
}
//判斷字元格式並返回
func DetermineEncoding(r *bufio.Reader) encoding.Encoding{
//提取結構體內的前1024個字元
byte , err := r.Peek(1024)
if err != nil{
panic(err)
}
//比較提取出來的字元進行判斷
e, _, _:= charset.DetermineEncoding(byte,"")
//返回判斷值
return e
}
package model
type Profile struct{
Name string //暱稱
Gender string //性別
Age int //年齡
Height int //身高
Weight int //體重
Income string //收入
Marriage string //婚姻
Education string //教育
Occupation string //職業
Hokou string //戶口
Xingzuo string //星座
House string //房子
Car string //車子
}
package parser
import (
"awesomeProject1/crawler/engine"
"regexp"
)
//獲取使用者資訊格式
const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)" [^>]*>([^<]+)</a>`
//contents為城市頁面地址 從每個城市第一頁中篩選資訊
func ParseCity(contents []byte) engine.ParseResult{
//確定要查詢的格式
re := regexp.MustCompile(cityRe)
//搜尋全部與格式相同的資訊
matches := re.FindAllSubmatch(contents, -1)
//建立結構體進行存放
result := engine.ParseResult{}
//把第一張頁面中的使用者名稱及地址取出
for _, m := range matches{
//m[2]為使用者名稱
name := string(m[2])
//在結構體中存入所有暱稱名字 並標識為User
result.Items = append(result.Items, "User "+name)
//這個函式中最關鍵的點
//將函式ParseProfile作為返回值 即確定了暱稱 由沒有改動結構體
result.Requests = append(result.Requests, engine.Request{
string(m[1]), //使用者頁面地址
func(c []byte) engine.ParseResult{ //使用者資訊
return ParseProfile(c, name)
},
})
}
return result
}
package parser
import (
"regexp"
"awesomeProject1/crawler/engine"
)
const citylistRe = `<a href="(http://www.zhenai.com/zhenghun/[a-z0-9]+)"[^>]*>([^<]+)</a>`
//提取所需內容 如網頁地址 城市名稱
func PrintCityList(contents []byte)engine.ParseResult{
//設定被提取者所需要的條件
re :=regexp.MustCompile(citylistRe)
//從網頁檔案中提取所需檔案
matches := re.FindAllSubmatch(contents, -1)
//提取出來的內容需要一個接受者 result定義型別
result := engine.ParseResult{}
limit := 10
for _, m := range matches{
//將提取出來的資訊按照順序放入變數result中
result.Items = append(result.Items,"City" + string(m[2]))
result.Requests = append(result.Requests,engine.Request{
string(m[1]),
ParseCity,
})
limit--
if limit == 0 {
break
}
}
//返回result result內部是陣列
return result
}
package parser
import (
"testing"
"io/ioutil"
)
func TestParseCityList(t *testing.T){
//原本應該是開啟網頁的 不過測試有可能主機不能聯網 網頁原始碼放到html檔案中 然後開啟html檔案
contents, err :=ioutil.ReadFile("citylist_test_data.html")
if err != nil{
panic(err)
}
//提取目標元素
result := PrintCityList(contents)
//之前獲取資訊 測試是否正確
const resultSize = 470
//之前獲取資訊 測試是否正確
expectedUrls := []string{
"http://www.zhenai.com/zhenghun/aba",
"http://www.zhenai.com/zhenghun/akesu",
"http://www.zhenai.com/zhenghun/alashanmeng",
}
//之前獲取資訊 測試是否正確
expectedCities := []string{
"City阿壩","City阿克蘇","City阿拉善盟",
}
//正常測試
if len(result.Requests) != resultSize{
t.Errorf("result should have %d requests; but had %d", resultSize, len(result.Requests))
}
for i, url := range expectedUrls{
if result.Requests[i].Url != url{
t.Errorf("expected url #%d: %s ; but was %s \n",i, url, result.Requests[i].Url)
}
}
for i, city := range expectedCities{
if result.Items[i].(string) != city{
t.Errorf("expected url #%d: %s ; but was %s \n",i, city, result.Items[i].(string))
}
}
if len(result.Items) != resultSize{
t.Errorf("result should have %d requests; but had %d", resultSize, len(result.Items))
}
}
package parser
import (
"awesomeProject1/crawler/engine"
"regexp"
"strconv"
"awesomeProject1/crawler/model"
)
//獲取正則表示式條件 並且在全域性變數中定義
var Gender = regexp.MustCompile(`<td><span class="label">性別:</span><span field="">([^<]+)</span></td>`)
var ageRe = regexp.MustCompile(`<td><span class="label">年齡:</span>([\d]+)歲</td>`)
var Height = regexp.MustCompile(`<td><span class="label">身高:</span>([\d]+)CM</td>`)
var Weight = regexp.MustCompile(`<td><span class="label">體重:</span><span field="">([\d]+)KG</span></td>`)
var Income = regexp.MustCompile(`<td><span class="label">月收入:</span>([^<]+)</td>`)
var Marriage = regexp.MustCompile(`<td><span class="label">婚況:</span><span field=""> ([^<]+)</span></td>`)
var Education = regexp.MustCompile(`<td><span class="label">學歷:</span>([^<]+)</td>`)
var Occupation = regexp.MustCompile(`<td><span class="label">職業: </span>([^<]+)</td>`)
var Hokou = regexp.MustCompile(`<td><span class="label">籍貫:</span>([^<]+)</td>`)
var Xingzuo = regexp.MustCompile(`<td><span class="label">星座:</span><span field="">([^<]+)</span></td>`)
var House = regexp.MustCompile(`<td><span class="label">住房條件:</span><span field="">([^<]+)</span></td>`)
var Car = regexp.MustCompile(`<td><span class="label">是否購車:</span><span field="">([^<]+)</span></td>`)
//執行每一個條件 獲取每一個內容
func ParseProfile(contents []byte,name string) engine.ParseResult{
profile := model.Profile{}
age, _ := strconv.Atoi(extractString(contents,ageRe))
profile.Age = age
height, _ := strconv.Atoi(extractString(contents,Height))
profile.Height = height
weight, _ := strconv.Atoi(extractString(contents,Weight))
profile.Weight = weight
profile.Name = name
profile.Gender = extractString(contents,Gender)
profile.Income = extractString(contents,Income)
profile.Marriage = extractString(contents,Marriage)
profile.Education = extractString(contents,Education)
profile.Occupation = extractString(contents,Occupation)
profile.Hokou = extractString(contents,Hokou)
profile.Xingzuo = extractString(contents,Xingzuo)
profile.House = extractString(contents,House)
profile.Car = extractString(contents,Car)
//只需要傳入內容
result := engine.ParseResult{
Items: []interface{}{profile},
}
return result
}
//將正則表示式的篩選值輸出
func extractString(contents []byte, re *regexp.Regexp) string{
match := re.FindSubmatch(contents)
if len(match) >= 2{
return string(match[1])
}else{
return ""
}
}
好了 完整單任務版程式碼都實現了 最後我們總結一下都用到了什麼
獲取網頁內容
.使用http.Get獲取內容
.使用Encoding來轉碼 :gbk->utf8
.使用charset.DetermineEncoding來判斷編碼
獲取城市資訊及連結
.使用css選擇器
.使用xpath (與css類似)
.使用正則表示式
爬蟲總體演算法
城市列表 城市列表解析器
城市 城市 城市解析器
使用者 使用者 使用者 使用者 使用者解析器
解析器Parser
輸入:utf-8編碼的文字
輸出:Request{URL, 對應的Parser}列表, Item列表 其中Item就是我們存取的有價值的資料
單任務版爬蟲結束