如何優雅的落地一個分布式爬蟲:實戰篇
本文將會以PC端微博進行講解,因為移動端微博數據不如PC短全面,而且抓取和解析難度都會小一些。文章比較長,由於篇幅所限,文章並沒有列出所有代碼,只是講了大致流程和思路。
要抓微博數據,第一步便是模擬登陸,因為很多信息(比如用戶信息,用戶主頁微博數據翻頁等各種翻頁)都需要在登錄狀態下才能查看
這裏我簡單說一下,做爬蟲的同學不要老想著用什麽機器學習的方法去識別復雜驗證碼,真的難度非常大,這應該也不是一個爬蟲工程師的工作重點,當然這只是我的個人建議。工程化的項目,我還是建議大家通過打碼平臺來解決驗證碼的問題。
說完模擬登陸(具體請參見我寫的那兩篇文章,篇幅所限,我就不copy過來了),我們現在正式進入微博的數據抓取。這裏我會以微博用戶信息抓取為例來進行分析和講解。
關於用戶信息抓取,可能我們有兩個目的。一個是我們只想抓一些指定用戶,另外一個是我們想盡可能多的抓取更多數量的用戶的信息。我的目的假定是第二種。那麽我們該以什麽樣的策略來抓取,才能獲得盡可能多的用戶信息呢?如果我們初始用戶選擇有誤,選了一些不活躍的用戶,很可能會形成一個環,這樣就抓不了太多的數據。這裏有一個很簡單的思路:我們把一些大V拿來做為種子用戶,我們先抓他們的個人信息,然後再抓大V所關註的用戶和粉絲,大V關註的用戶肯定也是類似大V的用戶,這樣的話,就不容易形成環了。
策略我們都清楚了。就該是分析和編碼了。
我們先來分析如何構造用戶信息的URL。這裏我以微博名為一起神吐槽的博主為例進行分析。做爬蟲的話,一個很重要的意識就是爬蟲能抓的數據都是人能看到的數據,反過來,人能在瀏覽器上看到的數據,爬蟲幾乎都能抓。這裏用的是幾乎,因為有的數據抓取難度特別。我們首先需要以正常人的流程看看怎麽獲取到用戶的信息。我們先進入該博主的主頁,如下圖
種子用戶主頁
點擊查看更多,可以查看到該博主的具體信息
種子博主具體信息
這裏我們就看到了他的具體信息了。然後,我們看該頁面的url構造
weibo.com/p/100505175…
我直接copy的地址欄的url。這樣做有啥不好的呢?對於老鳥來說,一下就看出來了,這樣做的話,可能會導致信息不全,因為可能有些信息是動態加載的。所以,我們需要通過抓包來判斷到底微博會通過該url返回所有信息,還是需要請求一些ajax 鏈接才會返回一些關鍵信息。這裏我就重復一下我的觀點:抓包很重要,抓包很重要,抓包很重要!重要的事情說三遍。
我們抓完包,發現並沒有ajax請求。那麽可以肯定請求前面的url,會返回所有信息。我們通過點擊鼠標右鍵,查看網頁源代碼,然後ctrl+a、ctrl+c將所有的頁面源碼保存到本地,這裏我命名為personinfo.html。我們用瀏覽器打開該文件,發現我們需要的所有信息都在這段源碼中,這個工作和抓包判斷數據是否全面有些重復,但是在我看來是必不可少的,因為我們解析頁面數據的時候還可以用到這個html文件,如果我們每次都通過網絡請求去解析內容的話,那麽可能賬號沒一會兒就會被封了(因為頻繁訪問微博信息),所以我們需要把要解析的文件保存到本地。
從上面分析中我們可以得知
weibo.com/p/100505175…
這個url就是獲取用戶數據的url。那麽我們在只知道用戶id的時候怎麽構造它呢?我們可以多拿幾個用戶id來做測試,看構造是否有規律,比如我這裏以用戶名為網易雲音樂的用戶做分析,發現它的用戶信息頁面構造如下
weibo.com/1721030997/…
這個就和上面那個不同了。但是我們仔細觀察,可以發現上面那個是個人用戶,下面是企業微博用戶。我們嘗試一下把它們url格式都統一為第一種或者第二種的格式
weibo.com/1751195602/…
這樣會出現404,那麽統一成上面那種呢?
weibo.com/p/100505172…
這樣子的話,它會被重定向到用戶主頁,而不是用戶詳細資料頁。所以也就不對了。那麽該以什麽依據判斷何時用第一種url格式,何時用第二種url格式呢?我們多翻幾個用戶,會發現除了100505之外,還有100305、100206等前綴,那麽我猜想這個應該可以區分不同用戶。這個前綴在哪裏可以得到呢?我們打開我們剛保存的頁面源碼,搜索100505,可以發現
domain
微博應該是根據這個來區分不同用戶類型的。這裏大家可以自己也可以試試,看不同用戶的domain是否不同。為了數據能全面,我也是做了大量測試,發現個人用戶的domain是1005051,作家是100305,其他基本都是認證的企業號。前兩個個人信息的url構造就是
weibo.com/p/domain+ui…
後者的是
weibo.com/uid/about
弄清楚了個人信息url的構造方式,但是還有一個問題。我們已知只有uid啊,沒有domain啊。如果是企業號,我們通過domain=100505會被重定向到主頁,如果是作家等(domain=100305或者100306),也會被重定向主頁。我們在主頁把domain提取出來,再請求一次,不就能拿到用戶詳細信息了嗎?
關於如何構造獲取用戶信息的url的相關分析就到這裏了。因為我們是在登錄的情況下進行數據抓取的,可能在抓取的時候,某個賬號突然就被封了,或者由於網絡原因,某次請求失敗了,該如何處理?對於前者,我們需要判斷每次請求返回的內容是否符合預期,也就是看response url是否正常,看response content是否是404或者讓你驗證手機號等,對於後者,我們可以做一個簡單的重試策略,大概代碼如下
@timeout_decorator
def get_page(url, user_verify=True, need_login=True):
"""
:param url: 待抓取url
:param user_verify: 是否為可能出現驗證碼的頁面(ajax連接不會出現驗證碼,如果是請求微博或者用戶信息可能出現驗證碼),否為抓取轉發的ajax連接
:param need_login: 抓取頁面是否需要登錄,這樣做可以減小一些賬號的壓力
:return: 返回請求的數據,如果出現404或者403,或者是別的異常,都返回空字符串
"""
crawler.info(‘本次抓取的url為{url}‘.format(url=url))
count = 0
while count < max_retries:
if need_login:
# 每次重試的時候都換cookies,並且和上次不同,如果只有一個賬號,那麽就允許相同
name_cookies = Cookies.fetch_cookies()
if name_cookies is None:
crawler.warning(‘cookie池中不存在cookie,正在檢查是否有可用賬號‘)
rs = get_login_info()
# 選擇狀態正常的賬號進行登錄,賬號都不可用就停掉celery worker
if len(rs) == 0:
crawler.error(‘賬號均不可用,請檢查賬號健康狀況‘)
# 殺死所有關於celery的進程
if ‘win32‘ in sys.platform:
os.popen(‘taskkill /F /IM "celery*"‘)
else:
os.popen(‘pkill -f "celery"‘)
else:
crawler.info(‘重新獲取cookie中...‘)
login.excute_login_task()
time.sleep(10)
try:
if need_login:
resp = requests.get(url, headers=headers, cookies=name_cookies[1], timeout=time_out, verify=False)
if "$CONFIG[‘islogin‘] = ‘0‘" in resp.text:
crawler.warning(‘賬號{}出現異常‘.format(name_cookies[0]))
freeze_account(name_cookies[0], 0)
Cookies.delete_cookies(name_cookies[0])
continue
else:
resp = requests.get(url, headers=headers, timeout=time_out, verify=False)
page = resp.text
if page:
page = page.encode(‘utf-8‘, ‘ignore‘).decode(‘utf-8‘)
else:
continue
# 每次抓取過後程序sleep的時間,降低封號危險
time.sleep(interal)
if user_verify:
if ‘unfreeze‘ in resp.url or ‘accessdeny‘ in resp.url or ‘userblock‘ in resp.url or is_403(page):
crawler.warning(‘賬號{}已經被凍結‘.format(name_cookies[0]))
freeze_account(name_cookies[0], 0)
Cookies.delete_cookies(name_cookies[0])
count += 1
continue
if ‘verifybmobile‘ in resp.url:
crawler.warning(‘賬號{}功能被鎖定,需要手機解鎖‘.format(name_cookies[0]))
freeze_account(name_cookies[0], -1)
Cookies.delete_cookies(name_cookies[0])
continue
if not is_complete(page):
count += 1
continue
if is_404(page):
crawler.warning(‘url為{url}的連接不存在‘.format(url=url))
return ‘‘
except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError, AttributeError) as e:
crawler.warning(‘抓取{}出現異常,具體信息是{}‘.format(url, e))
count += 1
time.sleep(excp_interal)
else:
Urls.store_crawl_url(url, 1)
return page
crawler.warning(‘抓取{}已達到最大重試次數,請在redis的失敗隊列中查看該url並檢查原因‘.format(url))
Urls.store_crawl_url(url, 0)
return ‘‘
這裏大家把上述代碼當一段偽代碼讀就行了,主要看看如何處理抓取時候的異常。因為如果貼整個用戶抓取的代碼,不是很現實,代碼量有點大。
下面講頁面解析的分析。有一些做PC端微博信息抓取的同學,可能曾經遇到過這麽個問題:保存到本地的html文件打開都能看到所有信息啊,為啥在頁面源碼中找不到呢?因為PC端微博頁面的關鍵信息都是像下圖這樣,被FM.view()包裹起來的,裏面的數據可能被json encode過。