起因
-
無聊的時候會翻出去看看國外的漫畫,然而一頁一頁載入總是會很慢,偶爾還需要多重新整理幾次才能顯示出來,非常影響體驗。於是就寫了個指令碼去抓某一個漫畫下所有的圖片,這樣跑一遍指令碼,就能在本地看圖片了。
-
為了偷懶,第一個版本用的單執行緒模型,幾百張圖片序列請求,真的慢。
-
實際工作中一直沒什麼機會用到非同步IO,正好拿來練練手。
分析
併發的下載圖片,有多執行緒和事件驅動兩套方案。
多執行緒的實現方式,例如一部漫畫有300張圖,我不可能開300個Thread,系統受不了。比較實際的做法是使用一個容量為N的ThreadPool,那麼,同時就只能發出N個請求,然後所有執行緒Block等待,其實效率也不高
然而事件驅動的方式就不一樣了,我可以一口氣把所有請求發出去,當有請求完成時,就呼叫事先定義的回撥Handle,實現了300張圖片的並行下載。
先上圖看看效果
從圖中就可以看出,所有的請求都發出去之後,才陸續有響應結果亂序到達。這就是典型的非同步IO的情景。
基於EventMachine的非同步圖片爬蟲
EventMachine是ruby社群知名的事件驅動庫,類似於Netty、NodeJS
通過 EM.run{}就可以開始一個事件迴圈
以下是關鍵程式碼
#img_info = [{file_name: '1.jpg', url:'xxx'}...]
def getImg(img_info)
EM.run{ #開啟事件迴圈
multi = EventMachine::MultiRequest.new #request容器
@img_info_copy = img_info.dup
img_info.each do |info|
file_name = File.join(@dir, info[:file_name])
if FileTest::exist?(file_name)
@img_info_copy.delete(info)
puts "#{file_name} skip".blue
next
end
puts "#{file_name} start".green
req = EventMachine::HttpRequest.new(info[:url]).get #建立request
multi.add "#{file_name}",req
req.callback { #成功回撥
File.open(file_name, 'w') { |file| file.write(req.response) }
@img_info_copy.delete(info)
puts "#{file_name} done".green
}
req.errback { #失敗回撥
puts "#{file_name} fail".red
}
end
multi.callback do #所有request都完成後的回撥
if @img_info_copy.size == 0 #如果沒有圖片下載失敗
EM.stop
else #遞迴呼叫,重新下載的圖片
puts "Total fails: #{@img_info_copy.size}, solving...".red
getImg @img_info_copy.dup
end
end
}
end
複製程式碼
遇到的小坑
EM.run {}之後,主執行緒就block了,所有寫在它後面的程式碼都不執行
複製程式碼
效果
通過這次的優化,下載一部兩三百頁漫畫的時間從之前單執行緒版本的二十多分鐘,變成了現在的兩分鐘左右!