起因
無聊的時候會翻出去看看國外的漫畫,然而一頁一頁加載總是會很慢,偶爾還需要多刷新幾次才能顯示出來,非常影響體驗。于是就寫了個腳本去抓某一個漫畫下所有的圖片,這樣跑一遍腳本,就能在本地看圖片了。
為了偷懶,第一個版本用的單線程模型,幾百張圖片串行請求,真的慢。
實際工作中一直沒什么機會用到異步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了,所有寫在它后面的代碼都不執行
效果
通過這次的優化,下載一部兩三百頁漫畫的時間從之前單線程版本的二十多分鐘,變成了現在的兩分鐘左右!