介紹兩種直接爬取知乎的方法,一種是通過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
)