鍍金池/ 教程/ Python/ 爬取百度貼吧帖子
綜述
Cookie 的使用
爬蟲基礎(chǔ)了解
計(jì)算大學(xué)本學(xué)期績點(diǎn)
抓取淘寶 MM 照片
爬蟲框架 Scrapy 安裝配置
模擬登錄淘寶并獲取所有訂單
Urllib 庫的高級(jí)用法
URLError 異常處理
正則表達(dá)式
Beautiful Soup 的用法
爬取糗事百科段子
爬取百度貼吧帖子
Urllib 庫的基本使用

爬取百度貼吧帖子

大家好,上次我們實(shí)驗(yàn)了爬取了糗事百科的段子,那么這次我們來嘗試一下爬取百度貼吧的帖子。與上一篇不同的是,這次我們需要用到文件的相關(guān)操作。

本篇目標(biāo)

  1. 對(duì)百度貼吧的任意帖子進(jìn)行抓取

  2. 指定是否只抓取樓主發(fā)帖內(nèi)容

  3. 將抓取到的內(nèi)容分析并保存到文件

URL 格式的確定

首先,我們先觀察一下百度貼吧的任意一個(gè)帖子。

比如:http://tieba.baidu.com/p/3138733512?see_lz=1&pn=1,這是一個(gè)關(guān)于 NBA50 大的盤點(diǎn),分析一下這個(gè)地址。

 http://  代表資源傳輸使用http協(xié)議
 tieba.baidu.com 是百度的二級(jí)域名,指向百度貼吧的服務(wù)器。
 /p/3138733512 是服務(wù)器某個(gè)資源,即這個(gè)帖子的地址定位符
 see_lz和pn是該URL的兩個(gè)參數(shù),分別代表了只看樓主和帖子頁碼,等于1表示該條件為真  

所以我們可以把 URL 分為兩部分,一部分為基礎(chǔ)部分,一部分為參數(shù)部分。

例如,上面的URL我們劃分基礎(chǔ)部分是 http://tieba.baidu.com/p/3138733512,參數(shù)部分是 ?see_lz=1&pn=1

頁面的抓取

熟悉了 URL 的格式,那就讓我們用 urllib2 庫來試著抓取頁面內(nèi)容吧。上一篇糗事百科我們最后改成了面向?qū)ο蟮木幋a方式,這次我們直接嘗試一下,定義一個(gè)類名叫 BDTB(百度貼吧),一個(gè)初始化方法,一個(gè)獲取頁面的方法。

其中,有些帖子我們想指定給程序是否要只看樓主,所以我們把只看樓主的參數(shù)初始化放在類的初始化上,即 init 方法。另外,獲取頁面的方法我們需要知道一個(gè)參數(shù)就是帖子頁碼,所以這個(gè)參數(shù)的指定我們放在該方法中。

綜上,我們初步構(gòu)建出基礎(chǔ)代碼如下:

\_\_author\_\_ = 'CQC'
\# -*- coding:utf-8 -*-
import urllib
import urllib2
import re

\#百度貼吧爬蟲類
class BDTB:

    \#初始化,傳入基地址,是否只看樓主的參數(shù)
    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"連接百度貼吧失敗,錯(cuò)誤原因",e.reason
                return None

baseURL = 'http://tieba.baidu.com/p/3138733512'
bdtb = BDTB(baseURL,1)
bdtb.getPage(1)  

運(yùn)行代碼,我們可以看到屏幕上打印出了這個(gè)帖子第一頁樓主發(fā)言的所有內(nèi)容,形式為 HTML 代碼。

http://wiki.jikexueyuan.com/project/python-crawler-guide/images/09.png" alt="" />

提取相關(guān)信息

提取帖子標(biāo)題

首先,讓我們提取帖子的標(biāo)題。

在瀏覽器中審查元素,或者按 F12,查看頁面源代碼,我們找到標(biāo)題所在的代碼段,可以發(fā)現(xiàn)這個(gè)標(biāo)題的 HTML 代碼是

<h1 class="core_title_txt  " title="純?cè)瓌?chuàng)我心中的NBA2014-2015賽季現(xiàn)役50大" style="width: 396px">純?cè)瓌?chuàng)我心中的NBA2014-2015賽季現(xiàn)役50大</h1>  

所以我們想提取

標(biāo)簽中的內(nèi)容,同時(shí)還要指定這個(gè) class 確定唯一,因?yàn)?h1 標(biāo)簽實(shí)在太多啦。

正則表達(dá)式如下

<h1 class="core_title_txt.*?>(.*?)</h1>  

所以,我們?cè)黾右粋€(gè)獲取頁面標(biāo)題的方法

\#獲取帖子標(biāo)題
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)  #測(cè)試輸出
        return result.group(1).strip()
    else:
        return None  

提取帖子頁數(shù)

同樣地,帖子總頁數(shù)我們也可以通過分析頁面中的共?頁來獲取。所以我們的獲取總頁數(shù)的方法如下

\#獲取帖子一共有多少頁
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)  #測(cè)試輸出
        return result.group(1).strip()
    else:
        return None  

提取正文內(nèi)容

審查元素,我們可以看到百度貼吧每一層樓的主要內(nèi)容都在

標(biāo)簽里面,所以我們可以寫如下的正則表達(dá)式

<div id="post_content_.*?>(.*?)</div>  

相應(yīng)地,獲取頁面所有樓層數(shù)據(jù)的方法可以寫成如下方法

\#獲取每一層樓的內(nèi)容,傳入頁面內(nèi)容
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  

好,我們運(yùn)行一下結(jié)果看一下

http://wiki.jikexueyuan.com/project/python-crawler-guide/images/10.png" alt="" />

真是醉了,還有一大片換行符和圖片符,好口怕!既然這樣,我們就要對(duì)這些文本進(jìn)行處理,把各種各樣復(fù)雜的標(biāo)簽給它剔除掉,還原精華內(nèi)容,把文本處理寫成一個(gè)方法也可以,不過為了實(shí)現(xiàn)更好的代碼架構(gòu)和代碼重用,我們可以考慮把標(biāo)簽等的處理寫作一個(gè)類。

那我們就叫它 Tool(工具類吧),里面定義了一個(gè)方法,叫 replace,是替換各種標(biāo)簽的。在類中定義了幾個(gè)正則表達(dá)式,主要利用了 re.sub 方法對(duì)文本進(jìn)行匹配后然后替換。具體的思路已經(jīng)寫到注釋中,大家可以看一下這個(gè)類

import re

\#處理頁面標(biāo)簽類
class Tool:
    \#去除img標(biāo)簽,7位長空格
    removeImg = re.compile('<img.*?>| {7}|')
    \#刪除超鏈接標(biāo)簽
    removeAddr = re.compile('<a.*?>|</a>')
    \#把換行的標(biāo)簽換為\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>')
    \#將其余標(biāo)簽剔除
    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()將前后多余內(nèi)容刪除
        return x.strip()  

在使用時(shí),我們只需要初始化一下這個(gè)類,然后調(diào)用 replace 方法即可。

現(xiàn)在整體代碼是如下這樣子的,現(xiàn)在我的代碼是寫到這樣子的

__author__ = 'CQC'
\# -*- coding:utf-8 -*-
import urllib
import urllib2
import re

\#處理頁面標(biāo)簽類
class Tool:
    \#去除img標(biāo)簽,7位長空格
    removeImg = re.compile('<img.*?>| {7}|')
    \#刪除超鏈接標(biāo)簽
    removeAddr = re.compile('<a.*?>|</a>')
    \#把換行的標(biāo)簽換為\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>')
    \#將其余標(biāo)簽剔除
    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()將前后多余內(nèi)容刪除
        return x.strip()

\#百度貼吧爬蟲類
class BDTB:

    \#初始化,傳入基地址,是否只看樓主的參數(shù)
    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"連接百度貼吧失敗,錯(cuò)誤原因",e.reason
                return None

    \#獲取帖子標(biāo)題
    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)  #測(cè)試輸出
            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)  #測(cè)試輸出
            return result.group(1).strip()
        else:
            return None

    \#獲取每一層樓的內(nèi)容,傳入頁面內(nèi)容
    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))  

我們嘗試一下,重新再看一下效果,這下經(jīng)過處理之后應(yīng)該就沒問題了,是不是感覺好酸爽!

http://wiki.jikexueyuan.com/project/python-crawler-guide/images/11.png" alt="" />

替換樓層

至于這個(gè)問題,我感覺直接提取樓層沒什么必要呀,因?yàn)橹豢礃侵鞯脑?,有些樓層的編?hào)是間隔的,所以我們得到的樓層序號(hào)是不連續(xù)的,這樣我們保存下來也沒什么用。

所以可以嘗試下面的方法:

  1. 每打印輸出一段樓層,寫入一行橫線來間隔,或者換行符也好。
  2. 試著重新編一個(gè)樓層,按照順序,設(shè)置一個(gè)變量,每打印出一個(gè)結(jié)果變量加一,打印出這個(gè)變量當(dāng)做樓層。

這里我們嘗試一下吧,看看效果怎樣

把 getContent 方法修改如下

\#獲取每一層樓的內(nèi)容,傳入頁面內(nèi)容
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  

運(yùn)行一下看看效果

http://wiki.jikexueyuan.com/project/python-crawler-guide/images/12.png" alt="" />

嘿嘿,效果還不錯(cuò)吧,感覺真酸爽!接下來我們完善一下,然后寫入文件

寫入文件

最后便是寫入文件的過程,過程很簡單,就幾句話的代碼而已,主要是利用了以下兩句

file = open(“tb.txt”,”w”)
file.writelines(obj)  

這里不再贅述,稍后直接貼上完善之后的代碼。

完善代碼

現(xiàn)在我們對(duì)代碼進(jìn)行優(yōu)化,重構(gòu),在一些地方添加必要的打印信息,整理如下

__author__ = 'CQC'
\# -*- coding:utf-8 -*-
import urllib
import urllib2
import re

\#處理頁面標(biāo)簽類
class Tool:
    \#去除img標(biāo)簽,7位長空格
    removeImg = re.compile('<img.*?>| {7}|')
    \#刪除超鏈接標(biāo)簽
    removeAddr = re.compile('<a.*?>|</a>')
    \#把換行的標(biāo)簽換為\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>')
    \#將其余標(biāo)簽剔除
    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()將前后多余內(nèi)容刪除
        return x.strip()

\#百度貼吧爬蟲類
class BDTB:

    #初始化,傳入基地址,是否只看樓主的參數(shù)
    def __init__(self,baseUrl,seeLZ,floorTag):
        \#base鏈接地址
        self.baseURL = baseUrl
        \#是否只看樓主
        self.seeLZ = '?see_lz='+str(seeLZ)
        \#HTML標(biāo)簽剔除工具類對(duì)象
        self.tool = Tool()
        \#全局file變量,文件寫入操作對(duì)象
        self.file = None
        \#樓層標(biāo)號(hào),初始為1
        self.floor = 1
        \#默認(rèn)的標(biāo)題,如果沒有成功獲取到標(biāo)題的話則會(huì)用這個(gè)標(biāo)題
        self.defaultTitle = u"百度貼吧"
        \#是否寫入樓分隔符的標(biāo)記
        self.floorTag = floorTag

    \#傳入頁碼,獲取該頁帖子的代碼
    def getPage(self,pageNum):
        try:
            \#構(gòu)建URL
            url = self.baseURL+ self.seeLZ + '&pn=' + str(pageNum)
            request = urllib2.Request(url)
            response = urllib2.urlopen(request)
            \#返回UTF-8格式編碼內(nèi)容
            return response.read().decode('utf-8')
        \#無法連接,報(bào)錯(cuò)
        except urllib2.URLError, e:
            if hasattr(e,"reason"):
                print u"連接百度貼吧失敗,錯(cuò)誤原因",e.reason
                return None

    \#獲取帖子標(biāo)題
    def getTitle(self,page):
        \#得到標(biāo)題的正則表達(dá)式
        pattern = re.compile('<h1 class="core_title_txt.*?>(.*?)</h1>',re.S)
        result = re.search(pattern,page)
        if result:
            \#如果存在,則返回標(biāo)題
            return result.group(1).strip()
        else:
            return None

    \#獲取帖子一共有多少頁
    def getPageNum(self,page):
        \#獲取帖子頁數(shù)的正則表達(dá)式
        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

    \#獲取每一層樓的內(nèi)容,傳入頁面內(nèi)容
    def getContent(self,page):
        \#匹配所有樓層的內(nèi)容
        pattern = re.compile('<div id="post_content_.*?>(.*?)</div>',re.S)
        items = re.findall(pattern,page)
        contents = []
        for item in items:
            \#將文本進(jìn)行去除標(biāo)簽處理,同時(shí)在前后加入換行符
            content = "\n"+self.tool.replace(item)+"\n"
            contents.append(content.encode('utf-8'))
        return contents

    def setFileTitle(self,title):
        \#如果標(biāo)題不是為None,即成功獲取到標(biāo)題
        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已失效,請(qǐng)重試"
            return
        try:
            print "該帖子共有" + str(pageNum) + "頁"
            for i in range(1,int(pageNum)+1):
                print "正在寫入第" + str(i) + "頁數(shù)據(jù)"
                page = self.getPage(i)
                contents = self.getContent(page)
                self.writeData(contents)
        \#出現(xiàn)寫入異常
        except IOError,e:
            print "寫入異常,原因" + e.message
        finally:
            print "寫入任務(wù)完成"

print u"請(qǐng)輸入帖子代號(hào)"
baseURL = 'http://tieba.baidu.com/p/' + str(raw_input(u'http://tieba.baidu.com/p/'))
seeLZ = raw_input("是否只獲取樓主發(fā)言,是輸入1,否輸入0\n")
floorTag = raw_input("是否寫入樓層信息,是輸入1,否輸入0\n")
bdtb = BDTB(baseURL,seeLZ,floorTag)
bdtb.start()  

現(xiàn)在程序演示如下

http://wiki.jikexueyuan.com/project/python-crawler-guide/images/13.png" alt="" />

完成之后,可以查看一下當(dāng)前目錄下多了一個(gè)以該帖子命名的 txt 文件,內(nèi)容便是帖子的所有數(shù)據(jù)。

抓貼吧,就是這么簡單和任性!