python scrapy 最新詳解 爬取知乎

ps:歡迎來我的博客:http://www.dwlufvexyu.com

廢話不多說,直接入正題。

關於模擬登錄,另一篇再講解(這篇寫太多了) 我們先來分析好頁面。

首先打開知乎,點擊進入首頁的隨便一個問答可以看到url是這樣的👇

在這裏插入圖片描述
觀察url可以發現question有一個id,answer也同樣有
在這裏插入圖片描述

退出來隨便再點一個問答,驗證我們的想法

答案顯而易見

但一篇問答,總不會只有一個答案answer是吧,一個問題有很多答案,那麼這個url顯示的answer_id是什麼呢?我們猜想可能是看到的第一個回答(應該沒錯是高贊,但其實不是)如何來驗證我們的想法,同樣的進行檢查。
在這裏插入圖片描述

可以看到一個itemID。和上面url所顯示的answer_id是一樣的。

證實了answer_id;另外可以看到第二個回答,也有一個itemID。
在這裏插入圖片描述

設想一下如果他的贊超過了前者,那麼url是否會變化?正當我試着去尋找兩個贊差距不大(主要是第一名第二名)的回答的時候,發現了一個不一樣的結果,其實並不是單獨地贊高就會把你放在第一設爲answer_id,看下方兩張圖👇,哪個纔是最終贏家呢?事實上,是後者
在這裏插入圖片描述

贊多,並不一定排第一。 那難道是評論多優先?那再仔細地去尋找幾篇,會發現這樣的情況:第二(評論少)第三(評論多)。 那麼猜想一下是否是一個公式來計算熱度呢? 好了點到爲止,感興趣的可以百度搜一下。 (應該是有的,我也還沒百度)

以上都是“檢查”時發現的一些東西。前面主要是講answer_id的由來。接下來是主要的。我們點擊查看全部回答;並且可以發現每一個問答,初始都一定會有3個回答。可自行查看,關於這個問題後面會講到。

在這裏插入圖片描述

點擊後,結果是這樣的:
在這裏插入圖片描述

並且如果你夠細心那麼你會發現url變化了(其實我也是後面才發現)

在這裏插入圖片描述

往下翻能看到全部的答案 但是答案並不是直接靜態就顯示出來的而是隨着你頁面的下滑,可以看到左下角的數據是一直在變化的。

在這裏插入圖片描述

如果想直接這樣爬是不方便的。我們可以看到之前點擊“查看全部”可以看到有新的請求。Filter過濾一下,輸入一些關鍵字(如answer、answer_id)查看請求的Response。可以看到如下👇

此圖像的alt屬性爲空
我們看到了api接口,還有question_id,offset=3(可以和之前講的只有三個回答對應了),limit=5,幾個關鍵的數據,點擊response,可以清晰地看到數據。

在這裏插入圖片描述

雙擊跳轉頁面。裝一個JSONview插件,更好的查看json數據。這裏我先把data合併了,我們先分析下方的paging。

此圖像的alt屬性爲空;文件名爲image-47-1024x348.png

拖到next及previous最後面,可以看到

此圖像的alt屬性爲空;文件名爲image-48.png

3-5=-2->0最小爲0,我們修改url offset=0,那麼當然 is_start:true;那麼同理,offset>=319(因爲從0開始的,最後一個是318)時,那麼is_end一定也爲true.

此圖像的alt屬性爲空;文件名爲image-50.png

接下來我們看limit=5這一項,我們查看data,下面有5個項。關於數據我們後面再進行分析。
在這裏插入圖片描述

這裏修改limit=10,那麼就會data下面的項肯定會變化。這裏就自行去嘗試了。

接下來分析數據,下面是關於question的一些摘要,這個接口並沒有把question的所有內容搞下來,不然後面對於問題的爬取將變得十分簡單。

lt屬性爲空;文件名爲image-51.png

回答者的信息

此圖像的alt屬性爲空;文件名爲image-53.png

此圖像的alt屬性爲空;文件名爲image-55.png

最後就是回答者的content了,這裏不貼圖了,這個人回答太多了😂。

關於answer的分析就到這裏。接下來就是關於question的問題。question就相對於更簡單了。

------------------------分割線------------------------

下面來分析問答的 “問” ,其實也就這些東西

在這裏插入圖片描述

所屬專欄,標題,question_id, 還有它的描述內容(有些是爲空)、還有評論數、關注者、被瀏覽數。這裏看不到創建時間和更新時間,前面在分析answer的時候我們看到了。這裏看你心情,想爬就爬,因爲具體看不懂這個時間,只知道這個數字大小,小的更前,所以其實前面answer的time其實也可以沒有爬取的必要。

在這裏插入圖片描述

我們檢查一下這些元素,首先是title

此圖像的alt屬性爲空;文件名爲image-58-1024x187.png

上下箭頭,可以看到 “1 of 2” 的 “ 1 ”是在👇

此圖像的alt屬性爲空;文件名爲image-59.png

刷新一下,正常情況是沒有的。
在這裏插入圖片描述

那麼什麼時候有呢,當你的鼠標滾輪下滑時,就可以看到出現了這個標題。上滑就會消失。如下圖。

此圖像的alt屬性爲空;文件名爲image-61-1024x322.png

好的其實管他無所謂是不是唯一,能取得出來標題就行。放入xpath_helper

此圖像的alt屬性爲空;文件名爲image-62-1024x98.png

出現兩個值,但其實真正在爬取時,只有一個值。我們來看看scrapy shell 記得加上 -s USER_AGENT模擬瀏覽器登錄,不然會被反爬。後面寫代碼也是必帶的。

此圖像的alt屬性爲空;文件名爲image-63-1024x80.png

內容呢如下,關於顯示全部,後面的內容如何提取呢?
在這裏插入圖片描述

其實一樣的scrapy shell調試可以看到 是直接就能全部抓取到的。
在這裏插入圖片描述
而最開始我通過檢查頁面並不能獲取到全部。可以看到多了一些內容。
在這裏插入圖片描述

除此之外,關鍵是在 // ,之前寫我只寫了一根斜線,返回一直爲空列表。然後我就一直百度,最後沒解決換成了css就好了 ,結果時自己太憨了。 因爲不是直接在div的下一個子結點上,所以需要注意!!!
此圖像的alt屬性爲空;文件名爲image-64-1024x98.png

此圖像的alt屬性爲空;文件名爲image-65-1024x72.png

最後一個需要注意的是 關注者與被瀏覽數(其他的可以自己試試)

此圖像的alt屬性爲空;文件名爲image-66-1024x94.png
關注者

此圖像的alt屬性爲空;文件名爲image-67-1024x82.png
被瀏覽數

這兩個是一樣的~ 會爬到五個數據(具體五個可以自行嘗試,按下箭頭,其實另外三個就是第一個answer的作者相關信息),但在實際中其實只爬取的到兩個 。

此圖像的alt屬性爲空;文件名爲image-68-1024x102.png

同樣的看scrapy shell,無論是css還是xpath只爬取到兩個數據。

此圖像的alt屬性爲空;文件名爲image-69-1024x151.png

好,問題的“問”也解決了

關於驗證碼識別,模擬登錄在另一篇補全了。

接下來直接上🐎(架 架 架~~~) 關於模擬登錄在另一篇進行更新

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">
import json

import scrapy
import re
from selenium import webdriver
import pickle
from urllib import parse
from scrapy.loader import ItemLoader
from douban.items import ZhihuAnswerItem,ZhihuQuestionItem
import datetime

from mouse import move,click

import time

from selenium.webdriver.common.keys import Keys


class ZhihuSpider(scrapy.Spider):
    name = 'zhihu'
    allowed_domains = ['www.zhihu.com']
    start_urls = ['https://www.zhihu.com/']
    #這裏是構造api接口的url放在了開頭
    start_answer_url="https://www.zhihu.com/api/v4/questions/{0}/answers?include=data%5B*%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%2Cis_recognized%2Cpaid_info%2Cpaid_info_content%3Bdata%5B*%5D.mark_infos%5B*%5D.url%3Bdata%5B*%5D.author.follower_count%2Cbadge%5B*%5D.topics&amp;offset={1}&amp;limit={2}&amp;sort_by=default&amp;platform=desktop"

    headers = {
        # "HOST": "www.zhihu.com",
        # "Referer": "https://www.zhizhu.com",
        #User-Agent必不可少
        'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"
    }
    def parse(self, response):
        #xpath檢測知乎標題 取出所有a標籤的href_url
        all_urls = response.xpath("//a/@href").extract()
        #接下來遍歷list類型的all_urls
        
        #![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200315181224106.png)
        
        #因爲如上圖,這裏用parse的方法將兩個url組合起來,scrapy裏常用 
        all_urls = [parse.urljoin(response.url, url) for url in all_urls]
        for url in all_urls:
            # print(url)
           #正則匹配 我們只需要匹配不帶answer_id的url,前文分析過了,相當於是顯示全部答案。
            match_obj=re.match("(.*zhihu.com/question/(\d+)).*",url)
            if match_obj:
                request_url=match_obj.group(1)
                # scrapy的經典用法callback,也非常常用
                yield scrapy.Request(request_url,headers=self.headers,callback=self.parse_question)
                # break
            #這裏,如果不匹配,則繼續進一步跟着url,尋找匹配的url(深度處理的思想)
            else:
                yield scrapy.Request(url,headers=self.headers,callback=self.parse)
                # print(request_url,question_id)
    

  #問答的問的解析,這裏用的css比xpath更簡潔。(兩者都需要會)
    def parse_question(self,response):
        match_obj = re.match("(.*zhihu.com/question/(\d+)).*", response.url)
        if match_obj:
            question_id = int(match_obj.group(2))
        item_loader=ItemLoader(item=ZhihuQuestionItem(),response=response)
        #print(response)
        #標題
        item_loader.add_css("title", 'h1.QuestionHeader-title::text')
        #內容
        item_loader.add_css('content', '.QuestionHeader-detail')
        #直接add_value賦值
        item_loader.add_value('url', response.url)
        #同樣也是
        item_loader.add_value('zhihu_id', question_id)
        #記得span
        item_loader.add_css('answer_num', '.List-headerText span::text')
        #記得button
        item_loader.add_css('comments_num', '.QuestionHeader-Comment button::text')
        #所屬專欄
        item_loader.add_css('topics', '.QuestionHeader-topics .Popover div::text')
        #關注者,及瀏覽數,不用寫兩個,因爲是列表,取[0][1]就可以了
        item_loader.add_css('watch_user_num', '.NumberBoard-itemValue::text')

        question_item = item_loader.load_item()
        #這裏格式化之前的api接口url
        yield scrapy.Request(self.start_answer_url.format(question_id, 0, 20), headers=self.headers, callback=self.parse_answer)
        yield question_item

        #問答的答解析
    def parse_answer(self,response):
       # json格式處理將字符串變成字典 詳情百度
        ans_json = json.loads(response.text)
        
        #一步一步地取細心一點,還是很簡單的
        is_end = ans_json["paging"]["is_end"]
        next_url = ans_json["paging"]["next"]
        totals_num=ans_json["paging"]["totals"]
        #遍歷["data"],提取具體字段, 可對照查看具體json數據
        for answer in ans_json["data"]:
            answer_item = ZhihuAnswerItem()
            answer_item["zhihu_id"] = answer["id"]
            answer_item["url"] = answer["url"]
            answer_item["question_id"] = answer["question"]["id"]
            answer_item["author_id"] = answer["author"]["id"] if "id" in answer["author"] else None
            answer_item["content"] = answer["content"] if "content" in answer else None
            answer_item["praise_num"] = answer["voteup_count"]
            answer_item["comments_num"] = answer["comment_count"]
            answer_item["create_time"] = answer["created_time"]
            answer_item["update_time"] = answer["updated_time"]
            answer_item["crawl_time"] = datetime.datetime.now()

            yield answer_item
        #這裏是如果沒有結束,則繼續調用answer爬取。
        if not is_end:
            yield scrapy.Request(next_url,headers=self.headers,callback=self.parse_answer)






   #這裏是調用事先準備好的cookie
   #這裏自己改一下自己的參數就行了,可以百度到
    def start_requests(self):


        cookies=pickle.load(open("D:/Pythonstudy/douban/douban/spiders/cookies/zhihu_cookies","rb"))#cookie存放位置
        cookie_dict = {}

        for cookie in cookies:
            cookie_dict[cookie['name']] = cookie['value']
                     
        return [scrapy.Request(url=self.start_urls[0], dont_filter=True, cookies=cookie_dict)]<strong>#這裏start_urls是列表記得[0]

items.py

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"> import re
import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst,MapCompose
import datetime
from douban.settings import SQL_DATE_FORMAT,SQL_DATETIME_FORMAT

class DoubanItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass

class doubanItemLoader(ItemLoader):
    default_output_processor = TakeFirst()

def RemoveFormatter(value):
    #去除如\n \xa0 \u3000等格式符
    Need_remove="".join(value.split())
    return Need_remove
def date_convert(value):
    #date轉換
    try:
        Time =datetime.datetime.strptime(value, "%Y").date()
    except Exception as e:
        Time = datetime.datetime.now().date()
    return Time

def get_nums(value):
    #正則取數字
    rex_str = ".*?(\d+).*"
    match_obj = re.match(rex_str, value)
    if match_obj:
        value = match_obj.group(1)
    return value

def get_time(value):
    rex_str = ".*?(\d+).*"
    match_obj = re.match(rex_str, value)
    if match_obj:
        value = match_obj.group(1)
    return value
def Remove_nums(value):
    rex_str = "\d+/(.*)"
    match_obj = re.match(rex_str, value)
    if match_obj:
        value=match_obj.group(1)
    return value

def return_value(value):
    return value


def get_Madein(value):
    value=list(value)
    # result = ""
    # for i in range(89, 91):
    #     result += value[i]
    # return result
    return str(value[89] + value[90])

# def get_lauguage(value):
#     lau=value[0]
#     return lau
def add_Num(value):
    return "No."+value

class ZhihuQuestionItem(scrapy.Item):
    zhihu_id=scrapy.Field()
    topics=scrapy.Field()
    url=scrapy.Field()
    title=scrapy.Field()
    content=scrapy.Field()
    answer_num=scrapy.Field()
    comments_num=scrapy.Field()
    watch_user_num=scrapy.Field()
    click_num=scrapy.Field()
    crawl_time=scrapy.Field()
    

    def get_insert_sql(self):
       #因爲爬取可能會造成數據重複,所以採用更新的手段,防止報錯
        insert_sql="""
            insert into zhihu_question(zhihu_id,topics,url,title,content,answer_num,comments_num,
            watch_user_num,click_num,crawl_time
            )
            VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
           <strong>ON DUPLICATE KEY UPDATE content=VALUES(content), answer_num=VALUES(answer_num), comments_num=VALUES(comments_num),
              watch_user_num=VALUES(watch_user_num), click_num=VALUES(click_num)</strong>
        """
        zhihu_id=self["zhihu_id"][0]
        topics=",".join(self["topics"])
        url=self["url"][0]
        title="".join(self["title"])
        content="".join(self["content"])
        answer_num=get_nums("".join(self["answer_num"]).replace(",", ""))
        comments_num=get_nums("".join(self["comments_num"]))
       #有坑,這裏因爲假如數字是1,351,那麼直接int會什麼都沒有,所以需要replace去掉",",這裏當時確實坑到我了
        watch_user_num =int(self["watch_user_num"][0].replace(",", ""))
        click_num = int(self["watch_user_num"][1].replace(",", ""))
       # SQL_DATETIME_FORMAT= 這個可以在setting裏設置,因爲經常用
        #爬取時間,取now()就行了
     crawl_time=datetime.datetime.now().strftime(SQL_DATETIME_FORMAT)

        params = (zhihu_id, topics, url, title, content, answer_num, comments_num,
              watch_user_num, click_num, crawl_time)
        return insert_sql, params #記得返回

class ZhihuAnswerItem(scrapy.Item):
    zhihu_id=scrapy.Field()
    url=scrapy.Field()
    question_id=scrapy.Field()
    author_id=scrapy.Field()
    content=scrapy.Field()
    praise_num=scrapy.Field()
    comments_num=scrapy.Field()
    create_time=scrapy.Field()
    update_time=scrapy.Field()
    crawl_time=scrapy.Field()

    def get_insert_sql(self):
        insert_sql = """
               insert into zhihu_answer(zhihu_id,url,question_id,author_id,content,praise_num,comments_num,
               create_time,update_time,crawl_time
               ) 
               VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
               <strong>ON DUPLICATE KEY UPDATE content=VALUES(content), comments_num=VALUES(comments_num), praise_num=VALUES(praise_num),
              update_time=VALUES(update_time)</strong>
           """
       **#將int_time變成datetime**
        create_time=datetime.datetime.<strong>fromtimestamp</strong>(self["create_time"]).strftime(SQL_DATETIME_FORMAT)
        update_time=datetime.datetime.<strong>fromtimestamp</strong>(self["update_time"]).strftime(SQL_DATETIME_FORMAT)
        params = (
            self["zhihu_id"], self["url"], self["question_id"],
            self["author_id"], self["content"], self["praise_num"],
            self["comments_num"], create_time, update_time,
            self["crawl_time"].strftime(SQL_DATETIME_FORMAT),
        )
        return insert_sql, params<strong> ##記得返回




pipelines.py

class MysqlTwistedPipeline(object):
		  def __init__(self,dbpool):
		        self.dbpool=dbpool
		    #異步
		    @classmethod
		    def from_settings(cls,settings):
		        dbparms=dict(
		        host=settings['MYSQL_HOST'],
		        db=settings['MYSQL_DBNAME'],
		        user=settings['MYSQL_USER'],
		        passwd=settings['MYSQL_PASSWD'],
		        charset='utf8',
		        cursorclass=MySQLdb.cursors.DictCursor,
		        use_unicode=True,
		        )
    dbpool=adbapi.ConnectionPool("MySQLdb",**dbparms)

    return cls(dbpool)

def process_item(self, item, spider):
    #使用Twisted將mysql插入變成異步執行
    query=self.dbpool.runInteraction(self.do_insert,item)
    query.addErrback(self.handle_error,item,spider) #處理異常


def handle_error(self,failure,item,spider):
    #處理異步插入異常
    print(failure)

def do_insert(self,cursor,item):
    #執行具體的插入
    # insert_sql = """
    #             insert into duwenzhang(title,create_time,url,url_object_id,belong,article_plot)
    #             VALUES (%s,%s,%s,%s,%s,%s)
    #         """
    # cursor.execute(insert_sql, (
    # item["title"], item["create_time"], item["url"], item["url_obj_id"], item["belong"], item["article_plot"]))
    insert_sql,params=item.get_insert_sql()
    cursor.execute(insert_sql,params)

settings.py加上

#格式化
SQL_DATETIME_FORMAT="%Y-%m-%d %H:%M:%S"
SQL_DATE_FORMAT="%Y-%m-%d"

#MYSQL參數
MYSQL_HOST = "localhost"
MYSQL_DBNAME = "zhihu"
MYSQL_USER = "root"
MYSQL_PASSWD = "root"

#設置爲true
COOKIES_ENABLED = True

#取消註釋
DOWNLOADER_MIDDLEWARES = {
   # 'douban.middlewares.DoubanDownloaderMiddleware': 543,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware':2

}
#加上user_agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36'

在這裏插入圖片描述
在這裏插入圖片描述
----後記-----
content標籤給整忘去了~這樣就可以了
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章