技術| Python的從零開始系列連載(三十一)
大家好,上次我們實驗了爬取了糗事百科的段子,那麼這次我們來嘗試一下爬取百度貼吧的帖子。與上一篇不同的是,這次我們需要用到檔案的相關操作。
本篇目標
1.對百度貼吧的任意帖子進行抓取
2.指定是否只抓取樓主發帖內容
3.將抓取到的內容分析並儲存到檔案
1.URL格式的確定
首先,我們先觀察一下百度貼吧的任意一個帖子。
比如:http://tieba.baidu.com/p/3138733512?see_lz=1&pn=1,這是一個關於NBA50大的盤點,分析一下這個地址。
http:// 代表資源傳輸使用http協議
tieba.baidu.com 是百度的二級域名,指向百度貼吧的伺服器。
/p/3138733512 是伺服器某個資源,即這個帖子的地址定位符
see_lz和pn是該URL的兩個引數,分別代表了只看樓主和帖子頁碼,等於1表示該條件為真
所以我們可以把URL分為兩部分,一部分為基礎部分,一部分為引數部分。
例如,上面的URL我們劃分基礎部分是 http://tieba.baidu.com/p/3138733512,引數部分是 ?see_lz=1&pn=1
2.頁面的抓取
熟悉了URL的格式,那就讓我們用urllib2庫來試著抓取頁面內容吧。上一篇糗事百科我們最後改成了面向物件的編碼方式,這次我們直接嘗試一下,定義一個類名叫BDTB(百度貼吧),一個初始化方法,一個獲取頁面的方法。
其中,有些帖子我們想指定給程式是否要只看樓主,所以我們把只看樓主的引數初始化放在類的初始化上,即init方法。另外,獲取頁面的方法我們需要知道一個引數就是帖子頁碼,所以這個引數的指定我們放在該方法中。
綜上,我們初步構建出基礎程式碼如下:
__author__ = 'CQC' # -*- coding:utf-8 -*- import urllib import urllib2 import re #百度貼吧爬蟲類 class BDTB: #初始化,傳入基地址,是否只看樓主的引數 def __init__(self,baseUrl,seeLZ): self.baseURL = baseUrl self.seeLZ = '?see_lz='+str(seeLZ) #傳入頁碼,獲取該頁帖子的程式碼 def getPage(self,pageNum): try: url = self.baseURL+ self.seeLZ + '&pn=' + str(pageNum) request = urllib2.Request(url) response = urllib2.urlopen(request) print response.read() return response except urllib2.URLError, e: if hasattr(e,"reason"): print u"連線百度貼吧失敗,錯誤原因",e.reason return None baseURL = 'http://tieba.baidu.com/p/3138733512' bdtb = BDTB(baseURL,1) bdtb.getPage(1)
執行程式碼,我們可以看到螢幕上打印出了這個帖子第一頁樓主發言的所有內容,形式為HTML程式碼。
3.提取相關資訊
1)提取帖子標題
首先,讓我們提取帖子的標題。
在瀏覽器中審查元素,或者按F12,檢視頁面原始碼,我們找到標題所在的程式碼段,可以發現這個標題的HTML程式碼是
<h1 class="core_title_txt" title="純原創我心中的NBA2014-2015賽季現役50大" style="width: 396px">純原創我心中的NBA2014-2015賽季現役50大</h1>
所以我們想提取<h1>標籤中的內容,同時還要指定這個class確定唯一,因為h1標籤實在太多啦。
正則表示式如下
<h1 class="core_title_txt.*?>(.*?)</h1>
所以,我們增加一個獲取頁面標題的方法
#獲取帖子標題 def getTitle(self): page = self.getPage(1) pattern = re.compile('<h1 class="core_title_txt.*?>(.*?)</h1>',re.S) result = re.search(pattern,page) if result: #print result.group(1)#測試輸出 return result.group(1).strip() else: return None
2)提取帖子頁數
同樣地,帖子總頁數我們也可以通過分析頁面中的共?頁來獲取。所以我們的獲取總頁數的方法如下
#獲取帖子一共有多少頁 def getPageNum(self): page = self.getPage(1) pattern = re.compile('<li class="l_reply_num.*?</span>.*?<span.*?>(.*?)</span>',re.S) result = re.search(pattern,page) if result: #print result.group(1)#測試輸出 return result.group(1).strip() else: return None
3)提取正文內容
審查元素,我們可以看到百度貼吧每一層樓的主要內容都在<div id=”post_content_xxxx”></div>標籤裡面,所以我們可以寫如下的正則表示式
<div id="post_content_.*?>(.*?)</div>
相應地,獲取頁面所有樓層資料的方法可以寫成如下方法
#獲取每一層樓的內容,傳入頁面內容 def getContent(self,page): pattern = re.compile('<div id="post_content_.*?>(.*?)</div>',re.S) items = re.findall(pattern,page) for item in items: print item
好,我們執行一下結果看一下
真是醉了,還有一大片換行符和圖片符,好口怕!既然這樣,我們就要對這些文字進行處理,把各種各樣複雜的標籤給它剔除掉,還原精華內容,把文字處理寫成一個方法也可以,不過為了實現更好的程式碼架構和程式碼重用,我們可以考慮把標籤等的處理寫作一個類。
那我們就叫它Tool(工具類吧),裡面定義了一個方法,叫replace,是替換各種標籤的。在類中定義了幾個正則表示式,主要利用了re.sub方法對文字進行匹配後然後替換。具體的思路已經寫到註釋中,大家可以看一下這個類
import re #處理頁面標籤類 class Tool: #去除img標籤,7位長空格 removeImg = re.compile('<img.*?>| {7}|') #刪除超連結標籤 removeAddr = re.compile('<a.*?>|</a>') #把換行的標籤換為\n replaceLine = re.compile('<tr>|<div>|</div>|</p>') #將表格製表<td>替換為\t replaceTD= re.compile('<td>') #把段落開頭換為\n加空兩格 replacePara = re.compile('<p.*?>') #將換行符或雙換行符替換為\n replaceBR = re.compile('<br><br>|<br>') #將其餘標籤剔除 removeExtraTag = re.compile('<.*?>') def replace(self,x): x = re.sub(self.removeImg,"",x) x = re.sub(self.removeAddr,"",x) x = re.sub(self.replaceLine,"\n",x) x = re.sub(self.replaceTD,"\t",x) x = re.sub(self.replacePara,"\n",x) x = re.sub(self.replaceBR,"\n",x) x = re.sub(self.removeExtraTag,"",x) #strip()將前後多餘內容刪除 return x.strip()
在使用時,我們只需要初始化一下這個類,然後呼叫replace方法即可。
現在整體程式碼是如下這樣子的,現在我的程式碼是寫到這樣子的
__author__ = 'CQC' # -*- coding:utf-8 -*- import urllib import urllib2 import re #處理頁面標籤類 class Tool: #去除img標籤,7位長空格 removeImg = re.compile('<img.*?>| {7}|') #刪除超連結標籤 removeAddr = re.compile('<a.*?>|</a>') #把換行的標籤換為\n replaceLine = re.compile('<tr>|<div>|</div>|</p>') #將表格製表<td>替換為\t replaceTD= re.compile('<td>') #把段落開頭換為\n加空兩格 replacePara = re.compile('<p.*?>') #將換行符或雙換行符替換為\n replaceBR = re.compile('<br><br>|<br>') #將其餘標籤剔除 removeExtraTag = re.compile('<.*?>') def replace(self,x): x = re.sub(self.removeImg,"",x) x = re.sub(self.removeAddr,"",x) x = re.sub(self.replaceLine,"\n",x) x = re.sub(self.replaceTD,"\t",x) x = re.sub(self.replacePara,"\n",x) x = re.sub(self.replaceBR,"\n",x) x = re.sub(self.removeExtraTag,"",x) #strip()將前後多餘內容刪除 return x.strip() #百度貼吧爬蟲類 class BDTB: #初始化,傳入基地址,是否只看樓主的引數 def __init__(self,baseUrl,seeLZ): self.baseURL = baseUrl self.seeLZ = '?see_lz='+str(seeLZ) self.tool = Tool() #傳入頁碼,獲取該頁帖子的程式碼 def getPage(self,pageNum): try: url = self.baseURL+ self.seeLZ + '&pn=' + str(pageNum) request = urllib2.Request(url) response = urllib2.urlopen(request) return response.read().decode('utf-8') except urllib2.URLError, e: if hasattr(e,"reason"): print u"連線百度貼吧失敗,錯誤原因",e.reason return None #獲取帖子標題 def getTitle(self): page = self.getPage(1) pattern = re.compile('<h1 class="core_title_txt.*?>(.*?)</h1>',re.S) result = re.search(pattern,page) if result: #print result.group(1)#測試輸出 return result.group(1).strip() else: return None #獲取帖子一共有多少頁 def getPageNum(self): page = self.getPage(1) pattern = re.compile('<li class="l_reply_num.*?</span>.*?<span.*?>(.*?)</span>',re.S) result = re.search(pattern,page) if result: #print result.group(1)#測試輸出 return result.group(1).strip() else: return None #獲取每一層樓的內容,傳入頁面內容 def getContent(self,page): pattern = re.compile('<div id="post_content_.*?>(.*?)</div>',re.S) items = re.findall(pattern,page) #for item in items: #print item print self.tool.replace(items[1]) baseURL = 'http://tieba.baidu.com/p/3138733512' bdtb = BDTB(baseURL,1) bdtb.getContent(bdtb.getPage(1))
我們嘗試一下,重新再看一下效果,這下經過處理之後應該就沒問題了,是不是感覺好酸爽!
4)替換樓層
至於這個問題,我感覺直接提取樓層沒什麼必要呀,因為只看樓主的話,有些樓層的編號是間隔的,所以我們得到的樓層序號是不連續的,這樣我們儲存下來也沒什麼用。
所以可以嘗試下面的方法:
1.每列印輸出一段樓層,寫入一行橫線來間隔,或者換行符也好。
2.試著重新編一個樓層,按照順序,設定一個變數,每打印出一個結果變數加一,打印出這個變數當做樓層。
這裡我們嘗試一下吧,看看效果怎樣
把getContent方法修改如下
#獲取每一層樓的內容,傳入頁面內容 def getContent(self,page): pattern = re.compile('<div id="post_content_.*?>(.*?)</div>',re.S) items = re.findall(pattern,page) floor = 1 for item in items: print floor,u"樓------------------------------------------------------------------------------------------------------------------------------------\n" print self.tool.replace(item) floor += 1
執行一下看看效果
嘿嘿,效果還不錯吧,感覺真酸爽!接下來我們完善一下,然後寫入檔案
4.寫入檔案
最後便是寫入檔案的過程,過程很簡單,就幾句話的程式碼而已,主要是利用了以下兩句
file = open(“tb.txt”,”w”)
file.writelines(obj)
這裡不再贅述,稍後直接貼上完善之後的程式碼。
5.完善程式碼
現在我們對程式碼進行優化,重構,在一些地方新增必要的列印資訊,整理如下
__author__ = 'CQC' # -*- coding:utf-8 -*- import urllib import urllib2 import re #處理頁面標籤類 class Tool: #去除img標籤,7位長空格 removeImg = re.compile('<img.*?>| {7}|') #刪除超連結標籤 removeAddr = re.compile('<a.*?>|</a>') #把換行的標籤換為\n replaceLine = re.compile('<tr>|<div>|</div>|</p>') #將表格製表<td>替換為\t replaceTD= re.compile('<td>') #把段落開頭換為\n加空兩格 replacePara = re.compile('<p.*?>') #將換行符或雙換行符替換為\n replaceBR = re.compile('<br><br>|<br>') #將其餘標籤剔除 removeExtraTag = re.compile('<.*?>') def replace(self,x): x = re.sub(self.removeImg,"",x) x = re.sub(self.removeAddr,"",x) x = re.sub(self.replaceLine,"\n",x) x = re.sub(self.replaceTD,"\t",x) x = re.sub(self.replacePara,"\n",x) x = re.sub(self.replaceBR,"\n",x) x = re.sub(self.removeExtraTag,"",x) #strip()將前後多餘內容刪除 return x.strip() #百度貼吧爬蟲類 class BDTB: #初始化,傳入基地址,是否只看樓主的引數 def __init__(self,baseUrl,seeLZ,floorTag): #base連結地址 self.baseURL = baseUrl #是否只看樓主 self.seeLZ = '?see_lz='+str(seeLZ) #HTML標籤剔除工具類物件 self.tool = Tool() #全域性file變數,檔案寫入操作物件 self.file = None #樓層標號,初始為1 self.floor = 1 #預設的標題,如果沒有成功獲取到標題的話則會用這個標題 self.defaultTitle = u"百度貼吧" #是否寫入樓分隔符的標記 self.floorTag = floorTag #傳入頁碼,獲取該頁帖子的程式碼 def getPage(self,pageNum): try: #構建URL url = self.baseURL+ self.seeLZ + '&pn=' + str(pageNum) request = urllib2.Request(url) response = urllib2.urlopen(request) #返回UTF-8格式編碼內容 return response.read().decode('utf-8') #無法連線,報錯 except urllib2.URLError, e: if hasattr(e,"reason"): print u"連線百度貼吧失敗,錯誤原因",e.reason return None #獲取帖子標題 def getTitle(self,page): #得到標題的正則表示式 pattern = re.compile('<h1 class="core_title_txt.*?>(.*?)</h1>',re.S) result = re.search(pattern,page) if result: #如果存在,則返回標題 return result.group(1).strip() else: return None #獲取帖子一共有多少頁 def getPageNum(self,page): #獲取帖子頁數的正則表示式 pattern = re.compile('<li class="l_reply_num.*?</span>.*?<span.*?>(.*?)</span>',re.S) result = re.search(pattern,page) if result: return result.group(1).strip() else: return None #獲取每一層樓的內容,傳入頁面內容 def getContent(self,page): #匹配所有樓層的內容 pattern = re.compile('<div id="post_content_.*?>(.*?)</div>',re.S) items = re.findall(pattern,page) contents = [] for item in items: #將文字進行去除標籤處理,同時在前後加入換行符 content = "\n"+self.tool.replace(item)+"\n" contents.append(content.encode('utf-8')) return contents def setFileTitle(self,title): #如果標題不是為None,即成功獲取到標題 if title is not None: self.file = open(title + ".txt","w+") else: self.file = open(self.defaultTitle + ".txt","w+") def writeData(self,contents): #向檔案寫入每一樓的資訊 for item in contents: if self.floorTag == '1': #樓之間的分隔符 floorLine = "\n" + str(self.floor) + u"-----------------------------------------------------------------------------------------\n" self.file.write(floorLine) self.file.write(item) self.floor += 1 def start(self): indexPage = self.getPage(1) pageNum = self.getPageNum(indexPage) title = self.getTitle(indexPage) self.setFileTitle(title) if pageNum == None: print "URL已失效,請重試" return try: print "該帖子共有" + str(pageNum) + "頁" for i in range(1,int(pageNum)+1): print "正在寫入第" + str(i) + "頁資料" page = self.getPage(i) contents = self.getContent(page) self.writeData(contents) #出現寫入異常 except IOError,e: print "寫入異常,原因" + e.message finally: print "寫入任務完成" print u"請輸入帖子代號" baseURL = 'http://tieba.baidu.com/p/' + str(raw_input(u'http://tieba.baidu.com/p/')) seeLZ = raw_input("是否只獲取樓主發言,是輸入1,否輸入0\n") floorTag = raw_input("是否寫入樓層資訊,是輸入1,否輸入0\n") bdtb = BDTB(baseURL,seeLZ,floorTag) bdtb.start()
現在程式演示如下
完成之後,可以檢視一下當前目錄下多了一個以該帖子命名的txt檔案,內容便是帖子的所有資料。
抓貼吧,就是這麼簡單和任性!
原文釋出時間為:2018-11-30
本文作者:燈塔大資料
本文來自雲棲社群合作伙伴“ ofollow,noindex">燈塔大資料 ”,瞭解相關資訊可以關注“DTbigdata”微信公眾號