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标签给整忘去了~这样就可以了
在这里插入图片描述

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