golang练手小项目系列(2)-并发爬虫

本系列整理了10个工作量和难度适中的Golang小项目,适合已经掌握Go语法的工程师进一步熟练语法和常用库的用法。

问题描述:

实现一个网络爬虫,以输入的URL为起点,使用广度优先顺序访问页面。

要点:

实现对多个页面的并发访问,同时访问的页面数由参数 -concurrency 指定,默认为 20。

使用 -depth     指定访问的页面深度,默认为 3。

注意已经访问过的页面不要重复访问。

扩展:

将访问到的页面写入到本地以创建目标网站的本地镜像,注意,只有指定域名下的页面需要采集,写入本地的页面里的<a>元素的href的值需要被修改为指向镜像页面,而不是原始页面。

实现

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时表示进入unseenLinks队列,值为2时表示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个页面", 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)

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容