知乎是一個真實的網絡問答社區,社區氛圍友好與理性,連接各行各業的精英。用戶分享著彼此的專業知識、經驗和見解,為中文互聯網源源不斷地提供高質量的信息。
準確地講,知乎更像一個論壇:用戶圍繞著某一感興趣的話題進行相關的討論,同時可以關注興趣一致的人。對于概念性的解釋,網絡百科幾乎涵蓋了你所有的疑問;但是對于發散思維的整合,卻是知乎的一大特色。
為了膜拜“高學歷、高收入、高消費”的大佬們學習,本鶸嘗試用Scrapy
模擬登錄并爬取知乎上的問題以及其回答。
模擬登錄
在使用Scrapy
模擬登錄之前,有過使用requests
模擬登錄的經歷,其中用session
和cookies
幫我節約了不少時間。
在使用到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)