言
文的文字及圖片來源于網絡,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯系我們以作處理。
作者: 王平
源自:猿人學Python
PS:如有需要Python學習資料的小伙伴可以加點擊下方鏈接自行獲取
http://note.youdao.com/noteshare?id=3054cce4add8a909e784ad934f956cef
前一兩年抓過某工商信息網站,幾三周時間大約抓了過千萬多萬張頁面。那時由于公司沒啥經費,報銷又拖得很久,不想花錢在很多機器和帶寬上,所以當時花了較多精力研究如何讓一臺爬蟲機器達到抓取極限。
本篇偏爬蟲技術細節,先周知。
Python爬蟲這兩年貌似成為了一項必備技能,無論是搞技術的,做產品的,數據分析的,金融的,初創公司做冷啟動的,都想去抓點數據回來玩玩。這里面絕大多數一共都只抓幾萬或幾十萬條數據,這個數量級其實大可不必寫爬蟲,使用 chrome 插件 web scraper 或者讓 selenium 驅動 chrome 就好了,會為你節省很多分析網頁結構或研究如何登陸的時間。
本篇只關注如何讓爬蟲的抓取性能最大化上,沒有使用scrapy等爬蟲框架,就是多線程+Python requests庫搞定。
對一個網站定向抓取幾十萬張頁面一般只用解決訪問頻率限制問題就好了。對機器內存,硬盤空間,URL去重,網絡性能,抓取間隙時間調優一般都不會在意。如果要設計一個單臺每天抓取上百萬張網頁,共有一億張頁面的網站時,訪問頻率限制問題就不是最棘手的問題了,上述每一項都要很好解決才行。硬盤存儲,內存,網絡性能等問題我們一項項來拆解。
一、優化硬盤存儲
所以千萬級網頁的抓取是需要先設計的,先來做一個計算題。共要抓取一億張頁面,一般一張網頁的大小是400KB左右,一億張網頁就是1億X200KB=36TB 。這么大的存儲需求,一般的電腦和硬盤都是沒法存儲的。所以肯定要對網頁做壓縮后存儲,可以用zlib壓縮,也可以用壓縮率更好的bz2或pylzma 。
但是這樣還不夠,我們拿天眼查的網頁來舉例。天眼查一張公司詳情頁的大小是700KB 。
對這張網頁zlib壓縮后是100KB。
一億個100KB(9TB)還是太大,要對網頁特殊處理一下,可以把網頁的頭和尾都去掉,只要body部分再壓縮。因為一張html頁面里<head></head>和<footer></footer>大都是公共的頭尾信息和js/css代碼,對你以后做正文內容抽取不會影響(也可以以后做內容抽取時把頭尾信息補回去就好)。
來看一下去掉頭尾后的html頁面大小是300KB,壓縮后是47KB。
一億張就是4T,差不多算是能接受了。京東上一個4T硬盤600多元。
二、優化內存,URL去重
再來說內存占用問題,做爬蟲程序為了防止重復抓取URL,一般要把URL都加載進內存里,放在set()里面。拿天眼查的URL舉例:
https://www.tianyancha.com/company/23402373
這個完整URL有44個字節,一億個URL就是4G,一億個URL就要占用4G內存,這還沒有算存這一億個URL需要的數據結構內存,還有待抓取URL,已抓取URL還保存在內存中的html等等消耗的內存。
所以這樣直接用set()保存URL是不建議的,除非你的內存有十幾個G。
一個取巧的辦法是截斷URL。只把URL:
https://www.tianyancha.com/company/23402373
的后綴:23402373放進set()里,23402373只占8個字節,一億個URL占700多M內存。
但是如果你是用的野云主機,用來不斷撥號用的非正規云主機,這700多M內存也是吃不消的,機器會非???。
就還需要想辦法壓縮URL的內存占用,可以使用BloomFilter算法,是一個很經典的算法,非常適用海量數據的排重過濾,占用極少的內存,查詢效率也非常的高。它的原理是把一個字符串映射到一個bit上,剛才23402373占8個字節,現在只占用1個bit(1字節=8bit),內存節省了近64倍,以前700M內存,現在只需要10多M了。
BloomFilter調用也非常簡單,當然需要先install 安裝bloom_filter:
from bloom_filter import BloomFilter # 生成一個裝1億大小的 bloombloom = BloomFilter(max_elements=100000000, error_rate=0.1) # 向bloom添加URL bloom.add('https://www.tianyancha.com/company/23402373') #判斷URL是否在bloombloom.__contains__('https://www.tianyancha.com/company/23402373')
不過奇怪,bloom里沒有公有方法來判斷URL是否重復,我用的contains()方法,也可能是我沒用對,不過判重效果是一樣的。
三、反抓取訪問頻率限制
單臺機器,單個IP大家都明白,短時間內訪問一個網站幾十次后肯定會被屏蔽的。每個網站對IP的解封策略也不一樣,有的1小時候后又能重新訪問,有的要一天,有的要幾個月去了。突破抓取頻率限制有兩種方式,一種是研究網站的反爬策略。有的網站不對列表頁做頻率控制,只對詳情頁控制。有的針對特定UA,referer,或者微信的H5頁面的頻率控制要弱很多。 另一種方式就是多IP抓取,多IP抓取又分IP代理池和adsl撥號兩種,我這里說adsl撥號的方式,IP代理池相對于adsl來說,我覺得收費太貴了。要穩定大規模抓取肯定是要用付費的,一個月也就100多塊錢。
adsl的特點是可以短時間內重新撥號切換IP,IP被禁止了重新撥號一下就可以了。這樣你就可以開足馬力瘋狂抓取了,但是一天只有24小時合86400秒,要如何一天抓過百萬網頁,讓網絡性能最大化也是需要下一些功夫的,后面我再詳說。
至于有哪些可以adsl撥號的野云主機,你在百度搜”vps adsl”,能選擇的廠商很多的。大多宣稱有百萬級IP資源可撥號,我曾測試過一段時間,把每次撥號的IP記錄下來,有真實二三十萬IP的就算不錯了。
選adsl的一個注意事項是,有的廠商撥號IP只能播出C段和D段IP,110(A段).132(B段).3(C段).2(D段),A和B段都不會變,靠C,D段IP高頻次抓取對方網站,有可能對方網站把整個C/D段IP都封掉。
C/D段加一起255X255就是6萬多個IP全都報廢,所以要選撥號IP范圍較寬的廠商。 你要問我哪家好,我也不知道,這些都是野云主機,質量和穩定性本就沒那么好。只有多試一試,試的成本也不大,買一臺玩玩一個月也就一百多元,還可以按天買。
上面我為什么說不用付費的IP代理池?
因為比adsl撥號貴很多,因為全速抓取時,一個反爬做得可以的網站10秒內就會封掉這個IP,所以10秒就要換一個IP,理想狀況下一天86400秒,要換8640個IP。
如果用付費IP代理池的話,一個代理IP收費4分錢,8640個IP一天就要345元。 adsl撥號的主機一個月才100多元。
adsl撥號Python代碼
怎么撥號廠商都會提供的,建議是用廠商提供的方式,這里只是示例:
windows下用os調用rasdial撥號:
import os # 撥號斷開 os.popen('rasdial 網絡鏈接名稱 /disconnect') # 撥號 os.popen('rasdial 網絡鏈接名稱 adsl賬號 adsl密碼')
linux下撥號:
import os # 撥號斷開 code = os.system('ifdown 網絡鏈接名稱')# 撥號 code = os.system('ifup 網絡鏈接名稱')
四、網絡性能,抓取技術細節調優
上面步驟做完了,每天能達到抓取五萬網頁的樣子,要達到百萬級規模,還需把網絡性能和抓取技術細節調優。
1.調試開多少個線程,多長時間撥號切換IP一次最優。
每個網站對短時間內訪問次數的屏蔽策略不一樣,這需要實際測試,找出抓取效率最大化的時間點。先開一個線程,一直抓取到IP被屏蔽,記錄下抓取耗時,總抓取次數,和成功抓取次數。 再開2個線程,重復上面步驟,記錄抓取耗時,總的和成功的抓取次數。再開4個線程,重復上面步驟。整理成一個表格如下,下圖是我抓天眼查時,統計抓取極限和細節調優的表格:
從上圖比較可以看出,當有6個線程時,是比較好的情況。耗時6秒,成功抓取80-110次。雖然8個線程只耗時4秒,但是成功抓取次數已經在下降了。所以線程數可以設定為開6個。
開多少個線程調試出來了,那多久撥號一次呢?
從上面的圖片看到,貌似每隔6秒撥號是一個不錯的選擇。可以這樣做,但是我選了另一個度量單位,就是每總抓取120次就重新撥號。為什么這樣選呢?從上圖也能看到,基本抓到120次左右就會被屏蔽,每隔6秒撥號其實誤差比較大,因為網絡延遲等各種問題,導致6秒內可能抓100次,也可能抓120次。
2.requests請求優化
要優化requests.get(timeout=1.5)的超時時間,不設置超時的話,有可能get()請求會一直掛起等待。而且野云主機本身性能就不穩定,長時間不回請求很正常。如果要追求抓取效率,超時時間設置短一點,設置10秒超時完全沒有意義。對于超時請求失敗的,大不了以后再二次請求,也比設置10秒的抓取效率高很多。
3.優化adsl撥號等待時間
上面步驟已算把單臺機器的抓取技術問題優化到一個高度了,還剩一個優化野云主機的問題。就是每次斷開撥號后,要等待幾秒鐘再撥號,太短時間內再撥號有可能又撥到上一個IP,還有可能撥號失敗,所以要等待6秒鐘(測試值)。所以要把撥號代碼改一下:
import os # 斷開撥號 os.popen('rasdial 網絡名稱 /disconnect') time.sleep(6) # 撥號 os.popen('rasdial 網絡名稱 adsl賬號名 adsl密碼')
而且 os.popen(‘rasdial 網絡名稱 adsl賬號名 adsl密碼’) 撥號完成后,你還不能馬上使用,那時外網還是不可用的,你需要檢測一下外網是否聯通。
我使用 ping 功能來檢測外網連通性:
import os code = os.system('ping www.baidu.com')
code為0時表示聯通,不為0時還要重新撥號。而ping也很耗時間的,一個ping命令會ping 4次,就要耗時4秒。
上面撥號等待6秒加上 ping 的4秒,消耗了10秒鐘。上面猿人學Python說了,抓120次才用6秒,每撥號一次要消耗10秒,而且是每抓120次就要重撥號,想下這個時間太可惜了,每天8萬多秒有一半時間都消耗在撥號上面了,但是也沒辦法。
當然好點的野云主機,除了上面說的IP范圍的差異,就是撥號質量差異。好的撥號等待時間更短一點,撥號出錯的概率要小一點。
通過上面我們可以輕松計算出一組抓取的耗時是6秒,撥號耗時10秒,總耗時16秒。一天86400秒,就是5400組抓取,上面說了一組抓取是120次。一天就可以抓取5400X120=64萬張網頁。
按照上述的設計就可以做到一天抓60多萬張頁面,如果你把adsl撥號耗時再優化一點,每次再節約2-3秒,就趨近于百萬抓取量級了。
另外野云主機一個月才100多,很便宜,所以你可以再開一臺adsl撥號主機,用兩臺一起抓取,一天就能抓一百多萬張網頁。幾天時間就能鏡像一個過千萬網頁的網站。
、唯一定律
無論有多少人共同參與同一項目,一定要確保每一行代碼都像是唯一個人編寫的。
二、HTML
2.1 語法
(1)用兩個空格來代替制表符(tab) -- 這是唯一能保證在所有環境下獲得一致展現的方法。
(2)嵌套元素應當縮進一次(即兩個空格)。
(3)對于屬性的定義,確保全部使用雙引號,絕不要使用單引號。
(4)不要在自閉合(self-closing)元素的尾部添加斜線 -- HTML5 規范中明確說明這是可選的。
(5)不要省略可選的結束標簽(closing tag)(例如,</li> 或 </body>)。
2.2 Example
三、HTML5 doctype
為每個 HTML 頁面的第一行添加標準模式(standard mode)的聲明,這樣能夠確保在每個瀏覽器中擁有一致的展現。
四、語言屬性
根據 HTML5 規范:
強烈建議為 html 根元素指定 lang 屬性,從而為文檔設置正確的語言。這將有助于語音合成工具確定其所應該采用的發音,有助于翻譯工具確定其翻譯時所應遵守的規則等等。
五、IE 兼容模式
IE 支持通過特定的 <meta> 標簽來確定繪制當前頁面所應該采用的 IE 版本。除非有強烈的特殊需求,否則最好是設置為 edge mode,從而通知 IE 采用其所支持的最新的模式。
六、字符編碼
通過明確聲明字符編碼,能夠確保瀏覽器快速并容易的判斷頁面內容的渲染方式。這樣做的好處是,可以避免在 HTML 中使用字符實體標記(character entity),從而全部與文檔編碼一致(一般采用 UTF-8 編碼)。
七、引入 CSS 和 JavaScript 文件
根據 HTML5 規范,在引入 CSS 和 JavaScript 文件時一般不需要指定 type 屬性,因為 text/css 和 text/javascript 分別是它們的默認值。
八、實用為王
盡量遵循 HTML 標準和語義,但是不要以犧牲實用性為代價。任何時候都要盡量使用最少的標簽并保持最小的復雜度。
九、屬性順序
9.1 從大到小
HTML 屬性應當按照以下給出的順序依次排列,確保代碼的易讀性。
(1)class
(2)id, name
(3)data-*
(4)src, for, type, href, value
(5)title, alt
(6)role, aria-*
9.2 Example
9.3 說明
class 用于標識高度可復用組件,因此應該排在首位。id 用于標識具體組件,應當謹慎使用(例如,頁面內的書簽),因此排在第二位。
十、布爾(boolean)型屬性
10.1 注意
(1)布爾型屬性可以在聲明時不賦值。XHTML 規范要求為其賦值,但是 HTML5 規范不需要。
(2)元素的布爾型屬性如果有值,就是 true,如果沒有值,就是 false。
(3)如果屬性存在,其值必須是空字符串或 [...] 屬性的規范名稱,并且不要在首尾添加空白符。
簡單來說,就是不用賦值。
10.2 Example
十一、減少標簽的數量
編寫 HTML 代碼時,盡量避免多余的父元素。很多時候,這需要迭代和重構來實現。
十二、減少 JavaScript 生成的標簽
通過 JavaScript 生成的標簽讓內容變得不易查找、編輯,并且降低性能。能避免時盡量避免。
最后,小編還給大家準備了web前端的學習資料
獲取方式:請大家轉發+關注并私信小編關鍵詞:“資料”即可獲取前端自學教程一套。
兩天有個客戶需要把網頁轉為pdf,之前也沒開發過類似的工具,就在百度搜索了一波,主要有下面三種
在百度(我一般用必應)搜索“在線網頁轉pdf”就有很多可以做這個事的網站,免費的如
各種pdf的操作都有,免費使用,速度一般。
官網地址https://tools.pdf24.org/zh
PDF24 Tools
開源免費項目,使用golang寫的,提供在線轉
官網地址http://doctron.lampnick.com/
doctron在線體驗demo
還有挺多其他的,可以自己搜索,但是都不符合我的預期。
Doctron,這是我今天要介紹的重頭戲。
Doctron是基于Docker、無狀態、簡單、快速、高質量的文檔轉換服務。目前支持將html轉為pdf、圖片(使用chrome(Chromium)瀏覽器內核,保證轉換質量)。支持PDF添加水印。
管他的,先把代碼下載下來再說
git clone https://gitcode.net/mirrors/lampnick/doctron.git
倉庫
運行
go build
./doctron --config conf/default.yaml
運行截圖
轉pdf,訪問http://127.0.0.1:8080/convert/html2pdf?u=doctron&p=lampnick&url=<url>,更換鏈接中的url為你需要轉換的url即可。
轉換效果
然后就可以寫程序去批量轉換需要的網頁了,但是我需要轉換的網頁有兩個需求
1、網站需要會員登錄,不然只能看得到一部分
2、需要把網站的頭和尾去掉的
這就為難我了,不會go語言啊,硬著頭皮搞了,肯定有個地方打開這個url的,就去代碼慢慢找,慢慢調試,功夫不負有心人,終于找到調用的地方了。
第一步:添加網站用戶登錄cookie
添加cookie之前
添加cookie之后
第二步:去掉網站頭尾
chromedp.Evaluate(`$('.header').css("display" , "none");
$('.btn-group').css("display" , "none");
$('.container .container:first').css("display" , "none");
$('.breadcrumb').css("display" , "none");
$('.footer').css("display" , "none")`, &ins.buf),
打開網頁后執行js代碼把頭尾隱藏掉
第三步:程序化,批量自動生成pdf
public static void createPDF(String folder , String cl , String pdfFile, String urlhref) {
try {
String fileName = pdfFile.replace("/", ":");
String filePath = folder + fileName;
File srcFile = new File(filePath);
File newFolder = new File("/Volumes/disk2/myproject" + File.separator + cl);
File destFile = new File(newFolder, fileName);
if(destFile.exists()){
return;
}
if(srcFile.exists()){
//移動到對應目錄
if(!newFolder.exists()){
newFolder.mkdirs();
}
FileUtils.moveFile(srcFile , destFile);
return;
}
if(!newFolder.exists()){
newFolder.mkdirs();
}
String url = "http://127.0.0.1:8888/convert/html2pdf?u=doctron&p=lampnick&url="+urlhref;
HttpEntity<String> entity = new HttpEntity<String>(null, null);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<byte[]> bytes = restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
if (bytes.getBody().length <= 100) {
if(urlList.containsKey(urlhref)){
Integer failCount = urlList.get(urlhref);
if(failCount > 3){
System.out.println("下載失?。?#34; + cl + " / " + pdfFile +" " + urlhref);
return;
}
failCount++;
urlList.put(urlhref , failCount);
}else{
urlList.put(urlhref , 1);
}
createPDF(folder , cl , pdfFile , urlhref);
}else{
if (!destFile.exists()) {
try {
destFile.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
}
try (FileOutputStream out = new FileOutputStream(destFile);) {
out.write(bytes.getBody(), 0, bytes.getBody().length);
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
最終成果:
文件夾分類存放
pdf文件
*請認真填寫需求信息,我們會在24小時內與您取得聯系。