使用Scrapy爬取知乎的問題以及回答

知乎是一個真實的網絡問答社區,社區氛圍友好與理性,連接各行各業的精英。用戶分享著彼此的專業知識、經驗和見解,為中文互聯網源源不斷地提供高質量的信息。
準確地講,知乎更像一個論壇:用戶圍繞著某一感興趣的話題進行相關的討論,同時可以關注興趣一致的人。對于概念性的解釋,網絡百科幾乎涵蓋了你所有的疑問;但是對于發散思維的整合,卻是知乎的一大特色。

為了膜拜“高學歷、高收入、高消費”的大佬們學習,本鶸嘗試用Scrapy模擬登錄并爬取知乎上的問題以及其回答。

模擬登錄

在使用Scrapy模擬登錄之前,有過使用requests模擬登錄的經歷,其中用sessioncookies幫我節約了不少時間。
在使用到Scrapy模擬登錄時,需要使用到Scrapy自己的Request
在模擬登錄的過程中,首先需要修改Scrapy默認的User-Agent,并且向登錄的URL POST所需要的數據。通過查看頁面和chrome開發者工具中的network,可以得到我們需要POST的URL以及數據。

    headers={
    "HOST":"www.zhihu.com",
    "Referer":"https://www.zhihu.com",
    "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0"
    }

Scrapy默認的User-Agent是無法爬取知乎這類有一定反爬蟲的網站的,所以我們需要添加自己的headers

既然要模擬登錄,需要向登錄頁面POST的數據肯定是不能少的。

import re
account = input("請輸入賬號\n--->")
password = input("請輸入密碼\n--->")
_xsrf = response.xpath('/html/body/div[1]/div/div[2]/div[2]/form/input/@value').extract_first()
if re.match("^1\d{10}", account):
    print("手機號碼登錄")
    post_url = "https://www.zhihu.com/login/phone_num"
    post_data = {
        "_xsrf": _xsrf,
        "phone_num": account,
        "password": password,
        "captcha":""
        }
else:
    if "@" in account:
        # 判斷用戶名是否為郵箱
        print("郵箱方式登錄")
        post_url = "https://www.zhihu.com/login/email"
        post_data = {
        "_xsrf": _xsrf,
        "email": account,
        "password": password,
        "captcha":""
        }

通過正則表達式判斷你輸入的賬號是手機號還是email。知乎對賬號登錄POST的地址會根據手機或email會有不同。

  • _xsrf是藏在登錄頁面中的一組隨機密鑰,可以使用正則或者Scrapy自己的XPath或者CSS選擇器從頁面提取出來
  • captcha就是驗證碼了。在登錄時知乎會要求輸入驗證碼。
    具體模擬登錄源碼如下:
import scrapy
import re
from PIL import Image
import json
from urllib import parse
class ZhihuSpider(scrapy.Spider):
    name = "zhihu"
    allowed_domains=["www.zhihu.com"]
    start_urls = ['https://www.zhihu.com/explore']
    headers={
    "HOST":"www.zhihu.com",
    "Referer":"https://www.zhihu.com",
    "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0",
    }

    def parse(self, response):
          pass

    def start_requests(self):
        #因為要登錄后才能查看知乎,所以要重寫入口

        return [scrapy.Request("https://www.zhihu.com/#signin",headers=self.headers,callback=self.login)]

    def login(self,response):

        _xsrf = response.xpath('/html/body/div[1]/div/div[2]/div[2]/form/input/@value').extract_first()
        account = input("請輸入賬號\n--->")
        password = input("請輸入密碼\n--->")
        if re.match("^1\d{10}", account):
            print("手機號碼登錄")
            post_url = "https://www.zhihu.com/login/phone_num"
            post_data = {
                "_xsrf": _xsrf,
                "phone_num": account,
                "password": password,
                "captcha":""
            }
        else:
            if "@" in account:
                # 判斷用戶名是否為郵箱
                print("郵箱方式登錄")
                post_url = "https://www.zhihu.com/login/email"
                post_data = {
                    "_xsrf": _xsrf,
                    "email": account,
                    "password": password,
                    "captcha":""
                }

        return [scrapy.FormRequest(
                url=post_url,
                formdata=post_data,
                headers=self.headers,
                meta={"post_data": post_data,
                      "post_url": post_url,
                      },
                callback=self.check_login
            )]
    def login_after_captcha(self,response):
        #獲取驗證碼
        print(response.headers)
        post_data = response.meta.get("post_data","")
        post_url = response.meta.get("post_url","")
        with open('captcha.gif', 'wb') as f:
            f.write(response.body)
        try:
            im = Image.open("captcha.gif")
            im.show()
            captcha = input("please input the captcha:")
            post_data["captcha"] = captcha
        except:
            print("未打開驗證碼文件")
        return [scrapy.FormRequest(
            url=post_url,
            formdata=post_data,
            headers=self.headers,
            callback=self.check_login,
        )]
    def check_login(self,response):
        response_text = json.loads(response.body)
        if response_text["r"] == 0:
            headers = response.headers
            cookie = dict(headers)[b'Set-Cookie']
            cookie = [str(c, encoding="utf-8") for c in cookie]
            cookies = ";".join(cookie)
            #登錄成功后才開始使用start_urls
            for url in self.start_urls:
                yield scrapy.Request(url,headers=self.headers,dont_filter=True)
        else:
            captcha_url = "https://www.zhihu.com/captcha.gif?&type=login"
            #因為scrapy是一個異步框架,所以為了保證驗證碼在同一個session下,就將這個request yield出去
            yield scrapy.Request(url=captcha_url,
                                     headers=self.headers,
                                     meta={"post_data":response.meta.get("post_data"),
                                           "post_url":response.meta.get("post_url"),
                                           },
                                     callback=self.login_after_captcha)
登錄后,整個知乎就在你眼前了。

數據的爬取

如何遍歷一個網站的所有我們需要的網頁?這是一個很麻煩的問題,一般會選擇深度優先遍歷(DFS)或者廣度優先遍歷(BFS)。我試著利用Scrapy的異步機制,用DFS一直跟蹤、下載我所能接觸到的URL,這樣總會將所有我需要的URL遍歷一次。

    def parse(self, response):
        """
        提取出check_login中yield中的URL即為我提取知乎URL的一個入口
        將其中所有的URL中類似/question/xxxx的URL提取出來,然后下載后放入解析函數
        :param response:
        :return:
        """
        all_urls = response.css("a::attr(href)").extract()
        all_urls = [parse.urljoin(response.url, url) for url in all_urls]
        all_urls = filter(lambda x:True if x.startswith("https") else False,all_urls)
        for url in all_urls:
            print(url)
            match_obj = re.match("(.*zhihu.com/question/(\d+))(/|$).*",url)
            #如果提取到question的URL則進行下載
            if match_obj:
                request_url = match_obj.group(1)
                question_id = match_obj.group(2)
                yield scrapy.Request(request_url,
                                     headers=self.headers,
                                     meta={"question_id":question_id},
                                     callback=self.parse_question)
            # 如果提取到的不是question的URL,則進行跟蹤
            else:               
                yield scrapy.Request(url,headers=self.headers,callback=self.parse)

這樣的找尋URL的邏輯在question頁面也可以使用。將找到的形如/question/...的URL交給專門處理question頁面的函數進行處理。

from ..items import ZhihuAnswerItem
    def parse_question(self,response):
        """
        處理question頁面,從頁面中取出我們需要的item
        :param response:
        :return:
        """
        question_id = response.meta.get("question_id")
        if "QuestionHeader-title" in response.text:
            #知乎的新版本
            item_loader = ItemLoader(item=ZhihuQuestionItem(),response=response)
            item_loader.add_css("title",".QuestionHeader-main .QuestionHeader-title::text")
            item_loader.add_css("topics",".TopicLink .Popover div::text")
            item_loader.add_css("content",".QuestionHeader-detail")
            item_loader.add_value("url",response.url)
            item_loader.add_value("zhihu_id",int(response.meta.get("question_id","")))
            item_loader.add_css("answer_num",".List-headerText span::text")
            item_loader.add_css("watch_user_num",'.NumberBoard-value::text')
            item_loader.add_css("click_num",'.NumberBoard-value::text')
            item_loader.add_css("comments_num",'.QuestionHeader-Comment button::text')

            QuestionItem = item_loader.load_item()
            #請求該問題的回答,這個URL會在后面給出。
            yield scrapy.Request(self.start_answer_urls.format(question_id,20,0),headers=self.headers,callback=self.parse_answer)
            yield QuestionItem
            #在question頁面中找question的URL.可有可無,主要是上面提取數據的邏輯
            all_urls = response.css("a::attr(href)").extract()
            all_urls = [parse.urljoin(response.url, url) for url in all_urls]
            all_urls = filter(lambda x: True if x.startswith("https") else False, all_urls)
            for url in all_urls:
                print(url)
                match_obj = re.match("(.*zhihu.com/question/(\d+))(/|$).*", url)
                # 如果提取到question的URL則進行下載
                if match_obj:
                    request_url = match_obj.group(1)
                    question_id = match_obj.group(2)
                    yield scrapy.Request(request_url,
                                         headers=self.headers,
                                         meta={"question_id": question_id},
                                         callback=self.parse_question)
                # 如果提取到的不是question的URL,則進行跟蹤
                else:
                    # pass
                    yield scrapy.Request(url, headers=self.headers, callback=self.parse)

        else:
            #知乎的老版本

            pass

知乎為我們開放了獲取回答的一個公共信息的API。

點擊之后,給我們展示的是一個json


里面會給我們很多有用的信息,比如paging里面的

  • is_end是判斷該頁的回答是否是該問題最后的回答
  • totals是顯示該問題所有的回答
  • next是爬取知乎回答最重要的一個數據。它算是我們爬取知乎問題的一個入口,它有三個重要的數據,question/xxxxxx/....表明我們可以通過question_id來找到該問題的回答;limit即為每頁回答的數量;offset是偏移量,表示頁面回答在所有回答中偏移位置。

后面的數據中可以看到許多我們需要的數據。(我隨便開的一個json,不小心截圖到誰了請找我。)

class ZhihuSpider(scrapy.Spider):
        ....
    #answer第一頁的請求URL
    start_answer_urls = "http://www.zhihu.com/api/v4/questions/{0}/answers?" \
                        "sort_by=default&include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2" \
                        "Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2" \
                        "Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2" \
                        "Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2" \
                        "Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2" \
                        "Cupvoted_followees%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%" \
                        "5B%3F%28type%3Dbest_answerer%29%5D.topics&limit={1}&offset={2}"
    def parse_answer(self,response):
        answer_json = json.loads(response.text)
        is_end = answer_json["paging"]["is_end"]
        total_anwsers = answer_json["paging"]["totals"]
        next_url = answer_json["paging"]["next"]
        AnswerItem = ZhihuAnswerItem()
        #提取answer的結構
        for answer in answer_json.get("data"):
            AnswerItem["zhihu_id"] = answer["id"]
            AnswerItem["url"] = answer["url"]
            AnswerItem["question_id"] = answer["question"]["id"]
            AnswerItem["author_id"] = answer["author"]["id"] if "id" in answer["author"] and answer["author"]["id"] is not "0" else None
            AnswerItem["author_name"] = answer["author"]["name"] if "id" in answer["author"] and  answer["author"]["id"] is not "0" else "匿名用戶"
            AnswerItem["content"] = answer["content"] if "content" in answer else None
            AnswerItem["praise_num"] = answer["voteup_count"]
            AnswerItem["comments_num"] = answer["comment_count"]
            AnswerItem["update_time"] = answer["updated_time"]
            AnswerItem["create_time"] = answer["created_time"]
            AnswerItem["crawl_time"] = datetime.datetime.now()
            yield AnswerItem
        if not is_end:
            yield scrapy.Request(next_url,headers=self.headers,callback=self.parse_answer)
這樣一個簡單的Scrapy爬取知乎問題以及回答的爬蟲就寫好了。理論上可以爬取所有的頁面,具體的嘗試需要等到我把pipeline和數據的處理存儲弄好后找臺服務器試一下。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容