Scrapy爬取知乎兩種思路

介紹兩種直接爬取知乎的方法,一種是通過CrawlSpider類,從Question頁面開始,通過Rule自動填充帶爬取頁面;第二種是登錄知乎首頁之後,通過模擬js下拉頁面發送ajax請求解析返回json數據填充原有的zhihu首頁爬取。實際上還有另外一種,我們使用scrapy splash模塊或者selenium工具模擬js操作,這種方法相比第二種方法更直接、簡便,之後的文章會有介紹。

爲了更好的閱讀體驗,可以移步到我的小站 peihao.space

在做這些之前,首先你要設置好item的feed以及通用的header。這些在我其他的一些文章中有提到過

爬取question頁面

知乎的問題頁面是不需要登錄就可以直接爬取的,有次我嘗試登錄失敗後,返回了錯誤代碼,但是發現這並未影響繼續對問題頁面的爬蟲。

爲題頁面的url一般類似/question/\d{8},收尾是8個數字,問題頁面會隨機出現與本問題有類似、相關共性的其他問題url,這就讓我們使用CrawlSpider類有了可能。關於CrawlSpider的前置學習,請參考:Scrapy筆記

通過使用CrawlSpider類支持的rules,制定自動提取的問題界面url規則:

rules = (Rule(LinkExtractor(allow = [r'/question/\d{8}$',r'https://www.zhihu.com/question/\d{8}$' ]), callback = 'parse_item', follow = True))

上面的規則允許提取符合爲題界面的absolute url或者relative url填充到爬蟲隊列中,並設置了請求這些url之後response的處理方法,parse_item(self,response),這裏要再說一次,callback直接寫parse_item,不要畫蛇添足self.parse。

設置一個你感興趣的問題,放入到start_urls列表中,Scrapy會自動在make_requests_from_url方法執行時搜索符合規則的url,當我們執行scrapy crawl spidernamestart_request後,程序開始調用start_requests方法,方法將start_urls中的url分別調用make_requests_from_url結束。如果我們不是直接就開始對start_url中的url進行解析,可以重寫start_requests,並指定requests的回調函數。

做完這些之後,我們就可以僅僅處理item解析的相關操作,不用管獲取新的url內容,至於爬取的爲題是不是我們感興趣的,就要看知乎的推薦算法怎麼養了。

from scrapy.selector import Selector
from scrapy.http import Request
from douban.items import ZhihuItem
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class ZhihuSipder(CrawlSpider) :
    name = "zhihu"
    allowed_domains = ["zhihu.com"]
    start_urls = [
        "https://www.zhihu.com/question/41472220"
    ]
    rules = (Rule(LinkExtractor(allow = [r'/question/\d{8}$',r'https://www.zhihu.com/question/\d{8}$' ]), callback = 'parse_item', follow = True),)
    headers = {
    "Accept": "*/*",
    "Accept-Encoding": "gzip,deflate",
    "Accept-Language": "en-US,en;q=0.8,zh-TW;q=0.6,zh;q=0.4",
    "Connection": "keep-alive",
    "Content-Type":" application/x-www-form-urlencoded; charset=UTF-8",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36",
    "Referer": "http://www.zhihu.com/"
    }


    def parse_item(self, response):
        problem = Selector(response)
        item = ZhihuItem()
        item['url'] = response.url
        item['name'] = problem.xpath('//span[@class="name"]/text()').extract()
        item['title'] = problem.xpath('//span[@class="zm-editable-content"]/text()').extract()
        item['description'] = problem.xpath('//div[@class="zm-editable-content"]/text()').extract()
        item['answer']= problem.xpath('//div[@class="zm-editable-content clearfix"]/text()').extract()
        return item

登陸模擬ajax爬取主頁

這種方法對於我們來講可能更加準確,因爲他是在zhihu.com主頁顯示的我們關注過的問題新高票答案,以及關注的領域專欄專欄文章。

由於是一直在www.zhihu.com頁面是操作,沒有爬取其他頁面,所以在使用CrawlSpider類就不太合適了。直接使用原始的scrapy.spider類。

關於知乎的登錄,可以參考文章簡書-模擬登陸

當然上面文章因爲時間較早,zhihu的登錄模塊已經改了一些東西,但是具體的思路和流程還是可以繼續用的。

#重寫 start_requests方法,登錄登錄面,注意登錄界面與舊版不同
def start_requests(self):
    return [Request("https://www.zhihu.com/#signin", meta = {'cookiejar' : 1}, callback = self.post_login)]

#處理知乎防爬蟲_xsrf字段 ,使用formRequest發送post信息,注意form的提交url與舊版的不同
def post_login(self, response):
    print('Preparing login')
    #下面這句話用於抓取請求網頁後返回網頁中的_xsrf字段的文字, 用於成功提交表單
    self.xsrf = Selector(response).xpath('//input[@name="_xsrf"]/@value').extract()[0]
    #FormRequeset.from_response是Scrapy提供的一個函數, 用於post表單
    #登陸成功後, 會調用after_login回調函數
    return [FormRequest(url="https://www.zhihu.com/login/email",
                        meta = {'cookiejar' : response.meta['cookiejar']},
                        formdata = {
                        '_xsrf': self.xsrf,
                        'email': '******',
                        'password': '******',
                        'remember_me': 'true'
                        },
                        headers=self.headers,
                        callback = self.after_login,
                        dont_filter = True
                        )]

def after_login(self, response):
    #這裏注意在header中加入兩個字段
    self.headers['X-Xsrftoken']=self.xsrf
    self.headers['X-Requested-With']="XMLHttpRequest"
    return Request('https://www.zhihu.com',meta = {'cookiejar' : response.meta['cookiejar']}, callback =self.parse,dont_filter = True)

此時我們可以抓取zhihu首次返回的大概10個問題信息,更多的信息需要下拉鼠標ajax發送請求才會返回。

我們通過珠寶工具找到相應的post信息,模擬操作:

FormRequest(url="https://www.zhihu.com/node/TopStory2FeedList",
                            meta = {'cookiejar' : response.meta['cookiejar']},
                            formdata = {
                            'params': '{"offset":%d,"start":%s}' % (self.count*10,str(self.count*10-1)),
                            'method': 'next',
                            },
                            headers=self.headers,
                            callback = self.parse,
                            dont_filter = True
                            )

依然通過FormRequest提交Post信息,注意formdata的params鍵值爲dict,要加上括號,原因可以在前一篇文章中找到答案。offset是獲取的問題數目,start是新獲取問題的index

剛開始請求時一直提示Bad Request信息,最後發現一定要在header中設置之前獲取的_xsrf。

服務器端根據我們的params字段返回不同的bytes形式json數據,我們需要拿到裏面的摸個鍵值,所以要將bytes解碼爲utf-8,拿到之後在編碼爲utf-8,因爲response只處理bytes形式。

最後,因爲response使用xpath方法是對response.text屬性操作,當我們獲取新的response之後就無法再改變response.text(在python中使用@property標註的屬性無法更改)但是我們可以通過response內置的response._set_body()方法修改response的body值,不能使用內置的xpath方法,就改用原始的lxml模塊提供的xpath工具,畢竟scrapy內置的xpath底層也是使用lxml模塊提供支持,大同小異。其他的一些問題就比較小了,不在這裏一一講:

from scrapy.http import Request, FormRequest
from douban.items import ZhihuAnswerItem
import scrapy,json,re
from lxml import etree

class ZhihuSipder(scrapy.Spider) :
    name = "zhihuanswer"
    #allowed_domains = ["zhihu.com"]
    start_urls = [
        "https://www.zhihu.com"
    ]
    headers = {
    ......
    }

    xsrf=""
    moreinfo=False
    count=0

    def start_requests(self):
        return [Request("https://www.zhihu.com/#signin", meta = {'cookiejar' : 1}, callback = self.post_login)]


    def post_login(self, response):
        print('Preparing login')
        self.xsrf = Selector(response).xpath('//input[@name="_xsrf"]/@value').extract()[0]
        print(self.xsrf)
        #FormRequeset.from_response是Scrapy提供的一個函數, 用於post表單
        #登陸成功後, 會調用after_login回調函數
        return [FormRequest(url="https://www.zhihu.com/login/email",
                            meta = {'cookiejar' : response.meta['cookiejar']},
                            formdata = {
                            '_xsrf': self.xsrf,
                            'email': '******',
                            'password': '******',
                            'remember_me': 'true'
                            },
                            headers=self.headers,
                            callback = self.after_login,
                            dont_filter = True
                            )]

    def after_login(self, response):
        print(response.body)
        self.headers['X-Xsrftoken']=self.xsrf
        self.headers['X-Requested-With']="XMLHttpRequest"
        return Request('https://www.zhihu.com',meta = {'cookiejar' : response.meta['cookiejar']}, callback =self.parse,dont_filter = True)

    def parse(self, response):
        if self.moreinfo:
            #使用模擬ajax發送post請求接受的response  編碼抓換解析相關  
            f=json.loads(response.body.decode('utf-8'))
            for item in range(1,len(f['msg'])-1):
                f['msg'][0]=f['msg'][0]+f['msg'][item]
            fs=f['msg'][0].encode('utf-8')
            response._set_body(fs)
        else:
            self.moreinfo=True
        html=etree.HTML(response.body.decode('utf-8'),parser=etree.HTMLParser(encoding='utf-8'))
        for p in html.xpath('//div[@class="feed-main"]'):
            url=p.xpath('div[2]/h2[@class="feed-title"]/a/@href')
            if url:url=url[0]
            # 通過決定是否使用if not re.findall('zhuanlan',url) 接受專欄文章或者問題回答,或者刪掉這部分,全部接受
            if re.findall('zhuanlan',url):continue
            item = ZhihuAnswerItem()
            item['url'] = url
            name=p.xpath('div[2]/div[@class="expandable entry-body"]/div[@class="zm-item-answer-author-info"]/span/span/a/text()')
            item['name'] = name if name else '匿名用戶'
            item['question'] = p.xpath('div[2]/h2[@class="feed-title"]/a/text()')
            item['answer']= p.xpath('div[2]/div[@class="expandable entry-body"]/div[@class="zm-item-rich-text expandable js-collapse-body"]/div[@class="zh-summary summary clearfix"]/text()')
            yield item

        #維持不同的params
        self.count=self.count+1
        yield FormRequest(url="https://www.zhihu.com/node/TopStory2FeedList",
                            meta = {'cookiejar' : response.meta['cookiejar']},
                            formdata = {
                            'params': '{"offset":%d,"start":%s}' % (self.count*10,str(self.count*10-1)),
                            'method': 'next',
                            },
                            headers=self.headers,
                            callback = self.parse,
                            dont_filter = True
                            )
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章