不久,我在 LearnML 子論壇上看到一篇帖子。樓主在這篇帖子中提到,他需要為自己的機器學習項目抓取網頁數據。很多人在回帖中給出了自己的方法,主要是學習如何使用 BeautifulSoup 和 Selenium。
我曾在一些 數據科學項目中使用過 BeautifulSoup 和 Selenium。在本文中,我將告訴你如何用一些有用的數據抓取一個網頁,并將其轉換成 pandas 數據結構(DataFrame)。
為什么要將其轉換成數據結構呢?這是因為大部分機器學習庫都能處理 pandas 數據結構,并且只需少量修改就可對你的模型進行編輯。
首先,我們要在維基百科上找到一個表來轉換成數據結構。我抓取的這張表,展示的是維基百科上瀏覽量最大的運動員數據。
其中一項大量的工作就是,通過瀏覽 HTML 樹來得到我們需要的表。
通過 request 和 regex 庫,我們開始使用 BeautifulSoup。
from bs4 import BeautifulSoup
import requests
import re
import pandas as pd
復制代碼
下面,我們將從網頁中提取 HTML 代碼:
website_url = requests.get('https://en.wikipedia.org/wiki/Wikipedia:Multiyear_ranking_of_most_viewed_pages').text
soup = BeautifulSoup(website_url, 'lxml')
print(soup.prettify())
</a>
</li>
<li id="footer-places-disclaimer">
<a href="/wiki/Wikipedia:General_disclaimer" title="Wikipedia:General disclaimer">
Disclaimers
</a>
</li>
<li id="footer-places-contact">
<a href="//en.wikipedia.org/wiki/Wikipedia:Contact_us">
Contact Wikipedia
</a>
</li>
<li id="footer-places-mobileview">
<a class="noprint stopMobileRedirectTog
復制代碼
從語料庫中收集所有的表,我們有一個較小的表面區域來搜索。
wiki_tables = soup.find_all('table', class_='wikitable')
wiki_tables
復制代碼
因為存在很多表,所以需要一種過濾它們的方法。
據我們所知,Cristiano Ronaldo(也就是葡萄牙足球運動員 C 羅)有一個錨標記,這可能在幾個表中是獨一無二的。
通過 Cristiano Ronaldo 文本,我們可以過濾那些被錨標記的表。此外,我們還發現一些包含這個錨標記的父元素。
links = []
for table in wiki_tables:
_table = table.find('a', string=re.compile('Cristiano Ronaldo'))
if not _table:
continue
print(_table)
_parent = _table.parent
print(_parent)
links.append(_parent)
<a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
<td style="text-align: left;"><a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
</td>
<a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
<td style="text-align: left;"><a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
</td>
<a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
<td style="text-align: left;"><a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
</td>
復制代碼
父元素只顯示單元格。
這是一個帶有瀏覽器 web 開發工具的單元格。
parent_lst = []
for anchor in links:
_ = anchor.find_parents('tbody')
print(_)
parent_lst.append(_)
復制代碼
利用 tbody,我們可以返回包含以前的錨標記的其他表。
為進一步過濾,我們可以在以下表中的不同標題進行搜索:
for i in parent_lst:
print(i[0].find('tr'))
tr>
<th>Rank*</th>
<th>Page</th>
<th>Views in millions
</th></tr>
<tr>
<th>Rank</th>
<th>Page</th>
<th>Views in millions
</th></tr>
<tr>
<th>Rank</th>
<th>Page</th>
<th>Sport</th>
<th>Views in millions
</th></tr>
復制代碼
第三張看起來很像我們所需要的表。
接下來,我們開始創建必要的邏輯來提取并清理我們需要的細節。
sports_table = parent_lst[2]
complete_row = []
for i in sports_table:
rows = i.find_all('tr')
print('\n--------row--------\n')
print(rows)
for row in rows:
cells = row.find_all('td')
print('\n-------cells--------\n')
print(cells)
if not cells:
continue
rank = cells[0].text.strip('\n')
page_name = cells[1].find('a').text
sport = cells[2].find('a').text
views = cells[3].text.strip('\n')
print('\n-------CLEAN--------\n')
print(rank)
print(page_name)
print(sport)
print(views)
complete_row.append([rank, page_name, sport, views])
for i in complete_row:
print(i)
復制代碼
分解一下:
sports_table = parent_lst[2]
complete_row = []
復制代碼
下面我們從上面的列表中選擇第三個元素。這就是我們需要的表。
接下來創建一個空列表,用于存儲每行的詳細信息。在遍歷這個表的時候,建立一個循環,遍歷表中的每一行,并將其保存到 rows 變量中。
for i in sports_table:
rows = i.find_all('tr')
print('\n--------row--------\n')
print(rows)
復制代碼
for row in rows:
cells = row.find_all('td')
print('\n-------cells--------\n')
print(cells)
復制代碼
建立了嵌套的循環。遍歷上一個循環中保存的每個行。在遍歷這些單元格時,我們將每個單元格保存在一個新的變量。
if not cells:
continue
復制代碼
這段簡短的代碼允許我們在從單元格中提取文本時,避免空單元格并防止發生錯誤。
rank = cells[0].text.strip('\n')
page_name = cells[1].find('a').text
sport = cells[2].find('a').text
views = cells[3].text.strip('\n')
復制代碼
在此,我們將各種單元格清理為純文本格式。清除后的值保存在其列名下的變量中。
print('\n-------CLEAN--------\n')
print(rank)
print(page_name)
print(sport)
print(views)
complete_row.append([rank, page_name, sport, views])
復制代碼
此處,我們向行列表添加這些值。然后輸出清理后的值。
-------cells--------
[<td>13
</td>, <td style="text-align: left;"><a href="/wiki/Conor_McGregor" title="Conor McGregor">Conor McGregor</a>
</td>, <td><a href="/wiki/Mixed_martial_arts" title="Mixed martial arts">Mixed martial arts</a>
</td>, <td>43
</td>]
-------CLEAN--------
13
Conor McGregor
Mixed martial arts
43
復制代碼
下面將其轉換為數據結構:
headers = ['Rank', 'Name', 'Sport', 'Views Mil']
df = pd.DataFrame(complete_row, columns=headers)
df
復制代碼
現在你可以在機器學習項目中使用的 pandas 數據結構了。你可以使用自己喜歡的庫來擬合模型數據。
作者介紹:
Tobi Olabode,對技術感興趣,目前主要關注機器學習。
原文鏈接:
https://blog.devgenius.io/how-to-scrape-a-website-for-your-ml-project-c3a4d6f160c7
常聽別人說通過Python可以輕松實現一個小爬蟲,甚至會有部分同學一聽到爬蟲就認為是Python,我竟無言以對。為了先從自己開始打破這種思想禁錮,花了一天時間查找資料了解網絡方面知識,用C程序接收數據。程序框架借鑒于他人。做出來的只是Demo,歡迎批評指正。
言
文的文字及圖片來源于網絡,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯系我們以作處理。
作者: 王平
源自:猿人學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撥號主機,用兩臺一起抓取,一天就能抓一百多萬張網頁。幾天時間就能鏡像一個過千萬網頁的網站。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。