基于golang的文章信息抓取
学习golang爬虫,实现广度爬取,抓取特定的网页地址:测试站点新笔趣阁(https://www.xsbiquge.com/)
主要学习golang的goroutine和channel之间的协作,无限爬取站点小说的地址仅限书目录地址,不进行文章内容爬取
在学习中遇到了一些问题:例如共享变量map写入竞争、连接提前关闭问题等等
福利彩蛋:没有好玩的 API 接口?上百款免费接口等你来,免费 API,免费 API 大全
为了解决map类型共享变量竞争问题,封装map类型添加读写锁限制,防止不同的goroutine之间出现写入竞争(虽然只有goroutine比较多的时候才会出现)
package util
import "sync"
//封装一个map结构体,主要用来过滤相同的url地址
type VisitMap struct {
sync.RWMutex
visited map[string]bool
}
func (vis *VisitMap) ReadMap(url string) bool {
vis.RLock()
value := vis.visited[url]
vis.RUnlock()
return value
}
func (vis *VisitMap) WriteMap(url string) {
vis.Lock()
vis.visited[url] = true
vis.Unlock()
}
//channel封装
type Pool struct {
VisitMap *VisitMap
queue chan string
}
func New(size int) *Pool {
if size < 1 {
size = 1
}
visitMap := new(VisitMap)
visitMap.visited = make(map[string]bool)
return &Pool{
VisitMap: visitMap,
queue: make(chan string, size),
wg: &sync.WaitGroup{},
}
}
接下来是main函数处理
package main
import (
"fmt"
"github.com/thinkeridea/go-extend/exstrings"
"golang.org/x/net/html"
"net/http"
"net/url"
"regexp"
"reptile/demo/queue/dao"
"reptile/demo/queue/model"
"reptile/tools"
"strings"
"time"
)
func main() {
//1,初始化Orm
_, err := tools.OrmEngine()
if err != nil {
fmt.Println(err)
panic(err)
}
bookUrl := "https://www.xsbiquge.com/"
//bookUrl = "https://www.xsbiquge.com/68_68470/"
pool := New(100)
pool.queue <- bookUrl
//根据主页面爬取子页面
u, err := url.Parse(bookUrl)
if err != nil {
fmt.Println(err)
return
}
hostName := u.Hostname()
for uri := range pool.queue {
go DownLoad(hostName, uri, pool)
fmt.Println("range pool.queue : " + uri)
}
fmt.Println("结束啦:" + bookUrl)
}
html抓取和信息处理,匹配小说列表链接
/**
下载
*/
func DownLoad(host, bookUrl string, pool *Pool) {
showTime("download start")
pool.VisitMap.WriteMap(bookUrl)
//http 客户端
client := http.Client{}
//创建请求
req, err := http.NewRequest("GET", bookUrl, nil)
if err != nil {
fmt.Println(err)
}
//设置请求header
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36")
//执行请求
showTime("client.Do start")
resp, err := client.Do(req)
showTime("client.Do end")
if err != nil {
fmt.Println(err)
}
//这里判断下,如果响应关闭。则直接返回,实测是存在这种情况的
if resp == nil || resp.Close {
return
}
defer resp.Body.Close()
showTime("links start")
body, err := html.Parse(resp.Body)
links := visit(nil, body)
showTime("links end")
for _, link := range links {
absolute := urlJoin(link, bookUrl)
//fmt.Println(runtime.NumGoroutine())
//匹配是否是文章页面
rh, _ := regexp.Compile(host)
host := rh.MatchString(absolute)
//匹配是否是文章页面
r, _ := regexp.Compile(`\.html`)
html := r.MatchString(absolute)
if bookUrl != " " && host && !html {
len := strings.Index(absolute, "#")
if len != -1 {
absolute = exstrings.SubString(absolute, 0, len)
}
fmt.Println("current url: " + absolute)
if !pool.VisitMap.ReadMap(absolute) {
fmt.Println("add url: " + absolute)
go urlQueue(absolute, pool)
}
}
}
}
func urlQueue(url string, pool *Pool) {
pool.queue <- url
}
func urlJoin(href, base string) string {
uri, err := url.Parse(href)
if err != nil {
return " "
}
baseUrl, err := url.Parse(base)
if err != nil {
return " "
}
return baseUrl.ResolveReference(uri).String()
}
func showTime(action string) {
//fmt.Println(fmt.Sprintf("%s :%s", action, time.Now().String()))
}
在页面链接处理过程中遇到了空指针问题:所以在处理之前先判断下 *html.Node指针是否是空指针
//文档链接处理
func visit(links []string, n *html.Node) []string {
if n != nil {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
}
return links
}
学习收货:
读写锁的使用、不同goroutine之间通过channel通讯、url地址解析、html解析、html内容提去链接地址、通过map进行链接地址去重、go-extend扩展工具包使用但是有一个问题,通过range来循环通道,如果通道内没有消息了就会处于等待状态,要如果退出?