-
爬蟲的分類
網絡爬蟲分為兩類
1. 通用爬蟲: 類似于baidu, google. 他們會把大量的資料挖下來, 儲存到自己的伺服器上. 使用者打開跳轉的時候, 其實先是跳轉到他們自己的伺服器.
2. 聚焦爬蟲: 其實就是有目标的爬蟲, 比如我隻需要内容資訊. 那我就隻爬取内容資訊.
通常我們使用的爬蟲都是聚焦爬蟲
-
項目總體結構
爬蟲的思想很簡單.
1. 寫一段程式, 從網絡上把資料抓下來
2. 儲存到我們的資料庫中
3. 寫一個前端頁面, 展示資料
-
go語言的爬蟲庫/架構
以上是go語言中已經you封裝好的爬蟲庫或者架構, 但我們寫爬蟲的目的是為了學習. 是以.....不使用架構了
-
本課程的爬蟲項目
1. 不用已有的爬蟲庫和架構
2. 資料庫使用ElasticSearch
3. 頁面展示使用标準庫的http
這個練習的目的,就是使用go基礎.之是以選擇爬蟲,是因為爬蟲有一定的複雜性
-
爬蟲的主題
哈哈, 要是還沒有女盆友, 又不想花錢的童鞋, 可以自己學習一下爬蟲技術
-
如何發現使用者
1. 通過http://www.zhenai.com/zhenghun頁面進入. 這是一個位址清單頁. 你想要找的那個她(他)是哪個城市的
2. 在使用者的詳情頁, 有推薦--猜你喜歡
-
爬蟲總體算法
1. 城市清單, 找到一個城市
2. 城市下面有使用者清單. 點選某一個使用者, 進去檢視使用者的詳情資訊
3. 使用者詳情頁右側有猜你喜歡, 連結到一個新的使用者詳情頁
需要注意的是, 使用者推薦, 會出現重複推薦的情況. 第一個頁面推薦了張三, 從上三進來推薦了李四. 從李四進來有推薦到第一個頁面了. 這就形成了死循環, 重複推薦
我們完成爬蟲, 分為三個階段
1. 單機版. 将所有功能在一個引用裡完成
2. 并發版. 有多個連接配接同時通路, 這裡使用了go的協程
3. 分布式. 多并發演進就是分布式了. 削峰, 減少伺服器的壓力.
下面開始項目階段
項目
一. 單任務版網絡爬蟲
目标: 抓取珍愛網中的使用者資訊.
1. 抓取使用者所在的城市清單資訊
2. 抓取某一個城市的某一個人的基本資訊, 把資訊存到我們自己的資料庫中
分析:
1. 通過url擷取網站資料. 拿到我們想要的位址,以及點選位址跳轉的url. 把位址資訊儲存到資料庫. 資料量預估300
2. 通過url循環擷取使用者清單. 拿到頁面詳情url, 在擷取使用者詳情資訊. 把使用者資訊儲存到資料庫. 資料量會比較大. 一個城市如果有10000個人注冊了, 那麼就有300w的資料量.
3. 是以, 資料庫選擇的是elasticSearch
-------------------
抓取城市清單頁, 也就是目标把這個頁面中我們要的内容抓取下來.
其實就兩個内容, 1. 城市名稱, 2. 點選城市名稱跳轉的url
第一步: 抓取頁面内容
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
)
func main() {
// 第一步, 通過url抓取頁面
resp, err := http.Get("http://www.zhenai.com/zhenghun")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
// 讀取出來body的所有内容
all, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
//fmt.Printf("%s\n", all)
printCityList(all)
}
第二步: 正規表達式, 提取城市名稱和跳轉的url
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
)
func main() {
// 第一步, 通過url抓取頁面
resp, err := http.Get("http://www.zhenai.com/zhenghun")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
// 讀取出來body的所有内容
all, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
//fmt.Printf("%s\n", all)
printCityList(all)
}
/**
* 正規表達式提取城市名稱和跳轉的url
*/
func printCityList(content []byte) {
re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z1-9]+)" data-v-5e16505f>([^<]+)</a>`)
all := re.FindAllSubmatch(content, -1)
for _, line := range all {
fmt.Printf("city: %s, url: %s\n", line[2], line[1])
}
}
結果如下:
這樣第一個頁面就抓取完成了. 第二個和第三個頁面可以了類似處理. 但這樣不好, 我們需要把結構進行抽象提取. 形成一個通用的子產品
再來分析我們的單機版爬蟲項目
項目結構---共有三層結構:
- 城市清單解析器: 用來解析城市清單
- 城市解析器: 用來解析某一個城市的頁面内容, 城市裡是使用者清單和分頁
- 使用者解析器: 從城市頁面點選使用者進入到使用者的詳情頁, 解析使用者的詳情資訊
解析器抽象
既然都是解析器, 那麼我們就把解析器抽象出來.
每一個解析器, 都有輸入參數和輸出參數
輸入參數: 通過url抓取的網頁内容.
輸出參數: Request{URL, Parse}清單, Item清單
為什麼輸出的第一個參數是Request{URL, Parse}清單呢?
- 城市清單解析器, 我們擷取到城市名稱和url, 點解url, 要進入的是城市解析器. 是以這裡的解析器應該是城市解析器.
- 城市解析器. 我們進入城市以後, 會擷取使用者的姓名和使用者詳情頁的url. 是以這裡的解析器, 應該傳的是使用者解析器.
- 使用者解析器. 用來解析使用者的資訊. 儲存入庫
項目架構
1. 有一個或多個種子頁面, 發情請求到處理引擎. 引擎不是馬上就對任務進行處理的. 他首先吧種子頁面添加到隊列裡去
2. 處理引擎從隊列中取出要處理的url, 交給提取器提取頁面内容. 然後将頁面内容傳回
3. 将頁面内容進行解析, 傳回的是Request{URL, Parse}清單和 Items清單
4. 我們将Request添加到任務隊列中. 然後下一次依然從任務隊列中取出一條記錄. 這樣就循環往複下去了
5. 隊列什麼時候結束呢? 有可能不會結束, 比如循環推薦, 也可能可以結束.
這樣,結構都有了, 入參出參也定義好了, 接下來就是編碼實作
我們先來改寫上面的抓取城市清單
項目結構
1. 有一個提取器
2. 有一個解析器. 解析器裡應該有三種類型的解析器
3. 有一個引擎來觸發操作
4. 有一個main方法入口
第一步: Fetcher--提取器
package fetcher
import (
"fmt"
"io/ioutil"
"net/http"
)
// 抓取器
func Fetch(url string) ([]byte, error) {
// 第一步, 通過url抓取頁面
client := http.Client{}
request, err := http.NewRequest("GET", url, nil)
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36")
resp, err := client.Do(request)
//resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("http get error :%s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http get error errCode:%d", http.StatusOK)
}
// 讀取出來body的所有内容
return ioutil.ReadAll(resp.Body)
}
第二步: 有一個城市解析器
package parser
import (
"aaa/crawler/zhenai/engine"
"regexp"
)
const cityListRegexp = `<a[^href]*href="(http://www.zhenai.com/zhenghun/[a-z1-9]+)"[^>]*>([^<]+)</a>`
func ParseCityList(content []byte) (engine.ParseResult) {
re := regexp.MustCompile(cityListRegexp)
all := re.FindAllSubmatch(content, -1)
pr := engine.ParseResult{}
count := 1
for _, line := range all {
req := engine.Request{
Url:string(line[1]),
ParseFun: ParseCity,
}
pr.Req = append(pr.Req, req)
pr.Items = append(pr.Items, "City: " + string(line[2]))
count --
if count <=0 {
break
}
}
return pr
}
第三步:定義引擎需要使用的結構體
package engine
type Request struct {
Url string
ParseFun func(content []byte) ParseResult
}
type ParseResult struct {
Req []Request
Items []interface{}
}
func NilParse(content []byte) ParseResult{
return ParseResult{}
}
第四步: 抽象出引擎
package engine
import (
"aaa/crawler/fetcher"
"fmt"
"github.com/astaxie/beego/logs"
)
func Run(seeds ...Request) {
var que []Request
for _, seed := range seeds {
que = append(que, seed)
}
for len(que) > 0 {
cur := que[0]
que = que[1:]
logs.Info("fetch url:", cur.Url)
cont, e := fetcher.Fetch(cur.Url)
if e != nil {
logs.Info("解析頁面異常 url:", cur.Url)
continue
}
resultParse := cur.ParseFun(cont)
que = append(que, resultParse.Req...)
for _, item := range resultParse.Items {
fmt.Printf("内容項: %s \n", item)
}
}
}
第五步: 定義程式入口
package main
import (
"aaa/crawler/zhenai/engine"
"aaa/crawler/zhenai/parser"
)
func main() {
req := engine.Request{
Url:"http://www.zhenai.com/zhenghun",
ParseFun: parser.ParseCityList,
}
engine.Run(req)
}
第六步: 城市解析器
package parser
import (
"aaa/crawler/zhenai/engine"
"regexp"
)
const cityRe = `<a[^href]*href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`
func ParseCity(content []byte) engine.ParseResult{
cityRegexp:= regexp.MustCompile(cityRe)
subs := cityRegexp.FindAllSubmatch(content, -1)
pr := engine.ParseResult{}
for _, sub := range subs {
name := string(sub[2])
// 擷取使用者的詳細位址
re := engine.Request{
Url:string(sub[1]),
// 注意, 這裡定義了一個函數來傳遞, 這樣可以吧name也傳遞過去
ParseFun: func(content []byte) engine.ParseResult {
return ParseUser(content, name)
},
}
pr.Req = append(pr.Req, re)
pr.Items = append(pr.Items, "Name: " + string(sub[2]))
}
return pr
}
城市解析器和城市清單解析器基本類似. 傳回的資料是request和使用者名
第七步: 使用者解析器
package parser
import (
"aaa/crawler/zhenai/engine"
"aaa/crawler/zhenai/model"
"regexp"
"strconv"
"strings"
)
// 個人基本資訊
const userRegexp = `<div[^class]*class="m-btn purple"[^>]*>([^<]+)</div>`
// 個人隐私資訊
const userPrivateRegexp = `<div data-v-8b1eac0c="" class="m-btn pink">([^<]+)</div>`
// 擇偶條件
const userPartRegexp = `<div data-v-8b1eac0c="" class="m-btn">([^<]+)</div>`
func ParseUser(content []byte, name string) engine.ParseResult {
pro := model.Profile{}
pro.Name = name
// 擷取使用者的年齡
userCompile := regexp.MustCompile(userRegexp)
usermatch := userCompile.FindAllSubmatch(content, -1)
pr := engine.ParseResult{}
for i, userInfo := range usermatch {
text := string(userInfo[1])
if i == 0 {
pro.Marry = text
continue
}
if strings.Contains(text, "歲") {
age, _ := strconv.Atoi(strings.Split(text, "歲")[0])
pro.Age = age
continue
}
if strings.Contains(text, "座") {
pro.Xingzuo = text
continue
}
if strings.Contains(text, "cm") {
height, _ := strconv.Atoi(strings.Split(text, "cm")[0])
pro.Height = height
continue
}
if strings.Contains(text, "kg") {
weight, _ := strconv.Atoi(strings.Split(text, "kg")[0])
pro.Weight = weight
continue
}
if strings.Contains(text, "工作地:") {
salary := strings.Split(text, "工作地:")[1]
pro.Salary = salary
continue
}
if strings.Contains(text, "月收入:") {
salary := strings.Split(text, "月收入:")[1]
pro.Salary = salary
continue
}
if i == 7 {
pro.Occuption = text
continue
}
if i == 8 {
pro.Education = text
continue
}
}
pr.Items = append(pr.Items, pro)
return pr
}
看一下抓取的效果吧
抓取的城市清單
抓取的某個城市的使用者清單
具體某個人的詳細資訊
至此, 完成了單機版爬蟲. 再來回顧一下.
做完了感覺, 這個爬蟲其實很簡單, 之前用java都實作過.隻不過這次是用go實作的
- 有一個種子頁面, 從這個頁面進來, 會擷取到源源不斷的使用者資訊
- 遇到一個403的問題. 需要使用自定義的http請求, 設定header 的User-agent,否則伺服器請求被拒絕
- 使用函數式程式設計. 函數的特點就是靈活. 靈活多變. 想怎麼封裝都行. 這裡是在cityParse解析出user資訊的時候,使用了函數式程式設計.把使用者名傳遞過去了