go語(yǔ)言實(shí)現(xiàn)并發(fā)網(wǎng)絡(luò)爬蟲(chóng)的示例代碼
go語(yǔ)言做爬蟲(chóng)也是很少嘗試,首先我的思路是看一下爬蟲(chóng)的串行實(shí)現(xiàn),然后通過(guò)兩個(gè)并發(fā)實(shí)現(xiàn):一個(gè)使用鎖,另一個(gè)使用通道
這里不涉及從頁(yè)面中提取URL的邏輯(請(qǐng)查看Go框架colly的內(nèi)容)。網(wǎng)絡(luò)抓取只是作為一個(gè)例子來(lái)考察Go的并發(fā)性。
我們想從我們的起始頁(yè)中提取所有的URL,將這些URL保存到一個(gè)列表中,然后對(duì)列表中的每個(gè)URL做同樣的處理。頁(yè)面的圖很可能是循環(huán)的,所以我們需要記住哪些頁(yè)面已經(jīng)經(jīng)歷了這個(gè)過(guò)程(或者在使用并發(fā)時(shí),處于這個(gè)過(guò)程的中間)。

串行爬蟲(chóng)首先檢查我們是否已經(jīng)在獲取地圖中獲取了該頁(yè)面。如果我們沒(méi)有,那么它就在頁(yè)面上找到的每個(gè)URL上調(diào)用自己。注意:map 在Go中是引用類(lèi)型,所以每次調(diào)用都會(huì)得到相同的 map。
func Serial(url string, fetcher Fetcher, fetched map[string]bool) {
if fetched[url] {
return
}
fetched[url] = true
urls, err := fetcher.Fetch(url)
if err != nil {
return
}
for _, u := range urls {
Serial(u, fetcher, fetched)
}
return
}
func main() {
Serial(<page>, fetcher, make(map[string]bool))
}
fetcher將包含提取URLs到列表中的邏輯(也可以對(duì)頁(yè)面的內(nèi)容做一些處理)。這個(gè)實(shí)現(xiàn)不是本講的重點(diǎn)。
由于網(wǎng)絡(luò)速度很慢,我們可以使用并發(fā)性來(lái)加快這個(gè)速度。為了實(shí)現(xiàn)這一點(diǎn),我們需要使用鎖(在讀/寫(xiě)時(shí)鎖定已經(jīng)獲取的頁(yè)面地圖)和 waitgroup(等待所有的goroutine完成)。
已經(jīng)獲取的頁(yè)面的 map 只能由持有鎖的線程訪問(wèn),因?yàn)槲覀儾幌M鄠€(gè)線程開(kāi)始處理同一個(gè)URL。如果在一個(gè)線程的讀和寫(xiě)之間,另一個(gè)線程在第一個(gè)線程更新之前從 map 上得到了相同的讀數(shù),這就可能發(fā)生。
我們定義了fetchState結(jié)構(gòu),將 map 和鎖組合在一起,并定義了一個(gè)方法來(lái)初始化它。
爬蟲(chóng)程序的開(kāi)始是一樣的,檢查我們是否已經(jīng)獲取了URL,但這次使用sync.Mutex來(lái)鎖定 map,如前所述。然后,對(duì)于頁(yè)面上發(fā)現(xiàn)的每個(gè)URL,我們?cè)谝粋€(gè)新的goroutine中啟動(dòng)相同的函數(shù)。在啟動(dòng)之前,我們將WaitGroup的計(jì)數(shù)器增加1,done.Wait()在退出之前等待所有的抓取工作完成。
func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {
f.mu.Lock()
already := f.fetched[url]
f.fetched[url] = true
f.mu.Unlock()
if already {
return
}
urls, err := fetcher.Fetch(url)
if err != nil {
return
}
var done sync.WaitGroup
for _, u := range urls {
done.Add(1)
go func(u string) {
defer done.Done()
ConcurrentMutex(u, fetcher, f)
}(u)
}
done.Wait()
return
}
type fetchState struct {
mu sync.Mutex
fetched map[string]bool
}
func makeState() *fetchState {
f := &fetchState{}
f.fetched = make(map[string]bool)
return f
}
func main() {
ConcurrentMutex(<page>, fetcher, makeState())
}
注意:
[1] done.Done()的調(diào)用被推遲了,以防我們?cè)谄渲幸粋€(gè)調(diào)用中出現(xiàn)錯(cuò)誤,在這種情況下,我們?nèi)匀灰f減WaitGroup的計(jì)數(shù)器。
[2] 這段代碼的一個(gè)問(wèn)題是,我們沒(méi)有限制線程的數(shù)量。但值得一提的是,goroutines比其他語(yǔ)言的線程更輕量級(jí),并且由Go運(yùn)行時(shí)管理,系統(tǒng)調(diào)用更少。
[3] 我們把字符串u傳給立即函數(shù),以便制作一個(gè)URL的副本,然后才把它送到goroutine,因?yàn)樽兞縰在外層for循環(huán)中發(fā)生了變化。要理解這樣做的必要性,一個(gè)更簡(jiǎn)單的例子是,在沒(méi)有WaitGroup的情況下。
func checkThisOut() {
s := "abc"
sec := time.Second
go func() {time.Sleep(sec); fmt.Printf("s = %v\n", s)}()
go func(u string) {time.Sleep(sec); fmt.Printf("u = %v\n", u)}(s)
s = "def"
time.Sleep(2 * sec)
}
// this prints out: u = abc, s = def
[4] 我們可以運(yùn)行內(nèi)置的數(shù)據(jù)競(jìng)賽檢測(cè)器,通過(guò)運(yùn)行g(shù)o run -race .來(lái)幫助檢測(cè)競(jìng)賽條件。它在這個(gè)例子中非常有效。
下一個(gè)并發(fā)版本在線程之間完全不共享內(nèi)存!嗯,這并不準(zhǔn)確。我們只是不會(huì)自己同步訪問(wèn)共享數(shù)據(jù)。相反,我們使用一個(gè)通道在goroutine之間進(jìn)行通信。
在這個(gè)最后的版本中,我們有一個(gè)主函數(shù)在主線程上運(yùn)行。只有這個(gè)函數(shù)能看到 map 并從通道中讀取。channel ,像 map 一樣,也是引用類(lèi)型。所以這里只有一個(gè)通道。
在啟動(dòng)時(shí),我們將第一個(gè)URL寫(xiě)到通道上。這是在一個(gè)goroutine中完成的,因?yàn)橄蛞粋€(gè)沒(méi)有緩沖的通道的寫(xiě)入會(huì)導(dǎo)致goroutine暫停,直到該值被另一個(gè)goroutine讀取。
我們?cè)谝粋€(gè)for循環(huán)中從通道中讀取URL的列表(從一個(gè)沒(méi)有緩沖的通道中讀取也會(huì)阻塞)。然后,我們以與之前的實(shí)現(xiàn)類(lèi)似的方式瀏覽該列表。通過(guò)使用一個(gè)計(jì)數(shù)器,一旦沒(méi)有更多的工作者,這個(gè)循環(huán)就會(huì)中斷。
工作者獲取URL的列表,將它們傳遞給通道。如果出現(xiàn)錯(cuò)誤,會(huì)傳遞一個(gè)空列表,這樣從通道讀取的for循環(huán)最終會(huì)退出(計(jì)數(shù)器的設(shè)置方式是,我們等待從每個(gè)goroutine讀取一個(gè)值)。
func ConcurrentChannel(url string, fetcher Fetcher) {
ch := make(chan []string)
go func() {
ch <- []string{url}
}()
master(ch, fetcher)
}
func master(ch chan []string, fetcher Fetcher) {
n := 1
fetched := make(map[string]bool)
for urls := range ch {
for _, u := range urls {
if fetched[u] == false {
fetched[u] = true
n += 1
go worker(u, ch, fetcher)
}
}
n -= 1
if n == 0 {
break
}
}
}
func worker(url string, ch chan []string, fetcher Fetcher) {
urls, err := fetcher.Fetch(url)
if err != nil {
ch <- []string{}
} else {
ch <- urls
}
}
到此這篇關(guān)于go語(yǔ)言實(shí)現(xiàn)并發(fā)網(wǎng)絡(luò)爬蟲(chóng)的示例代碼的文章就介紹到這了,更多相關(guān)go語(yǔ)言并發(fā)網(wǎng)絡(luò)爬蟲(chóng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GoLang職責(zé)鏈模式代碼實(shí)現(xiàn)介紹
這篇文章主要介紹了GoLang職責(zé)鏈模式代碼實(shí)現(xiàn),職責(zé)鏈模式是一種常用的設(shè)計(jì)模式,可以提高代碼的靈活性與可維護(hù)性,職責(zé)鏈模式將請(qǐng)求和處理分離,可以讓請(qǐng)求在處理鏈中依次經(jīng)過(guò)多個(gè)處理者,直到找到能夠處理請(qǐng)求的處理者為止2023-05-05
GoFrame框架gredis優(yōu)雅的取值和類(lèi)型轉(zhuǎn)換
Golang開(kāi)發(fā)命令行之flag包的使用方法

