上學期參加了一個大數據比賽,需要抓取大量數據,於是我從新浪微博下手,本來准備使用新浪的API的,無奈新浪並沒有開放關鍵字搜索的API,所以只能用爬蟲來獲取了。幸運的是,新浪提供了一個高級搜索功能,為我們爬取數據提供了一個很好的切入點。
在查閱了一些資料,參考了一些爬蟲的例子后,得到大體思路:構造URL,爬取網頁,然后解析網頁
具體往下看~
登陸新浪微博,進入高級搜索,如圖輸入,之后發送請求會發現地址欄變為如下: http://s.weibo.com/weibo/%25E4%25B8%25AD%25E5%25B1%25B1%25E5%25A4%25A7%25E5%25AD%25A6®ion=custom:44:1&typeall=1&suball=1×cope=custom:2015-08-07-0:2015-08-08-0&Refer=g
解析如下:
固定地址部分:http://s.weibo.com/weibo/
關鍵字二次UTF-8編碼:%25E4%25B8%25AD%25E5%25B1%25B1%25E5%25A4%25A7%25E5%25AD%25A6
搜索地區:region=custom:44:1
搜索時間范圍:timescope=custom:2015-08-07-0:2015-08-08-0
可忽略項:Refer=g
某次請求的頁數:page=1(第一頁可不加)
我們查看一下網頁源代碼看看有什么鬼:
小伙伴們第一次看到肯定大呼我的天啊,真的是看的眼花繚亂。
別着急,讓我娓娓道來。
首先,我們定位到圖示的地方,即出現字符串<script>STK && STK.pageletM && STK.pageletM.view({"pid":"pl_weibo_direct"的地方,此處即搜索到的微博頁面的代碼啦~
頁面是unicode碼,所以中文都不能正常顯示~而且上面沒有排版,才顯得如此雜亂。
我們可以先對抓取到的頁面處理一下,這時就要用到lxml的etree了,它可以將網頁內容的結點構建成一棵樹。
我們拿出其中一個結點出來看看:
<a class=\"W_texta W_fb\" nick-name=\"\u554a\u5be7\u5504\" href=\"http:\/\/weibo.com\/612364698\" target=\"_blank\" title=\"\u554a\u5be7\u5504\" usercard=\"id=1884932730&usercardkey=weibo_mp\"\t\tsuda-data=\"key=tblog_search_weibo&value=weibo_ss_1_name\" class=\"name_txt W_fb\">
在這個結點中,我們可以獲取該條微博的博主的一些信息,如nick-name,微博地址href。
我們再看看另一個結點:
<p class=\"comment_txt\" node-type=\"feed_list_content\" nick-name=\"\u554a\u5be7\u5504\">\u8fd9\u4e48\u52aa\u529b \u5c45\u7136\u5012\u6570\u7b2c\u4e94 \u5509 \u4e0d\u884c\u6211\u8981\u8ffd\u56de\u6765 \u8d8a\u632b\u8d8a\u52c7 \u4e0d\u53ef\u4ee5\u81ea\u66b4\u81ea\u5f03 \u4e0d\u53ef\u4ee5\u8ba9\u8d1f\u9762\u60c5\u7eea\u8dd1\u51fa\u6765 \u83dc\u575a\u5f3a \u52a0\u6cb9\u52a0\u6cb9\u52a0\u6cb9 \u6211\u8981\u4e0a<em class=\"red\">\u4e2d\u5c71\u5927\u5b66<\/em> \u6211\u8981\u548c\u5c0f\u54c8\u5427\u4e00\u6240\u5927\u5b66 \u62fc\u4e86<\/p>
這個結點包含的數據即為微博的內容。
這樣子就清晰很多了。至於如何搜索相應的結點,取得結點的屬性和內容等,我們用的是xpath這個工具。
關於xpath,見文 http://blog.csdn.net/raptor/article/details/4516441
獲得數據后,是數據的保存,我是將數據導入到excel中,用到的xlwt和xlrd這兩個模塊。
最后數據的效果(我搜集的信息比較具體,需要訪問博主的個人主頁獲取,為便於大家閱讀、理解,下面代碼中刪去了這部分):
代碼:
- # coding: utf-8
- '''''
- 以關鍵詞收集新浪微博
- '''
- import wx
- import sys
- import urllib
- import urllib2
- import re
- import json
- import hashlib
- import os
- import time
- from datetime import datetime
- from datetime import timedelta
- import random
- from lxml import etree
- import logging
- import xlwt
- import xlrd
- from xlutils.copy import copy
- class CollectData():
- """數據收集類
- 利用微博高級搜索功能,按關鍵字搜集一定時間范圍內的微博。
- """
- def __init__(self, keyword, startTime, interval='50', flag=True, begin_url_per = "http://s.weibo.com/weibo/"):
- self.begin_url_per = begin_url_per #設置固定地址部分
- self.setKeyword(keyword) #設置關鍵字
- self.setStartTimescope(startTime) #設置搜索的開始時間
- #self.setRegion(region) #設置搜索區域
- self.setInterval(interval) #設置鄰近網頁請求之間的基礎時間間隔(注意:過於頻繁會被認為是機器人)
- self.setFlag(flag)
- self.logger = logging.getLogger('main.CollectData') #初始化日志
- ##設置關鍵字
- ##關鍵字需解碼后編碼為utf-8
- def setKeyword(self, keyword):
- self.keyword = keyword.decode('GBK','ignore').encode("utf-8")
- print 'twice encode:',self.getKeyWord()
- ##關鍵字需要進行兩次urlencode
- def getKeyWord(self):
- once = urllib.urlencode({"kw":self.keyword})[3:]
- return urllib.urlencode({"kw":once})[3:]
- ##設置起始范圍,間隔為1天
- ##格式為:yyyy-mm-dd
- def setStartTimescope(self, startTime):
- if not (startTime == '-'):
- self.timescope = startTime + ":" + startTime
- else:
- self.timescope = '-'
- ##設置搜索地區
- #def setRegion(self, region):
- # self.region = region
- ##設置鄰近網頁請求之間的基礎時間間隔
- def setInterval(self, interval):
- self.interval = int(interval)
- ##設置是否被認為機器人的標志。若為False,需要進入頁面,手動輸入驗證碼
- def setFlag(self, flag):
- self.flag = flag
- ##構建URL
- def getURL(self):
- return self.begin_url_per+self.getKeyWord()+"&typeall=1&suball=1×cope=custom:"+self.timescope+"&page="
- ##爬取一次請求中的所有網頁,最多返回50頁
- def download(self, url, maxTryNum=4):
- hasMore = True #某次請求可能少於50頁,設置標記,判斷是否還有下一頁
- isCaught = False #某次請求被認為是機器人,設置標記,判斷是否被抓住。抓住后,需要,進入頁面,輸入驗證碼
- name_filter = set([]) #過濾重復的微博ID
- i = 1 #記錄本次請求所返回的頁數
- while hasMore and i < 51 and (not isCaught): #最多返回50頁,對每頁進行解析,並寫入結果文件
- source_url = url + str(i) #構建某頁的URL
- data = '' #存儲該頁的網頁數據
- goon = True #網絡中斷標記
- ##網絡不好的情況,試着嘗試請求三次
- for tryNum in range(maxTryNum):
- try:
- html = urllib2.urlopen(source_url, timeout=12)
- data = html.read()
- break
- except:
- if tryNum < (maxTryNum-1):
- time.sleep(10)
- else:
- print 'Internet Connect Error!'
- self.logger.error('Internet Connect Error!')
- self.logger.info('url: ' + source_url)
- self.logger.info('fileNum: ' + str(fileNum))
- self.logger.info('page: ' + str(i))
- self.flag = False
- goon = False
- break
- if goon:
- lines = data.splitlines()
- isCaught = True
- for line in lines:
- ## 判斷是否有微博內容,出現這一行,則說明沒有被認為是機器人
- if line.startswith('<script>STK && STK.pageletM && STK.pageletM.view({"pid":"pl_weibo_direct"'):
- isCaught = False
- n = line.find('html":"')
- if n > 0:
- j = line[n + 7: -12].encode("utf-8").decode('unicode_escape').encode("utf-8").replace("\\", "") #去掉所有的\
- ## 沒有更多結果頁面
- if (j.find('<div class="search_noresult">') > 0):
- hasMore = False
- ## 有結果的頁面
- else:
- #此處j要decode,因為上面j被encode成utf-8了
- page = etree.HTML(j.decode('utf-8'))
- ps = page.xpath("//p[@node-type='feed_list_content']") #使用xpath解析得到微博內容
- addrs = page.xpath("//a[@class='W_texta W_fb']") #使用xpath解析得到博主地址
- addri = 0
- #獲取昵稱和微博內容
- for p in ps:
- name = p.attrib.get('nick-name') #獲取昵稱
- txt = p.xpath('string(.)') #獲取微博內容
- addr = addrs[addri].attrib.get('href') #獲取微博地址
- addri += 1
- if(name != 'None' and str(txt) != 'None' and name not in name_filter): #導出數據到excel中
- name_filter.add(name)
- oldWb = xlrd.open_workbook('weiboData.xls', formatting_info=True)
- oldWs = oldWb.sheet_by_index(0)
- rows = int(oldWs.cell(0,0).value)
- newWb = copy(oldWb)
- newWs = newWb.get_sheet(0)
- newWs.write(rows, 0, str(rows))
- newWs.write(rows, 1, name)
- newWs.write(rows, 2, self.timescope)
- newWs.write(rows, 3, addr)
- newWs.write(rows, 4, txt)
- newWs.write(0, 0, str(rows+1))
- newWb.save('weiboData.xls')
- print "save with same name ok"
- break
- lines = None
- ## 處理被認為是機器人的情況
- if isCaught:
- print 'Be Caught!'
- self.logger.error('Be Caught Error!')
- self.logger.info('filePath: ' + savedir)
- self.logger.info('url: ' + source_url)
- self.logger.info('fileNum: ' + str(fileNum))
- self.logger.info('page:' + str(i))
- data = None
- self.flag = False
- break
- ## 沒有更多結果,結束該次請求,跳到下一個請求
- if not hasMore:
- print 'No More Results!'
- if i == 1:
- time.sleep(random.randint(3,8))
- else:
- time.sleep(10)
- data = None
- break
- i += 1
- ## 設置兩個鄰近URL請求之間的隨機休眠時間,防止Be Caught
- sleeptime_one = random.randint(self.interval-25,self.interval-15)
- sleeptime_two = random.randint(self.interval-15,self.interval)
- if i%2 == 0:
- sleeptime = sleeptime_two
- else:
- sleeptime = sleeptime_one
- print 'sleeping ' + str(sleeptime) + ' seconds...'
- time.sleep(sleeptime)
- else:
- break
- ##改變搜索的時間范圍,有利於獲取最多的數據
- def getTimescope(self, perTimescope):
- if not (perTimescope=='-'):
- times_list = perTimescope.split(':')
- start_date = datetime(int(times_list[-1][0:4]), int(times_list[-1][5:7]), int(times_list[-1][8:10]) )
- start_new_date = start_date + timedelta(days = 1)
- start_str = start_new_date.strftime("%Y-%m-%d")
- return start_str + ":" + start_str
- else:
- return '-'
- def main():
- logger = logging.getLogger('main')
- logFile = './collect.log'
- logger.setLevel(logging.DEBUG)
- filehandler = logging.FileHandler(logFile)
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s')
- filehandler.setFormatter(formatter)
- logger.addHandler(filehandler)
- while True:
- ## 接受鍵盤輸入
- keyword = raw_input('Enter the keyword(type \'quit\' to exit ):')
- if keyword == 'quit':
- sys.exit()
- startTime = raw_input('Enter the start time(Format:YYYY-mm-dd):')
- #region = raw_input('Enter the region([BJ]11:1000,[SH]31:1000,[GZ]44:1,[CD]51:1):')
- interval = raw_input('Enter the time interval( >30 and deafult:50):')
- ##實例化收集類,收集指定關鍵字和起始時間的微博
- cd = CollectData(keyword, startTime, interval)
- while cd.flag:
- print cd.timescope
- logger.info(cd.timescope)
- url = cd.getURL()
- cd.download(url)
- cd.timescope = cd.getTimescope(cd.timescope) #改變搜索的時間,到下一天
- else:
- cd = None
- print '-----------------------------------------------------'
- print '-----------------------------------------------------'
- else:
- logger.removeHandler(filehandler)
- logger = None
- ##if __name__ == '__main__':
- ## main()
上面實現了數據的爬取,再結合上一篇文章中的模擬登錄,就可以美美的抓數據啦~