golang練手小項(xiàng)目系列(2)-并發(fā)爬蟲

本系列整理了10個(gè)工作量和難度適中的Golang小項(xiàng)目咧七,適合已經(jīng)掌握Go語法的工程師進(jìn)一步熟練語法和常用庫的用法臂痕。

問題描述:

實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)爬蟲奏篙,以輸入的URL為起點(diǎn),使用廣度優(yōu)先順序訪問頁面挑童。

要點(diǎn):

實(shí)現(xiàn)對(duì)多個(gè)頁面的并發(fā)訪問,同時(shí)訪問的頁面數(shù)由參數(shù)?-concurrency?指定跃须,默認(rèn)為?20站叼。

使用?-depth ? ? 指定訪問的頁面深度,默認(rèn)為 3菇民。

注意已經(jīng)訪問過的頁面不要重復(fù)訪問尽楔。

擴(kuò)展:

將訪問到的頁面寫入到本地以創(chuàng)建目標(biāo)網(wǎng)站的本地鏡像,注意第练,只有指定域名下的頁面需要采集阔馋,寫入本地的頁面里的<a>元素的href的值需要被修改為指向鏡像頁面,而不是原始頁面娇掏。

實(shí)現(xiàn)

import (

? "bytes"

? "flag"

? "fmt"

? "golang.org/x/net/html"

? "io"

? "log"

? "net/http"

? "net/url"

? "os"

? "path/filepath"

? "strings"

? "sync"

? "time"

)

type URLInfo struct {

? url string

? depth int

}

var base *url.URL

func forEachNode(n *html.Node, pre, post func(n *html.Node)){

? if pre != nil{

? ? ? pre(n)

? }

? for c := n.FirstChild; c != nil; c = c.NextSibling{

? ? ? forEachNode(c, pre, post)

? }

? if post != nil{

? ? ? post(n)

? }

}

func linkNodes(n *html.Node) []*html.Node {

? var links []*html.Node

? visitNode := func(n *html.Node) {

? ? ? if n.Type == html.ElementNode && n.Data == "a" {

? ? ? ? links = append(links, n)

? ? ? }

? }

? forEachNode(n, visitNode, nil)

? return links

}

func linkURLs(linkNodes []*html.Node, base *url.URL) []string {

? var urls []string

? for _, n := range linkNodes {

? ? ? for _, a := range n.Attr {

? ? ? ? if a.Key != "href" {

? ? ? ? ? ? continue

? ? ? ? }

? ? ? ? link, err := base.Parse(a.Val)

? ? ? ? // ignore bad and non-local URLs

? ? ? ? if err != nil {

? ? ? ? ? ? log.Printf("skipping %q: %s", a.Val, err)

? ? ? ? ? ? continue

? ? ? ? }

? ? ? ? if link.Host != base.Host {

? ? ? ? ? ? //log.Printf("skipping %q: non-local host", a.Val)

? ? ? ? ? ? continue

? ? ? ? }

? ? ? ? if strings.HasPrefix(link.String(), "javascript"){

? ? ? ? ? ? continue

? ? ? ? }

? ? ? ? urls = append(urls, link.String())

? ? ? }

? }

? return urls

}

func rewriteLocalLinks(linkNodes []*html.Node, base *url.URL) {

? for _, n := range linkNodes {

? ? ? for i, a := range n.Attr {

? ? ? ? if a.Key != "href" {

? ? ? ? ? ? continue

? ? ? ? }

? ? ? ? link, err := base.Parse(a.Val)

? ? ? ? if err != nil || link.Host != base.Host {

? ? ? ? ? ? continue // ignore bad and non-local URLs

? ? ? ? }

? ? ? ? link.Scheme = ""

? ? ? ? link.Host = ""

? ? ? ? link.User = nil

? ? ? ? a.Val = link.String()

? ? ? ? n.Attr[i] = a

? ? ? }

? }

}

func Extract(url string)(urls []string, err error){

? timeout := time.Duration(10 * time.Second)

? client := http.Client{

? ? ? Timeout: timeout,

? }

? resp, err := client.Get(url)

? if err != nil{

? ? ? fmt.Println(err)

? ? ? return nil, err

? }

? if resp.StatusCode != http.StatusOK{

? ? ? resp.Body.Close()

? ? ? return nil, fmt.Errorf("getting %s:%s", url, resp.StatusCode)

? }

? if err != nil{

? ? ? return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)

? }

? u, err := base.Parse(url)

? if err != nil {

? ? ? return nil, err

? }

? if base.Host != u.Host {

? ? ? log.Printf("not saving %s: non-local", url)

? ? ? return nil, nil

? }

? var body io.Reader

? contentType := resp.Header["Content-Type"]

? if strings.Contains(strings.Join(contentType, ","), "text/html") {

? ? ? doc, err := html.Parse(resp.Body)

? ? ? resp.Body.Close()

? ? ? if err != nil {

? ? ? ? return nil, fmt.Errorf("parsing %s as HTML: %v", u, err)

? ? ? }

? ? ? nodes := linkNodes(doc)

? ? ? urls = linkURLs(nodes, u)

? ? ? rewriteLocalLinks(nodes, u)

? ? ? b := &bytes.Buffer{}

? ? ? err = html.Render(b, doc)

? ? ? if err != nil {

? ? ? ? log.Printf("render %s: %s", u, err)

? ? ? }

? ? ? body = b

? }

? err = save(resp, body)

? return urls, err

}

func crawl(url string) []string{

? list, err := Extract(url)

? if err != nil{

? ? ? log.Print(err)

? }

? return list

}

func save(resp *http.Response, body io.Reader) error {

? u := resp.Request.URL

? filename := filepath.Join(u.Host, u.Path)

? if filepath.Ext(u.Path) == "" {

? ? ? filename = filepath.Join(u.Host, u.Path, "index.html")

? }

? err := os.MkdirAll(filepath.Dir(filename), 0777)

? if err != nil {

? ? ? return err

? }

? fmt.Println("filename:", filename)

? file, err := os.Create(filename)

? if err != nil {

? ? ? return err

? }

? if body != nil {

? ? ? _, err = io.Copy(file, body)

? } else {

? ? ? _, err = io.Copy(file, resp.Body)

? }

? if err != nil {

? ? ? log.Print("save: ", err)

? }

? err = file.Close()

? if err != nil {

? ? ? log.Print("save: ", err)

? }

? return nil

}

func parallellyCrawl(initialLinks string, concurrency, depth int){

? worklist := make(chan []URLInfo, 1)

? unseenLinks := make(chan URLInfo, 1)

? //值為1時(shí)表示進(jìn)入unseenLinks隊(duì)列呕寝,值為2時(shí)表示crawl完成

? seen := make(map[string] int)

? seenLock := sync.Mutex{}

? var urlInfos []URLInfo

? for _, url := range strings.Split(initialLinks, " "){

? ? ? urlInfos = append(urlInfos, URLInfo{url, 1})

? }

? go func() {worklist <- urlInfos}()

? go func() {

? ? ? for{

? ? ? ? time.Sleep(1 * time.Second)

? ? ? ? seenFlag := true

? ? ? ? seenLock.Lock()

? ? ? ? for k := range seen{

? ? ? ? ? ? if seen[k] == 1{

? ? ? ? ? ? ? seenFlag = false

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? seenLock.Unlock()

? ? ? ? if seenFlag && len(worklist) == 0{

? ? ? ? ? ? close(unseenLinks)

? ? ? ? ? ? close(worklist)

? ? ? ? ? ? break

? ? ? ? }

? ? ? }

? }()

? for i := 0; i < concurrency; i++{

? ? ? go func() {

? ? ? ? for link := range unseenLinks{

? ? ? ? ? ? foundLinks := crawl(link.url)

? ? ? ? ? ? var urlInfos []URLInfo

? ? ? ? ? ? for _, u := range foundLinks{

? ? ? ? ? ? ? urlInfos = append(urlInfos, URLInfo{u, link.depth + 1})

? ? ? ? ? ? }

? ? ? ? ? ? go func(finishedUrl string) {

? ? ? ? ? ? ? worklist <- urlInfos

? ? ? ? ? ? ? seenLock.Lock()

? ? ? ? ? ? ? seen[finishedUrl] = 2

? ? ? ? ? ? ? seenLock.Unlock()

? ? ? ? ? ? }(link.url)

? ? ? ? }

? ? ? }()

? }

? for list := range worklist{

? ? ? for _, link := range list {

? ? ? ? if link.depth > depth{

? ? ? ? ? ? continue

? ? ? ? }

? ? ? ? seenLock.Lock()

? ? ? ? _, ok := seen[link.url]

? ? ? ? seenLock.Unlock()

? ? ? ? if !ok{

? ? ? ? ? ? seenLock.Lock()

? ? ? ? ? ? seen[link.url] = 1

? ? ? ? ? ? seenLock.Unlock()

? ? ? ? ? ? unseenLinks <- link

? ? ? ? }

? ? ? }

? }

? fmt.Printf("共訪問了%d個(gè)頁面", len(seen))

}

func main() {

? var maxDepth int

? var concurrency int

? var initialLink string

? flag.IntVar(&maxDepth, "d", 3, "max crawl depth")

? flag.IntVar(&concurrency, "c", 20, "number of crawl goroutines")

? flag.StringVar(&initialLink, "u", "", "initial link")

? flag.Parse()

? u, err := url.Parse(initialLink)

? if err != nil {

? ? ? fmt.Fprintf(os.Stderr, "invalid url: %s\n", err)

? ? ? os.Exit(1)

? }

? base = u

? parallellyCrawl(initialLink, concurrency, maxDepth)

}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市婴梧,隨后出現(xiàn)的幾起案子下梢,更是在濱河造成了極大的恐慌,老刑警劉巖塞蹭,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件孽江,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡番电,警方通過查閱死者的電腦和手機(jī)岗屏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漱办,“玉大人这刷,你說我怎么就攤上這事∶渚” “怎么了暇屋?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長撞牢。 經(jīng)常有香客問我率碾,道長,這世上最難降的妖魔是什么屋彪? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任所宰,我火速辦了婚禮,結(jié)果婚禮上畜挥,老公的妹妹穿的比我還像新娘仔粥。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布躯泰。 她就那樣靜靜地躺著谭羔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪麦向。 梳的紋絲不亂的頭發(fā)上瘟裸,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音诵竭,去河邊找鬼话告。 笑死,一個(gè)胖子當(dāng)著我的面吹牛卵慰,可吹牛的內(nèi)容都是我干的沙郭。 我是一名探鬼主播,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼裳朋,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼病线!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鲤嫡,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤送挑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后泛范,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體让虐,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡紊撕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年罢荡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片对扶。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡区赵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浪南,到底是詐尸還是另有隱情笼才,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布络凿,位于F島的核電站骡送,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏絮记。R本人自食惡果不足惜摔踱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怨愤。 院中可真熱鬧派敷,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至试躏,卻和暖如春猪勇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颠蕴。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來泰國打工埠对, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裁替。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓项玛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親弱判。 傳聞我的和親對(duì)象是個(gè)殘疾皇子襟沮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容