1000家公司五年的新浪微博採集
本文原始github地址為:動態IP解決新浪的反爬蟲機制,快速抓取內容。
作者:@szcf-weiya
目錄:
- ## 項目背景
- ## 目標網頁
- ## 解析網頁
- ## 數據存儲
- ## 動態IP
- ## 運行環境
- ## 結果
- [TOC]
## 項目背景
以公司名稱為關鍵詞在[新浪微博](微博搜索 - 微博)中搜索2011-2015五年的所有微博,並統計評論數、轉發數、點贊數,需要搜索的公司有1000個左右。
在ADSL撥號上網的計算機上運行Python爬蟲程序,並將數據存儲到sqlite3資料庫中。項目主頁為[動態IP解決新浪的反爬蟲機制,快速抓取內容。](動態IP解決新浪的反爬蟲機制,快速抓取內容。),其中有完整的爬蟲代碼。
同步發布在 反反爬蟲技術倉庫 以及博客:
以公司為關鍵詞的新浪微博採集 | | URl-team
本文將詳細闡述具體爬蟲實現過程。
## 目標網頁
注意到,在未登錄狀態下是沒有高級搜索的,以「東方海洋」公司為例,我們只能得到如下圖所示的搜索結果![](search1.png)
而我們需要搜索的是特定時間段的微博,發現在登錄狀態是有高級搜索的,可以設置搜索時間等等,如下圖![](search2.png)
但搜索結果的顯示頁面數是有限的,也就是不管微博數目有多少,都只顯示特定頁數的微博。比如搜索「東方海洋」,如果選擇2011.01.01-2012.12.31為搜索區間,則限制頁數為38頁。
![](pages38.png)
如果選擇2011.01.01-2015.12.31為搜索區間,則限制頁數為35頁。![](pages35.png)
可見新浪搜索結果的顯示條數是有限制的,大致為40個頁面(一般每個頁面20條微博)。所以如果我們之間設置搜索區間為2011.01.01-2015.12.31,是得不到**所有**微博的。於是我選擇的搜索策略是,**每次搜索一天的微博**,這樣頁面數一般就只有1個頁面,就算多一點也不太可能達到限制頁面的數量,如果假設**未達到限制頁面數的搜索結果,其顯示的是所有微博**,則可以認為此時得到的搜索結果就是每天的所有微博。接著對每一天進行搜索就OK了。
但上面的設置搜索時間是在登錄狀態下實現的,考慮隱私等因素,還是避免使用登錄(模擬登錄是可以實現的,自己筆記本上運行做過試驗了)。於是想直接在非登錄態之間訪問上面設置時間後的網址,如直接在瀏覽器中輸入網址[http://s.weibo.com/weibo/%25E4%25B8%259C%25E6%2596%25B9%25E6%25B5%25B7%25E6%25B4%258B&typeall=1&suball=1×cope=custom:2011-01-01:2012-01-01&Refer=g](http://s.weibo.com/weibo/%25E4%25B8%259C%25E6%2596%25B9%25E6%25B5%25B7%25E6%25B4%258B&typeall=1&suball=1×cope=custom:2011-01-01:2012-01-01&Refer=g),竟然發現也是可行的,只不過一個缺點是此時只顯示1個頁面的搜索結果,對於一天的微博量,這已經足夠了(登錄狀態下試驗,基本上單天的搜索結果都只有一個頁面)。
於是,我們直接在非登錄態下進行抓取,這樣我們就可以構造需要抓取的目標網頁。
```python
import datetimenimport urllibnntm_start = datetime.date(2011,1,1)ntm_delta = datetime.timedelta(days=1)ntm_end = datetime.date(2015,12,31)nkeyword = 東方海洋nclass SinaCrawl():n def __init__(self,keyword,tm_start,tm_delta,conn,session):n self.url = 微博搜索 - 微博n self.URL = self.url + urllib.quote(keyword) + &typeall=1&suball=1&timescope=custom:+str(tm_start)+:+str(tm_start)+&Refer=gn self.myheader = {User-Agent:"Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0"}n self.conn = connn self.session = sessionn
```
## 解析網頁
利用瀏覽器的開發者工具,我們首先對網頁內容進行分析,找到我們需要抓取內容在源碼中的位置。![](f12.png)
然而這樣會有一個問題,本以為找到了所要抓取內容的位置,但是當使用BeautifulSoup尋找結點時,會發現找不到。原來我們獲取得到的網頁源碼並非如開發者工具中所示,而是如下圖所示![](sources.png)
注意到圖中的紅色方框,圖中藍色高亮部分經過一定的處理後是一個JSON,而在開發者工具中顯示的元素結構是JSON中html對應的值。於是我們首先需要提取該條script,注意到該條script區別於其他script的是含有"pid":"pl_weibo_direct",利用正則表達式便可以提出該條script。
```python
import requestsnndef getWeiboContent(self):n weiboContent = ""n try:n req = self.session.get(self.URL, headers = self.myheader)n if req.status_code == 200:n print This session work.n else:n print This session not work with code 200.n return Falsen except:n print This session not work.n return Falsen try:n page = req.contentnn except httplib.IncompleteRead:n print Incompleted!n return Falsen n soupPage = BeautifulSoup(page, lxml)n numList = soupPage.find_all(script)n if len(numList) == 0:n print you may need to input an access coden return Falsen for i in range(0, len(numList)):n IsSearch = re.search(r""pid":"pl_weibo_direct"", str(numList[i]))n if IsSearch == None:n continuen else:n weiboContent = str(numList[i])n breakn return weiboContentn
```
提取有意義的script後進行JSON解析,取出html的值得到「真正」的微博網頁源碼weiboHtml。
```python
def getWeiboHtml(self):n weiboContent = self.getWeiboContent()n if weiboContent == "":n print WeiboContents are empty. You may need to input an access code.n return Falsen elif weiboContent == False:n return Falsen n # in case some empty json elementn substr = re.compile("[]")n weiboContent = substr.sub(""NULL"", weiboContent)n n substr1 = re.compile("^.*STK.pageletM.view(")n substr2 = re.compile(")$")n substr4 = re.compile(r[)n substr5 = re.compile(r])n substr6 = re.compile(r)</script>$)n weiboContent = substr1.sub("", weiboContent)n weiboContent = substr2.sub("", weiboContent)n weiboContent = substr4.sub("", weiboContent)n weiboContent = substr5.sub("", weiboContent)n weiboContent = substr6.sub("", weiboContent)n try:n weiboJson = json.loads(weiboContent)n except:n print Json Error!n return -3n if weiboJson == None:n print you may need to input an access coden return Truen weiboHtml = weiboJson["html"]n return weiboHtmln
```
然後我們分析weiboHtml,確定所要抓取信息的位置,利用BeautifulSoup提取相應的數據。
首先判斷是否無搜索結果
```python
soup = BeautifulSoup(weiboHtml, lxml)nNoresult = soup.find_all(div,{class:pl_noresult})nif len(Noresult) != 0:n print NO result for the current search criteria.n # No resultn return 0n
```
若有搜索結果返回當前頁面的每條微博
```python
WeiboItemAll = soup.find_all(div,{action-type:feed_list_item})nWeiboItemFeed = soup.find_all(ul, {class:feed_action_info feed_action_row4})nWeiboItemContent = soup.find_all(p,{class:comment_txt})n
```
WeiboItemAll 為整條微博,它包括WeiboItemContent和WeiboItemFeed ![](WeiboItemAll.png)
然後將每一條微博轉換為soup對象,提取微博內容、點贊數、評論數以及轉發數。
```python
for i in range(0, len(WeiboItemContent)):n soupContent = BeautifulSoup(str(WeiboItemContent[i]), "lxml")n soupAll = BeautifulSoup(str(WeiboItemAll[i]), "lxml")n soupFeed = BeautifulSoup(str(WeiboItemFeed[i]), "lxml")nn mid = str(soupAll.div.attrs[mid])n STR = ""n for string in soupContent.strings:n STR += stringn content = STRn when = soupAll.find(a,{class:W_textb})n weibotime = str(when[title])n WeiboItemPraised = soupFeed.ul.contents[7]n WeiboItemComment = soupFeed.ul.contents[5]n WeiboItemForward = soupFeed.ul.contents[3]n praisedString = WeiboItemPraised.contents[0].contents[0].contents[1].stringn commentContent = WeiboItemComment.contents[0].contents[0].contentsn forwardString = WeiboItemForward.contents[0].contents[0].contents[1].stringn n if praisedString == None:n tpraised = 0n else:n praised = int(str(praisedString))n if len(commentContent) == 1:n comment = 0n else:n comment = int(str(commentContent[1].string))nn if forwardString == None:n forward = 0n else:n forward = int(str(forwardString))n
```
## 數據存儲
採用sqlite3資料庫進行存儲數據
```python
import sqlite3ndef CreateWeiboTable(db_name):n conn = sqlite3.connect(db_name)n try:n create_tb_sql = n CREATE TABLE IF NOT EXISTS weibon (mid TEXT,n content TEXT,n time TEXT,n praised INTEGER,n comment INTEGER,n forward INTEGER);n n conn.execute(create_tb_sql)n except:n print CREATE table failedn return Falsen conn.commit()n conn.close()ntndbname = test.dbnCreateWeiboTable(dbname)nconn = sqlite3.connect(dbname)n# 抓取得到的微博數據nweiboitem = (mid, content, weibotime, praised, comment, forward)nconn.execute("INSERT INTO weibo VALUES(?,?,?,?,?,?)", weiboitem)nconn.commit()n
```
至此,基本的爬蟲代碼已經完成,但是實際運行會出現一個很嚴重的問題,便是會在抓取過程中出現驗證碼。
## 動態IP
因為我們需要不斷進行抓取,而抓取次數過高新浪會有IP限制,所以便需要輸入驗證碼(如下)。![](yzm.png)
經試驗,一般連續搜索20次就會要求輸入驗證碼。解決驗證碼問題是本項目的重點,也是最關鍵的一步,如果解決了,那按照上面的爬蟲進行就好了。為了解決驗證碼問題,進行了很多嘗試。
#### 識別驗證碼
太天真了,竟然想去識別。這裡嘗試的是pytesser庫,使用其實是很簡單的。
```python
from pytesser import *nimgfile = "test.png" # 當前路徑中的一個test.png驗證碼圖象nprint image_file_to_string(imgfile)n
```
如果是下面這種簡單的驗證碼,這種方式的正確率還是蠻高的。![](simpleyzm.jpg)
另外,也嘗試了PIL庫先對驗證碼進行預處理(均值濾波、中值濾波等等),但效果均不理想,故放棄。
#### 代理IP
網上也有相關的案例,但經試驗好多代理IP無法使用,或許也因為沒有採用正確的處理方式。
#### 人工輸入驗證碼
迫不得已的方法,竟然花了2個小時完成了一個公司的搜索,但之後也沒有繼續這樣傻傻地人工輸入了,權當是嘗試了各種方案都不行的放鬆吧。
#### ADSL撥號上網動態IP
最後採用的方案。首先得有一個ADSL撥號上網的計算機,可以在淘寶上購買ADSL撥號上網的winxp伺服器,得到寬頻上網賬號USERNAME及其密碼PASSWORD,設置寬頻名稱為NAME,則可以通過下列Python代碼斷開網路連接與撥號重連
```python
os.popen(rasdial /disconnect).read()nos.popen(rasdial NAME USERNAME PASSWORD).read()n
```
實際操作中,設置抓取15次(經試驗,新浪微博一般抓取20次左右要求輸入驗證碼)後斷開網路連接,然後重連,實現更換IP的效果,也就避免了輸出驗證碼。
## 運行環境
- Python2.7
- winxp伺服器(通過某寶購買,關鍵是ADSL撥號功能,不然無法實現動態IP,也就解決不了新浪的反爬蟲機制)
## 結果
- 每個公司五年內的微博(通過sqlite3存儲)存到一個對應的.db文件中,下面截圖為company0000.db的微博。 ![](db.png)
- 所有公司微博評論數、轉發數、點贊數的統計(直接使用sql查詢語句進行統計)
比如統計每年總微博數
```python
time_sql = [(2011-01-01 00:00, 2011-12-31 23:59),n (2012-01-01 00:00, 2012-12-31 23:59),n (2013-01-01 00:00, 2013-12-31 23:59),n (2014-01-01 00:00, 2014-12-31 23:59),n (2015-01-01 00:00, 2015-12-31 23:59)]nnnum_total_sql = "select count(*) from weibo where datetime(time) >= datetime(%s) and datetime(time) <=datetime(%s);" %(time_sql[i][0], time_sql[i][1])nnres = str(conn.execute(num_total_sql).fetchone()[0]n
```
最後將結果寫進excel文件中,部分結果如下圖所示 ![](results.png)
推薦閱讀: