python爬蟲入門 實戰(二)---爬百度貼吧


本篇涉及知識點:
1、xpath語法
2、正則表達式

踩坑:
1、xpath解析出的結點文本內容中文亂碼。
2、xpath解析時,結點內有多余標簽,文本被截斷。
3、用正則表達式匹配的分組輸出亂碼。


發送請求獲取html文本內容

import urllib2

#目標url,這里see_lz=1代表只看樓主,pn=1代表頁碼為1
url='https://tieba.baidu.com/p/3267113128?see_lz=1&pn=1’

request=urllib2.Request(url)#封裝請求
response=urllib2.urlopen(request)#打開url資源,得到響應
str_html=response.read()#讀取html文本內容
response.close()#關閉資源

print str_html#打印html文本

我們看輸出結果,這里中文是正常,沒有亂碼的:

html文本輸出

xpath解析并獲取每一個樓層的內容

1. 我們先來分析html文本結構

點擊左邊小紅框的按鈕再點擊目標,即可查看到相應的標簽。


樓層內容的標簽

可以看到這個div有很明顯的特征,id=“post_content_56723422722”,即id包括字段“post_content_”,我們可以直接用contains函數通過id來找到。

tree.xpath('//div[contains(@id,"post_content_")]']

不過為了保險起見,也為了多熟悉一下xpath解析的語法,我們多往上看兩層。目標是class=“d_post_content_main ”的div下的一個class=“p_content ”的div里的cc標簽下的唯一div。
所以我們可以用以下語句找到所有樓層的目標內容:

tree.xpath('//div[@class="d_post_content_main "]/div[@class="p_content  "]/cc/div')

同理我們可以分析結構,解析得到相應的樓層號:

tree.xpath('//div[@class="d_post_content_main "]/div[@class="core_reply j_lzl_wrapper"]/div[@class="core_reply_tail clearfix"]/div[@class="post-tail-wrap"]/span[2]')

2. 獲取每個樓層的內容

import urllib2
from lxml import html

url='https://tieba.baidu.com/p/3267113128?see_lz=1&pn=1'#目標url
request=urllib2.Request(url)#封裝請求
response=urllib2.urlopen(request)#打開url資源,得到響應
str_html=response.read()#讀取html文本內容
response.close()#關閉資源

tree=html.fromstring(str_html)
nodes=tree.xpath('//div[@class="d_post_content_main "]')#先找的所有的樓層再進一步解析樓層號碼和內容

for node in nodes:
    layer=node.xpath('div[@class="core_reply j_lzl_wrapper"]/div[@class="core_reply_tail clearfix"]/div[@class="post-tail-wrap"]/span[2]')[0].text
    print layer#輸出樓層號
     
    content=node.xpath('div[@class="p_content  "]/cc/div')[0].text
    print content#輸出樓層內容

這里就出現第一個坑了,前面輸出html文本是沒有中文亂碼的,這里xpath解析完以后輸出就中文亂碼了。運行一下看輸出結果:

xpath解析中文亂碼

不過在python里內置了unicode字符類型,這是解決亂碼問題的神器。將str類型的字符串轉換成unicode字符類型就可以了。轉換方法有兩種:

s1 = u"字符串"
s2 = unicode("字符串", "utf-8")

想具體了解unicode和python里的亂碼問題的,可以參考這一篇博客:關于Python的編碼、亂碼以及Unicode的一些研究

下面看修改后的代碼:
注:因為一樓的div的class有兩個,所以將獲取每個樓層結點的代碼改成有contains函數

import urllib2
from lxml import html

url='https://tieba.baidu.com/p/3267113128?see_lz=1&pn=1'#目標url
request=urllib2.Request(url)#封裝請求
response=urllib2.urlopen(request)#打開url資源,得到響應
str_html=response.read()#讀取html文本內容
response.close()#關閉資源

str_html=unicode(str_html,'utf-8')#將string字符類型轉換成unicode字符類型

tree=html.fromstring(str_html)
nodes=tree.xpath('//div[contains(@class,"d_post_content_main")]')#先找的所有的樓層再進一步解析樓層號碼和內容

for node in nodes:
    layer=node.xpath('div[@class="core_reply j_lzl_wrapper"]/div[@class="core_reply_tail clearfix"]/div[@class="post-tail-wrap"]/span[2]')[0].text
    print layer#輸出樓層號
     
    content=node.xpath('div[@class="p_content  "]/cc/div')[0].text
    print content#輸出樓層內容

運行可以看到結果里有一些樓層的輸出內容不完整,這就是第二個坑

樓層不完整

我們返回瀏覽器查看結構,可以發現,原來是這個div里有其他標簽(a和br),不是純文本的。這里就可以用string函數來過濾掉多余的標簽。

在前面的代碼里,輸出樓層的語句換成:

content=node.xpath('div[@class="p_content  "]/cc/div')[0]
content_txt=content.xpath('string(.)')#用string函數過濾掉多余子標簽,.代表本結點
print content_txt#輸出樓層內容

但是br也被過濾掉了,有些樓層有分序號的輸出結果也是一行,看著不方便。我們可以用正則表達式匹配上數字序號,并在之前插入換行符“\n”。re模塊的sub是匹配并替換字符串的方法。python中正則表達式的更多使用可以參考Python正則表達式指南

re_num=re.compile('([0-9]\.)') #匹配所有的數字序號(數字后通常跟"."),加上括號作為分組1,這樣可以進行替換的時候用上
for node in nodes:
    layer=node.xpath('div[@class="core_reply j_lzl_wrapper"]/div[@class="core_reply_tail clearfix"]/div[@class="post-tail-wrap"]/span[2]')[0].text
    print layer#輸出樓層號
     
    content=node.xpath('div[@class="p_content  "]/cc/div')[0]
    content_txt=content.xpath('string(.)')
    content_txt=re.sub(re_num,'\n\1',content_txt) #將目標匹配替換成換行符+本身分組(也就是分組1)

    print content_txt #輸出樓層內容

但輸出結果顯示,換行成功了,數字序號成了亂碼。這里是第三個坑,漏掉了一個小地方。


修改倒數第二句為:

 content_txt=re.sub(re_num,r'\n\1',content_txt)#加上一個字母'r'

這個“r”是代表防止字符轉義,也就是“original”原生字符。關于正則中字符的轉義,有興趣的同學可以google、百度。

接下來我們簡單將代碼寫成面向對象的風格。完整代碼如下:

import urllib2
from lxml import html
import re

class BaiduTieba:
    url_base='https://tieba.baidu.com/p/3267113128?see_lz='
    file = open("../text/bdtb.txt","w+")
    
    def __init__(self,see_lz):#see_lz=1表示只看樓主
        self.url_base=self.url_base+see_lz
    
    def setPage(self,page):
        self.url=self.url_base+'&pn='+page#目標url

    def printPage(self):
        request=urllib2.Request(self.url)#封裝請求
        response=urllib2.urlopen(request)#打開url資源,得到響應
        str_html=response.read()#讀取html文本內容
        response.close()#關閉資源
        
        str_html=unicode(str_html,'utf-8')#將string字符類型轉換成unicode字符類型
        
        tree=html.fromstring(str_html)
        nodes=tree.xpath('//div[contains(@class,"d_post_content_main")]')#先找的所有的樓層再進一步解析樓層號碼和內容
        
        re_num=re.compile('([0-9]\.)')
        for node in nodes:
            layer=node.xpath('div[@class="core_reply j_lzl_wrapper"]/div[@class="core_reply_tail clearfix"]/div[@class="post-tail-wrap"]/span[2]')[0].text
            
            self.file.write("\n")
            self.file.write("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n")
            self.file.write("------------------------------------------------------------------------------------\n")
            self.file.write("                                  "+layer.encode("utf-8")+"                                         \n")
            self.file.write("------------------------------------------------------------------------------------\n")
            content=node.xpath('div[@class="p_content  "]/cc/div')[0]
            content_txt=content.xpath('string(.)')
            content_txt=re.sub(re_num,r'\n\1',content_txt)
        
            self.file.write(content_txt.encode("utf-8"))#輸出樓層內容
            self.file.write("------------------------------------------------------------------------------------\n")


crawl_bdtb=BaiduTieba("1")
for i in range(5):#爬5頁
    crawl_bdtb.setPage(str(i+1))
    crawl_bdtb.printPage()

爬取結果截圖:


爬取結果

參考

Python正則表達式指南

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容