实现一个go语言的简单爬虫来爬取CSDN博文(一)

来源:互联网 发布:js null 编辑:程序博客网 时间:2024/05/23 10:52

http://blog.csdn.net/tyBaoErGe/article/details/50375802?hmsr=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com

前言

如何实现一个爬虫系统或则简单的小脚本?一般是定义一个入口页面,然后一个页面会有其他页面的URL,于是从当前页面获取到这些URL加入到爬虫的抓取队列中,然后进入到新页面后再递归的进行上述的操作,其实说来就跟深度遍历或广度遍历一样。 
golang由于其编译速度很快,而且对并发(goroutine)的天然支持,配合chan的协程处理,可以很好地实现一个稳定高效的爬虫系统.

用到的包

完全不借助第三方的框架,通过go sdk的标准库来实现一个爬虫应用,主要用到的包

  • net/http 标准库里内建了对http协议的支持,实现了一个http client,可以直接通过其进行get,post等请求
  • strings 不像java的String是一个引用类型,go语言中的字符串类型是一个内建的基础类型, 而且go语言默认只支持UTF-8编码,strings包实现了一些简单的针对utf-8字符串操作的函数
  • regexp go sdk中的正则表达式包
  • io/ioutil io处理的工具包
  • encoding/xml 解析xml的包

channel机制

Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论。 CSP模型的消息传递在收发消息进程间包含了一个交会点,即发送方只能在接收方准备好接收消息时才能发送消息。 
golang在其并发实现中,主要是用channel来实现通信的。其中channel包括两种,缓冲的channel和非缓冲的channel.

  • 缓冲的channel:保证往缓冲中存数据先于对应的取数据,简单说就是在取的时候里面肯定有数据,否则就因取不到而阻塞.
  • 非缓冲的channel:保证取数据先于存数据,就是保证存的时候肯定有其他的goroutine在取,否则就因放不进去而阻塞。

Go Channel基本操作语法

Go Channel的基本操作语法如下:

c := make(chan bool) //创建一个无缓冲的bool型Channel
c <- x        //向一个Channel发送一个值<- c          //从一个Channel中接收一个值x = <- c      //从Channel c接收一个值并将其存储到x中x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false
  • 1
  • 2
  • 3
  • 4
  • 5

不带缓冲的Channel兼具通信和同步两种特性,适合协调多个routines。

for/select的基本操作

我们在使用select时很少只是对其进行一次evaluation,我们常常将其与for {}结合在一起使用,并选择适当时机从for{}中退出。

for {        select {        case x := <- somechan:            // … 使用x进行一些操作        case y, ok := <- someOtherchan:            // … 使用y进行一些操作,            // 检查ok值判断someOtherchan是否已经关闭        case outputChan <- z:            // … z值被成功发送到Channel上时        default:            // … 上面case均无法通信时,执行此分支        }} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

range操作

Golang中的for range除了可以迭代一些集合类型还可以来循环从channel中取数据,当channel中无数据时便阻塞当前循环。

for url := range urlChannel {        fmt.Println("routines num = ", runtime.NumGoroutine(), "chan len = ", len(urlChannel))        go Spy(url)}
  • 1
  • 2
  • 3
  • 4

goroutine

Go语言通过goroutine提供了对于并发编程的非常清晰直接的支持,但goroutine是Go语言运行库的功能,不是操作系统提供的功能,goroutine不是用线程实现的.goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行

除了被系统调用阻塞的线程外,Go运行库最多会启动$GOMAXPROCS个线程来运行goroutine
  • 1
  • 2

实现CSDN博文爬虫

由于实现的爬虫功能简单,所以所有代码均再main包下完成. 
首先我们需要在main包下声明一个全局的urlchannel用来同步开启的多个routines在某个页面获取的<a> 标签的href属性

var urlChannel = make(chan string, 200) //chan中存入string类型的href属性,缓冲200
  • 1

声明在html文档中获取<a> 的正则表达式

var atagRegExp = regexp.MustCompile(`<a[^>]+[(href)|(HREF)]\s*\t*\n*=\s*\t*\n*[(".+")|('.+')][^>]*>[^<]*</a>`) //以Must前缀的方法或函数都是必须保证一定能执行成功的,否则将引发一次panic
  • 1

入口函数main

当进入main函数时,将启动一个goroutine来从入口url=”http:/blog.csdn.net”开始爬取(Spy函数)页面内容分析<a> 标签 
接下来通过for range urlChannel来循环取出爬取到的<a> 标签中的href属性,并再次开启一个新的goroutine来爬取这个href属性对应的html文档内容

func main() {    go Spy("http:/blog.csdn.net")    //go Spy("http://www.iteye.com/")    for url := range urlChannel {        fmt.Println("routines num = ", runtime.NumGoroutine(), "chan len = ", len(urlChannel)) //通过runtime可以获取当前运行时的一些相关参数等        go Spy(url)    }    fmt.Println("a")}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Spy函数

由于每个爬取goroutine都是调用Spy函数来分析一个url对应的html文档,所以需要在函数开始就defer 一个匿名函数来处理(recover)可能出现的异常(panic),防止异常导致程序终止,defer执行的函数会在当前函数执行完成后结果返回前执行,无论该函数是panic的还是正常执行

    defer func() {        if r := recover(); r != nil {            log.Println("[E]", r)        }    }()
  • 1
  • 2
  • 3
  • 4
  • 5

由于go内建了对http协议的支持,可以直接通过http包下的http.Get或则http.Post函数来请求url.但由于大部分网站对请求都有防范DDOS等的限制,需要自定义请求的header,设置代理服务器(CSDN好像对同一IP的请求平率限制并不严格,iteye亲测很严格,每分钟上万会被封住IP)等操作,可以使用http包下的http.NewRequest(method, urlStr string, body io.Reader) (*Request, error)函数,然后通过Request的Header对象设置User-Agent,Host等,最后调用http包下内置的DefaultClient对象的Do方法完成请求. 
当拿到服务器响应后(*Response)通过ioutil包下的工具函数转换为string,找出文档中的<a>标签 分析出href属性,存入urlChannel中.

func Spy(url string) {    defer func() {        if r := recover(); r != nil {            log.Println("[E]", r)        }    }()    req, _ := http.NewRequest("GET", url, nil)    req.Header.Set("User-Agent", GetRandomUserAgent())    client := http.DefaultClient    res, e := client.Do(req)    if e != nil {        fmt.Errorf("Get请求%s返回错误:%s", url, e)        return    }    if res.StatusCode == 200 {        body := res.Body        defer body.Close()        bodyByte, _ := ioutil.ReadAll(body)        resStr := string(bodyByte)        atag := atagRegExp.FindAllString(resStr, -1)        for _, a := range atag {            href,_ := GetHref(a)            if strings.Contains(href, "article/details/") {                fmt.Println("☆", href)            }else {                fmt.Println("□", href)            }            urlChannel <- href        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

随机伪造User-Agent

var userAgent = [...]string{"Mozilla/5.0 (compatible, MSIE 10.0, Windows NT, DigExt)",    "Mozilla/4.0 (compatible, MSIE 7.0, Windows NT 5.1, 360SE)",    "Mozilla/4.0 (compatible, MSIE 8.0, Windows NT 6.0, Trident/4.0)",    "Mozilla/5.0 (compatible, MSIE 9.0, Windows NT 6.1, Trident/5.0,",    "Opera/9.80 (Windows NT 6.1, U, en) Presto/2.8.131 Version/11.11",    "Mozilla/4.0 (compatible, MSIE 7.0, Windows NT 5.1, TencentTraveler 4.0)",    "Mozilla/5.0 (Windows, U, Windows NT 6.1, en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",    "Mozilla/5.0 (Macintosh, Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",    "Mozilla/5.0 (Macintosh, U, Intel Mac OS X 10_6_8, en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",    "Mozilla/5.0 (Linux, U, Android 3.0, en-us, Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13",    "Mozilla/5.0 (iPad, U, CPU OS 4_3_3 like Mac OS X, en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",    "Mozilla/4.0 (compatible, MSIE 7.0, Windows NT 5.1, Trident/4.0, SE 2.X MetaSr 1.0, SE 2.X MetaSr 1.0, .NET CLR 2.0.50727, SE 2.X MetaSr 1.0)",    "Mozilla/5.0 (iPhone, U, CPU iPhone OS 4_3_3 like Mac OS X, en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5",    "MQQBrowser/26 Mozilla/5.0 (Linux, U, Android 2.3.7, zh-cn, MB200 Build/GRJ22, CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"}var r = rand.New(rand.NewSource(time.Now().UnixNano()))func GetRandomUserAgent() string {    return userAgent[r.Intn(len(userAgent))]}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

解析<a> 元素

<a> 可以当做一份xml文档(只有一个a为根节点的简单xml)来解析出href/HREF属性,通过go标准库中xml.NewDecoder来完成

func GetHref(atag string) (href,content string) {    inputReader := strings.NewReader(atag)    decoder := xml.NewDecoder(inputReader)    for t, err := decoder.Token(); err == nil; t, err = decoder.Token() {        switch token := t.(type) {        // 处理元素开始(标签)        case xml.StartElement:            for _, attr := range token.Attr {                attrName := attr.Name.Local                attrValue := attr.Value                if(strings.EqualFold(attrName,"href") || strings.EqualFold(attrName,"HREF")){                    href = attrValue                }            }        // 处理元素结束(标签)        case xml.EndElement:        // 处理字符数据(这里就是元素的文本)        case xml.CharData:            content = string([]byte(token))        default:            href = ""            content = ""        }    }    return href, content}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

总结

通过以上代码,一个简单的网络爬虫就实现了.而且对goroutine和range的配合使用基本就了解了.但如你所见,goroutine的运行机制和chan的设计原理绝非以上寥寥数句代码就可窥见其真面目. 
go语言实现的简单爬虫来爬取CSDN博文

原创粉丝点击