爬蟲
介紹
今日在追一篇網(wǎng)絡(luò)小說倘潜,由于個(gè)人比較窮译打,所以看不起正規(guī)網(wǎng)站的小說屑迂,無奈之下只能看一些免費(fèi)的網(wǎng)站,我估計(jì)免費(fèi)網(wǎng)站的小說也是爬取來的內(nèi)容娘摔。但是想必大家都清楚,哪些免費(fèi)的網(wǎng)站是靠什么盈利的唤反。那就是廣告啊凳寺,鋪天蓋地的的廣告,防不勝防彤侍。
那怎么辦呢肠缨,學(xué)了這么久的golang,試試自己爬一下吧盏阶。從哪里爬呢晒奕,就從免費(fèi)的網(wǎng)站上爬吧。
準(zhǔn)備
在實(shí)現(xiàn)爬蟲之前,先介紹兩個(gè)go的庫
goquery
做過 Web 開發(fā)的脑慧,應(yīng)該都用過或聽過 jQuery魄眉,它提供了方便的操作 DOM 的 API。使用 Go 語言做服務(wù)器端開發(fā)闷袒,有時(shí)候需要解析 HTML 文件坑律,比如抓取網(wǎng)站內(nèi)容、寫一個(gè)爬蟲等囊骤。這時(shí)候如果有一個(gè)類似 jQuery 的庫可以使用晃择,操作 DOM 會(huì)很方便,而且也物,上手也會(huì)很快宫屠。github.com/PuerkitoBio/goquery這個(gè)庫就實(shí)現(xiàn)了類似 jQuery 的功能,讓你能方便的使用 Go 語言操作 HTML 文檔焦除。
另外有一篇很不錯(cuò)的goquery介紹的文章:GO 語言版 JQUERY — GOQUERY激况。具體使用方法可以參考這篇文章
mahonia
這個(gè)包是golang中的字符集轉(zhuǎn)換用的,因?yàn)楹芏鄷r(shí)候膘魄,我們爬取的內(nèi)容的編碼格式可能是各種各樣的乌逐,這里我們需要做一個(gè)統(tǒng)一的處理,轉(zhuǎn)成utf-8的格式创葡。
然后使用這個(gè)工具包浙踢,再進(jìn)行一些簡單的封裝,形成以下的方法:
package commonimport("github.com/axgle/mahonia")funcConvertToString(srcstring, srcCodestring, tagCodestring)string{? ? srcCoder := mahonia.NewDecoder(srcCode)? ? srcResult := srcCoder.ConvertString(src)? ? tagCoder := mahonia.NewDecoder(tagCode)? ? _, cdata, _ := tagCoder.Translate([]byte(srcResult),true)? ? result :=string(cdata)returnresult}func GbkToUtf8(srcstring)string{returnConvertToString(src,"gbk","utf-8")}
因?yàn)楹芏嗲闆r我們遇到的都是gbk格式的編碼灿渴,所以這里寫了個(gè)gbk轉(zhuǎn)utf-8的方法
實(shí)現(xiàn)爬蟲
前面的2個(gè)工具已經(jīng)介紹完了洛波,接下來,就是把他們拼裝組合一下骚露,就是我自己的爬蟲了蹬挤。是不是很簡單呢?下面就做一些簡單的實(shí)現(xiàn)吧棘幸。
數(shù)據(jù)庫設(shè)計(jì)
因?yàn)楸敬螌?shí)現(xiàn)的比較簡單焰扳,所以數(shù)據(jù)庫的表也不復(fù)雜,表結(jié)構(gòu)如下:
數(shù)據(jù)庫表結(jié)構(gòu)
其中误续,book表中的name字段是由唯一約束的吨悍,是為了保證書名唯一,這也是對于書名的簡單去重處理蹋嵌。
另外育瓜,url表記錄的是爬取的url地址信息,status的取值為:-1 -> 失效url, 0 -> 已爬取url, 1 -> 未爬取url栽烂。url字段具有唯一約束躏仇,避免重復(fù)恋脚。
爬蟲設(shè)計(jì)
前面已經(jīng)設(shè)計(jì)好了數(shù)據(jù)庫,下面就是爬取數(shù)據(jù)钙态,然后對數(shù)據(jù)庫進(jìn)行增刪改查的操作了慧起。
配置信息
首先,我們需要一些信息進(jìn)行配置册倒,包括日志蚓挤,還有數(shù)據(jù)庫:
[log]level = 1adapter = file[db]host = 127.0.0.1port = 3306user = rootpassword = 654321name = chain_booktimezone = Asia/Shanghaiprefix = bo_
當(dāng)然這里的配置模塊,以及日志模塊是我之前寫的一些簡單的庫里面的功能驻子,可參考:igo
數(shù)據(jù)庫操作
數(shù)據(jù)庫的orm我使用的是beego的orm模塊灿意,不熟悉的可以看下beego 官方文檔。
這里崇呵,我僅將init函數(shù)貼出來缤剧,具體model的操作方法,可看源碼:
packagemodelsimport("net/url""github.com/Chain-Zhang/igo/conf"_"github.com/go-sql-driver/mysql""github.com/astaxie/beego/orm")funcinit(){? ? dbhost := conf.AppConfig.GetString("db.host")? ? dbport := conf.AppConfig.GetString("db.port")? ? dbuser := conf.AppConfig.GetString("db.user")? ? dbpwd := conf.AppConfig.GetString("db.password")? ? dbname := conf.AppConfig.GetString("db.name")? ? timezone := conf.AppConfig.GetString("db.timezone")ifdbport ==""{? ? ? ? dbport ="3306"}? ? dsn := dbuser +":"+ dbpwd +"@tcp("+ dbhost +":"+ dbport +")/"+ dbname +"?charset=utf8"iftimezone !=""{? ? ? ? dsn = dsn +"&loc="+ url.QueryEscape(timezone)? ? }? ? orm.RegisterDataBase("default","mysql",dsn)? ? orm.RegisterModel(new(Book),new(Chapter),new(Url))}
爬取數(shù)據(jù)
到此域慷,萬事具備只欠東風(fēng)了荒辕。接下來開始爬取數(shù)據(jù)了。
因?yàn)榭赡軙?huì)有不同的源數(shù)據(jù)犹褒,那么抵窒,html文檔的結(jié)構(gòu)肯定也就不一樣了。所以叠骑,這邊就先實(shí)現(xiàn)一個(gè)爬蟲的接口李皇。以及信息的結(jié)構(gòu)體和實(shí)例化方法:
package spiderimport("errors")type SBook struct{? ? NamestringImagestringUrlstringChapters []*SChapter}type SChapterstruct{TitlestringUrlstringOrderintPreintNextintContentstring}type Spider interface{? ? SpiderUrl(urlstring) error}funcNewSpider(fromstring)(Spider, error){switchfrom{case"booktxt":returnnew(BookTextSpider), nildefault:returnnil, errors.New("系統(tǒng)暫未處理該類型的配置文件")? ? }}
代碼中的booktxt的源是www.boyfuli.com,而且宙枷,這個(gè)Spider函數(shù)是針對一本書的爬鹊舴俊(因?yàn)槲抑皇桥牢易约合肟吹男≌f,所有不對全站爬取慰丛,不貪心)卓囚。
然后就是針對booktxt的爬取操作了。
package spiderimport ("time""strings""ispider/common""ispider/models""github.com/PuerkitoBio/goquery""github.com/Chain-Zhang/igo/ilog")type BookTextSpiderstruct{? ? }func (self*BookTextSpider)SpiderUrl(url string)( error){? ? book := SBook{}? ? book.Url = url? ? doc, err := goquery.NewDocument(url)iferr !=nil{returnerr? ? }? ? bookname := common.GbkToUtf8(doc.Find("#info h1").Text())? ? ? ? b, err := models.GetBookByName(bookname)iferr !=nil{? ? ? ? b := models.Book{Name:bookname, CreatedAt:time.Now(), UpdatedAt:time.Now()}? ? ? ? models.BookAdd(&b)? ? }? ? doc.Find("#list dd").Each(func (iint, contentSelection *goquery.Selection){ifi <9{return}? ? ? ? pre := i -9next := i-7title := common.GbkToUtf8(contentSelection.Find("a").Text())? ? ? ? href, _ := contentSelection.Find("a").Attr("href")? ? ? ? chapter := SChapter{Title:title,Url:"http://www.boyfuli.com"+href, Order:i -8, Pre:pre, Next:next}? ? ? ? book.Chapters = append(book.Chapters, &chapter)? ? ? ? u := models.Url{Url:chapter.Url}? ? ? ? models.UrlAdd(&u)? ? })? ? ? ? channel := make(chanstruct{},100)for_, chapter := range book.Chapters{? ? ? ? channel <-struct{}{}? ? ? ? go SpiderChapter(b.Id, chapter, channel)? ? }fori :=0; i <100; i++{? ? ? ? channel <-struct{}{}? ? }? ? close(channel)returnnil}type ChanTagstruct{}func SpiderChapter(bookidint, chapter *SChapter, c chanstruct{}){? ? defer func(){<- c}()ifmodels.IsValidUrl(chapter.Url){? ? ? ? doc, err := goquery.NewDocument(chapter.Url)iferr !=nil{? ? ? ? ? ? ilog.AppLog.Error("get chapter details error: ", err.Error())return}? ? ? ? content := doc.Find("#content").Text()? ? ? ? content = common.GbkToUtf8(content)? ? ? ? content = strings.Replace(content,"聽"," ",-1)? ? ? ? ch := models.Chapter{BookId:bookid, Title:chapter.Title, Content:content,Sort:chapter.Order, Pre:chapter.Pre, Next:chapter.Next, CreatedAt:time.Now(),UpdatedAt:time.Now()}? ? ? ? models.ChapterAdd(&ch)? ? ? ? models.SpideredUrl(chapter.Url)? ? }}
調(diào)用
package mainimport("ispider/spider""fmt""github.com/Chain-Zhang/igo/ilog")funcmain(){? ? ilog.AppLog.Info("start")? ? s, err := spider.NewSpider("booktxt")iferr != nil{? ? ? ? ilog.AppLog.Fatal("new Spider error: ", err.Error())? ? }? ? err = s.SpiderUrl("http://www.boyfuli.com/2_2219/")iferr != nil{? ? ? ? ilog.AppLog.Fatal("new Document error: ", err.Error())? ? }? ? var strstringfmt.Scan(&str)}
這樣诅病,我們的小說就進(jìn)到數(shù)據(jù)庫里啦哪亿,只要寫個(gè)前端看就行了,再也不會(huì)有廣告了睬隶。哈哈哈。