scrapy+selenium按照某一主题爬取zhihu相关内容

紧接前一篇分析zhihu反爬方法的博文,经过好几天的折腾,最终我还是选择通过百度搜索相关的问题,直接对问题详情页进行解析。这样做的好处在于知乎问题详情页是可以使用selenium爬取的,不必与反爬斗智斗勇,也就不用担心万一很快进一步升级反爬策略后爬虫失效,不足之处在于爬取速度肯定比不上requests请求,不过对我来说影响不是很大,所以也算找到了一种可行的方法啦。

今天这篇文章将详细记录一下scrapy+selenium+mongodb爬取zhihu某主题问题与答案的方法。

爬虫的总体流程很简单:使用百度搜索“【关键词】+知乎”->从检索结果链接中提取符合知乎问题格式的链接->访问提取到的链接->对问题和答案信息进行解析->存入mongodb。

在爬虫的具体实现中,有几个比较重要的部分:

目录

1. scrapy与selenium对接

2. scrapy与mongodb的对接

3. 解析百度页面

4. 解析知乎页面

5. 过滤已爬取问题链接

6. 断点续爬


1. scrapy与selenium对接

使用scrapy的同学们肯定知道,在scrapy中,使用scrapy.Request函数可以很方便地对url进行请求,设置是否对重复链接进行过滤,传递meta数据以及确定callback函数。那么如何在scrapy的框架中导入selenium呢,那就需要用到scrapy的下载器中间件了。这一部分参考了崔大神的博客https://cuiqingcai.com/8397.html,相当有用!当然,在具体操作的时候还是要根据自己的需求做一些改动,比如对页面底部元素的xpath定位。

为了能够全面地爬取一个知乎问题下的所有答案,我选择按照答案的回答时间进行排序的页面,这样对于多于 20条的内容,知乎会进行分页展示,这样相比于默认排序,更容易在指定的等待时间中加载页面的所有信息。当内容少于20条时,页码将不再显示,但是我在翻阅后发现,页底啥也没有……但是在默认的问题页面(也就是不按照时间排序的页面),会出现一个“写答案”的按钮。当然,当一个问题没有任何回答时,这两个元素都不存在。

所以,我们需要查找的页底元素就确定啦,那就是当答案数>20时回答按时间排序的页码标签,当答案数<20时,则提取url中的问题id,重新生成url,跳转到默认的问题页面寻找“写答案”按钮。不过因为在中间件中我们并不对页面信息进行解析,所以并不能识别出页面中的答案数量,我这里采取了比较朴素的方法,也就是首先访问按照时间排序的页面,如果出现超时错误,则继续访问默认页面,获取“写答案”按钮。在后来的爬取中,还出现了一种页码标签与“写答案”标签均不存在的情况,那就是问题没有回答,虽然不影响我们爬取的正确性,但却会得到两个超时错误,对程序效率有较大影响。

通过对页面的检查,我发现在问题没有回答时,会出现一个特殊标签,可以用作标记。只需要在查找页码标签之前,先判断页面是否存在这个xpath为'.//div[@class="Card Answers-none"]'的元素,就可以有效过滤无回答的问题了。

还有一个问题在于,按照时间排序的页面与默认排序的页面加载方式不同,在selenium模拟页面下拉的过程时,均会出现一个页内的登录框,基于时间排序的页面中的页码加载不受其影响,但默认排序中“写回答”按钮则不会自动加载出来,需要我们去掉登录框后再进行下拉,直至页面底部才会异步加载出这个按钮。因此,在爬取回答数量小于20的默认问答页面时,步骤其实是:使用selenium模拟下拉页面以导致登录框出现->selenium模拟鼠标单击页面空白处,消除登录框->继续模拟下拉页面至底部->寻找“写回答”按钮。

爬虫中间件的第三次更新

上面所说的try-except方法虽然可行,但是在实践中由于在未找到时需要重复定位元素,比较严重地影响了效率,因此我进一步修改了判断zhihu的无回答、<=20个回答和>20个回答的代码。具体思路如下:

  1. 在确定问题编号后,构造正则表达式为'(https://www.zhihu.com/question/)(\d+)'的url,其中数字为问题编号
  2. 获取页面内容,首先通过'.//div[@class="Card Answers-none"]'判断该问题是否没有回答,如存在该元素,说明确实没有回答,直接返回None,否则则继续转下一步,解析该问题的回答数;
  3. 分析我们获取的页面,发现可以通过如下代码获得该问题的回答数:
  4. ans_num = br.find_element_by_xpath('.//h4/span').text
        s = re.search('([0-9,]+)', ans_num).group(1).replace(',','')
  5. 到此为止,我们没有用到需要异步加载的情况。将s转换为int类型,若s<=20,我们需要重新构造url,按照默认排序访问问题页,并且寻找“写回答”标签作为页面结束,这里重新定义了一个is_element_present函数,用来判断此时页面是否含有特定元素。此处有两种情况:
    • 一是回答数量较少,此时“写回答”标签是直接存在的,不需要进一步加载,并且也不会出现登录框子页面;
    • 还有一种情况是回答数较多,“写回答”标签不能一次加载出来,这时我们就需要模拟下拉页面、等待登录框出现、消除登录框、拉至页面底部,最后再获取“写回答”标签
  6. 同理,如果回答数大于20,我们只需要更改判断页面底部的元素即可,不过据观察,按照时间顺序排序的页面,尽管会出现登录框,但并不影响页面底部信息的加载,因此不需要定位登录框元素并消除。

除此之外,还有一个改动在于我的爬虫首先需要访问百度,这部分是不需要使用selenium的,直接使用原本的request函数即可,这部分的设置很简单,只需要在middleware导入re模块后在process_request函数开始加上两行代码即可:

if re.match('https://www.baidu.com', request.url):
    return None

在原本的函数中,process_request返回的就是None,所以当链接是百度网页的链接时,继续返回None,就可以按照scrapy原本的scrapy.Request方法使用了。

下面贴上middlewares.py的代码,因为一边写一边发现问题,不长的代码还是修改了不少次,其中spidermiddleware是默认的,并未修改,res_path函数是用来解析chronium和chrome driver路径的,主要在使用pyinstaller打包内容时用到,此处先不做介绍。下载器中间件的其他修改是添加了一个selenium模拟点击的函数click_locxy(),以及主要的对接selenium的函数process_request():

# -*- coding: utf-8 -*-

# Define here the models for your spider middleware
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/spider-middleware.html

from scrapy import signals
import re
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.common.action_chains import ActionChains
from scrapy.http import HtmlResponse
import time
import sys
import os
import zipfile
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

class BaiducrawlerSpiderMiddleware(object):
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the spider middleware does not modify the
    # passed objects.

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_spider_input(self, response, spider):
        # Called for each response that goes through the spider
        # middleware and into the spider.

        # Should return None or raise an exception.
        return None

    def process_spider_output(self, response, result, spider):
        # Called with the results returned from the Spider, after
        # it has processed the response.

        # Must return an iterable of Request, dict or Item objects.
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        # Called when a spider or process_spider_input() method
        # (from other spider middleware) raises an exception.

        # Should return either None or an iterable of Response, dict
        # or Item objects.
        pass

    def process_start_requests(self, start_requests, spider):
        # Called with the start requests of the spider, and works
        # similarly to the process_spider_output() method, except
        # that it doesn’t have a response associated.

        # Must return only requests (not items).
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)


class BaiducrawlerDownloaderMiddleware(object):
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the downloader middleware does not modify the
    # passed objects.

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def click_locxy(self, br, x, y, left_click=True):
        if left_click:
            ActionChains(br).move_by_offset(x, y).click().perform()
        else:
            ActionChains(br).move_by_offset(x, y).context_click().perform()
        ActionChains(br).move_by_offset(-x, -y).click().perform()

    def is_element_present(self, driver, how, what):
        try:
            driver.find_element(by=how, value=what)
        except NoSuchElementException:
            return False
        return True

    def res_path(self, relative_path):
        """获取资源绝对路径"""
        try:
            base_path = sys._MEIPASS
        except Exception:
            base_path = os.path.abspath(".")

        return os.path.join(base_path, relative_path)



    def process_request(self, request, spider):
        '''
        如果是知乎url,则接入selenium
        :param request:
        :param spider:
        :return:
        '''
        # Called for each request that goes through the downloader
        # middleware.

        # Must either:
        # - return None: continue processing this request
        # - or return a Response object
        # - or return a Request object
        # - or raise IgnoreRequest: process_exception() methods of
        #   installed downloader middleware will be called
        if re.match('https://www.baidu.com', request.url):
            return None
        elif re.match('(https://www.zhihu.com/question/)(\d+)', request.url):
            # desired_capabilities = DesiredCapabilities.CHROME
            # desired_capabilities["pageLoadStrategy"] = "none"

            # 先将zip压缩文件进行解压
            chrome_zip = zipfile.ZipFile(self.res_path('baiduCrawler/chrome-win.zip'))
            chrome_zip.extractall(sys._MEIPASS)


            option = webdriver.ChromeOptions()
            option.add_argument('headless')
            option.add_argument('log-level=1')
            prefs = {'profile.managed_default_content_settings.images': 2}
            option.add_experimental_option('prefs', prefs)

            option.binary_location = self.res_path('chrome-win/chrome.exe')
            driver_path = self.res_path('baiduCrawler/spiders/chromedriver.exe')
            br = webdriver.Chrome(executable_path=driver_path, chrome_options=option)
            wait = WebDriverWait(br, 20)
            wait2 = WebDriverWait(br, 10)
            br.get(request.url)

            try:
                if self.is_element_present(br, By.XPATH, './/div[@class="Card Answers-none"]'):
                    return None

                ans_num = br.find_element_by_xpath('.//h4/span').text
                s = re.search('([0-9,]+)', ans_num).group(1).replace(',','')
                if int(s) <= 20:
                    turl = re.match('(https://www.zhihu.com/question/)(\d+)', request.url)
                    tu = turl.group(1) + turl.group(2)
                    br.get(tu)
                    if self.is_element_present(br, By.XPATH, './/button[@class="Button QuestionAnswers-answerButton Button--blue Button--spread"]'):
                        pass
                    else:
                        '''
                        如果问题的回答数量少于20,则没有翻页信息,寻找‘写回答’的标签。
                        如果该问题没有回答,则不提取该页面
                        '''
                        js = "var q=document.documentElement.scrollTop=100000"
                        br.execute_script(js)
                        time.sleep(2)
                        inner = wait.until(EC.presence_of_element_located((By.XPATH, './/div[@class="Modal-inner"]')))
                        self.click_locxy(br, 100, 0)  # 左键点击
                        js = "var q=document.documentElement.scrollTop=100000"
                        br.execute_script(js)
                        time.sleep(2)
                        target = wait2.until(EC.presence_of_element_located((By.XPATH,
                                                                             './/button[@class="Button QuestionAnswers-answerButton Button--blue Button--spread"]')))
                        target.location_once_scrolled_into_view
                else:
                    if self.is_element_present(br, By.XPATH, './/div[@class="Pagination"]'):
                        pass
                    else:
                        target = wait.until(EC.presence_of_element_located((By.XPATH, './/div[@class="Pagination"]')))
                        target.location_once_scrolled_into_view
            except TimeoutException:
                return HtmlResponse(url=request.url, status=500, request=request)

            # try:
            #     # js = "var q=document.documentElement.scrollTop=100000"
            #     # br.execute_script(js)
            #     # time.sleep(2)
            #     # inner = wait.until(EC.presence_of_element_located((By.XPATH, './/div[@class="Modal-inner"]')))
            #     # self.click_locxy(br, 100, 0)  # 左键点击
            #     target = wait.until(EC.presence_of_element_located((By.XPATH, './/div[@class="Pagination"]')))
            #     target.location_once_scrolled_into_view
            # except TimeoutException:
            #     turl= re.match('(https://www.zhihu.com/question/)(\d+)',request.url)
            #     tu=turl.group(1)+turl.group(2)
            #     br.get(tu)
            #     try:
            #         '''
            #         如果问题的回答数量少于20,则没有翻页信息,寻找‘写回答’的标签。
            #         如果该问题没有回答,则不提取该页面
            #         '''
            #         js = "var q=document.documentElement.scrollTop=100000"
            #         br.execute_script(js)
            #         time.sleep(2)
            #         inner = wait.until(EC.presence_of_element_located((By.XPATH, './/div[@class="Modal-inner"]')))
            #         self.click_locxy(br, 100, 0)  # 左键点击
            #         js = "var q=document.documentElement.scrollTop=100000"
            #         br.execute_script(js)
            #         time.sleep(2)
            #         target = wait2.until(EC.presence_of_element_located((By.XPATH, './/button[@class="Button QuestionAnswers-answerButton Button--blue Button--spread"]')))
            #         target.location_once_scrolled_into_view
            #     except TimeoutException:
            #         return HtmlResponse(url=request.url, status=500, request=request)
            # html_str = br.page_source
            #br.quit()
            # inner = wait.until(EC.presence_of_element_located((By.XPATH, './/div[@class="Modal-inner"]')))
            # self.click_locxy(br, 100, 0)  # 左键点击
            return HtmlResponse(url=request.url, body=br.page_source, request=request, encoding='utf-8',
                                status=200)

    def process_response(self, request, response, spider):
        # Called with the response returned from the downloader.

        # Must either;
        # - return a Response object
        # - return a Request object
        # - or raise IgnoreRequest
        return response

    def process_exception(self, request, exception, spider):
        # Called when a download handler or a process_request()
        # (from other downloader middleware) raises an exception.

        # Must either:
        # - return None: continue processing this exception
        # - return a Response object: stops process_exception() chain
        # - return a Request object: stops process_exception() chain
        pass

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

2. scrapy与mongodb的对接

mongodb是一个典型的NoSQL数据库,在数据库中,各条信息以键值对的形式存在,并且支持查询、索引等功能。首先在官网(https://www.mongodb.com/try/download/community)下载mongodb,然后下载可视化工具RoBo 3T (https://robomongo.org/),这样我们就可以很方便地查看数据库中的信息了。

那么如何将scrapy中解析得到的数据存入mongodb呢,这就需要用到pymongo这个库了,使用pip就可以下载啦。这次我们需要配置的地方是pipilines.py这个文件,这个过程在之前scrapy与 selenium对接的博文中也有详细的介绍,直接用就没有问题,需要修改的是items.py中的数据结构,根据我们实际需要获取的信息定义即可。当然,不要忘记在setting中对中间件和pipilines进行指定。一个小提示是我们要为每个爬虫使用单独的setting时,不要在setting文件中进行设置,可以直接在爬虫文件中定义custom_setting字典,例如:

custom_settings = {
        'DEFAULT_REQUEST_HEADERS': header,
        'DOWNLOAD_DELAY': 0.5,
        'DOWNLOADER_MIDDLEWARES': {
           'baiduCrawler.middlewares.BaiducrawlerDownloaderMiddleware': 543,
        },
        'MONGO_URI': '127.0.0.1',
        'MONGO_DB': 'zhihuSpider',
        'ITEM_PIPELINES': {
           'baiduCrawler.pipelines.BaiducrawlerPipeline': 300,
        },
        'BREAKING_POINT': True
    }

其中header是我们自己定义的请求头部分。BREAKING POINT标识是否启用断点,将在下面介绍。

在items.py我一共定义了两个item,分别记录问题和答案信息,能够联系起问题和回答的键值对是问题id,也就是每个问题url中跟在question后面的数字。为了加快检索速度,我们可以使用Robo 3T对每个collection建立唯一的索引,对于每个回答,使用(url+百度检索时的keyword)建立索引,而由于每个问题可能存在很多页,不同页url不同,所以使用(问题id+url)建立索引。

3. 解析百度页面

百度页面相对来说还是比较简单的,主要的链接形式为“https://www.baidu.com/s?wd={}&pn={}”,其中,“wd=”后面接的是搜索关键词的url编码,“pn=”后面接的是一个数字,可以用这个数字决定搜索结果的页码,计算公式是pn=(page-1)*10。

在解析时主要有两个需要注意的点:一是请求头的设置需要多一些参数,只设置一个User-Agent是没有用的哦;二是我们拿到的每条结果左下角的链接并不是目标网站的真实链接,需要对这些链接再请求一次,得到响应的“location”才是真正的链接。下面是爬取百度搜索页面时的请求头:

header = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6',
        'Accept-Encoding': 'gzip, deflate, br',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
    }

当然,还存在一次请求没有请求到结果信息就返回的情况,这种情况下可以由xpath判断返回的结果链接数量,如果小于一定的值,则再请求一次。

4. 解析知乎页面

我们需要解析的页面是答案按照时间排序的问题页面,但是知乎其实有很多类型的页面,检索结果还有可能是topic页面或是某一个答案单独的页面,因此,在获得百度结果的目标链接后,我们需要使用正则匹配,对只包含一个答案的页面,提取url中question id部分,再加入meta数据中的页码,就可以重新构造我们需要的页面url了。

按照时间顺序排列答案的问题页面url格式为:“https://www.zhihu.com/question/{}/answers/updated?page={}”,使用format函数将我们从url提取到的信息填入就可以了。

现在我们终于得到了包含我们需要信息的页面,下面不妨定义一个新的解析函数parse_zhihu,对页面中我们需要的信息进行解析和存储。这个函数同之前对百度链接进行解析的parse函数一样,使用yeild返回同一个scrapy.Request对象(在当前不是最后一页的情况下)。形式如下:

yield scrapy.Request(self.zhihu_url.format(question_num, page), dont_filter=True,
                                     meta={"page": page, "question_num": question_num,                         
                                     "keyword": key},
                                     callback=self.parse_zhihu)

将当前页码、问题id以及搜索关键词层层传递下去,并回调给自身,解析下一个相同格式的页面。

在我们获取到目标页面的HTML之后,就可以使用各种方法对我们需要的标签内容进行提取啦。这个过程总体来说是很简单的,但还是要注意知乎页面有相当多的小细节,需要分情况处理,比如在答案回答时间的标识上,就分为去年及以前、今年、48小时内、24小时内等不同的标识,类似的情况相当多,还是需要细心发现才可以。

5. 过滤已爬取问题链接

本项目利用爬虫收集数据并非是一蹴而就,而是在用户需要时对内容进行增量更新,同时,在一次爬取过程中,不同关键词有可能对应同一个问题,在这种情况下,url尽管相同,却不能被过滤,因此直接使用dont_filter这个属性是不合适的。这就牵出了以下两个方法来解决上面的问题:使用hash值对爬虫数据进行增量更新,设置过滤字典。

在一次爬取过程中,我们需要对结果页面中重复的问题进行过滤,防止重复爬取,然而,对于不同检索关键词下的同一个问题,是不应该过滤的。因此,这里我没有直接使用dont_filter这个参数,而是另外设置了一个visited_dict字典,建立一个由{关键词:[问题id]}组成的键值对集合,在解析百度结果中的链接时,如果该关键词对应的问题id列表中已经存在这个问题id,则直接跳过,这样就避免了同个关键词结果中相同问题的反复爬取。

在多次爬取过程中,则会存在对同个问题多次爬取的情况,这时则需要判断问题内容和回答是否自上次爬取后有更新,比如正文内容、点赞情况、评论情况等。总体来说,就是判断内容是否已经在数据库中存在,如果不存在,则加入数据库中;如果存在,则判断信息是否有变动,如果有,则更新数据,如果没有,则跳过。这个过程就用到了我们之前在使用mongodb时为每个collection设置的索引了,因为每条数据的索引都是唯一标识这条数据的,所以可以以此判断数据是否已经存在。这部分代码是这样的:

#如果该关键词下这个问题id没有出现过,则作为新问题添加
#如果存在,则对比hash值,如果不相等,更新该条数据
if q_collection.count_documents({'question_num': question_num, 'keyword': keyword}) == 0:
    logger.info("【new question】:{}, 【keyword】:{}, 【total answer number】:{}".format(
                zhiItem['question_text'], zhiItem['keyword'], zhiItem['count_answer']
            )
            )
    self.new_question_count = self.new_question_count+1
    assert page == 1
    yield zhiItem
else:
    q_hashs =q_collection.find({'question_num': question_num, 'keyword': keyword})
    assert q_hashs.count() == 1
    for q in q_hashs:
        q_hash = q['question_hash']
        if q_hash != zhiItem['question_hash']:
            logger.info("【update question】:" + zhiItem['question_text']+' 【keyword】:'+zhiItem['keyword'])
            q_collection.update({'question_num': question_num, 'keyword': keyword}, zhiItem)
            self.update_question_count = self.update_question_count+1
         else:
            pass

以上是对于问题item的增量更新过程,对于回答的增量更新与之类似。在else之后的语句中,对比数据库中各项信息生成的hash值与目前网页中收集信息的hash值,如果不相等则更新相关信息。其中q_collection是mongodb的一个collection,count_documents,find,update分别表示数据库根据索引键值对的计数、查找、更新操作,想在爬虫中直接访问数据库,需要在spider文件中再导入一下mongodb:

mongo_uri = custom_settings['MONGO_URI']
mongo_db = custom_settings['MONGO_DB']
client = pymongo.MongoClient(host=mongo_uri, port=27017)
db = client[mongo_db]

在计算hash值时还有两个小坑,一个是用于计算hash值的字符串内容要选好,比如keyword,question_text,问题关注者数量等都需要随时对应更新,因此应该加入这个字符串,但是对于问题的url,由于翻页信息的存在,同个问题可能存在很多不同的url,在keyword和question_number可以唯一标识问题的情况下,问题url是没必要加进去的。第二个坑则是hash值的计算,python有自带的hash函数,然而,出于某种安全保护机制,在不同进程中对同一个字符串进行哈希,得到的结果是不同的!那么如何对同个字符串始终得到相同的hash值呢,这就需要用到hashlib这个库了。具体方式如下:

md5_2 = hashlib.md5()
md5_2.update(hsh_txt.encode('utf-8'))  #hsh_txt为待求hash值的字符串
item['answer_hash'] = md5_2.hexdigest()

md5_2 = hashlib.md5()这一句在每个字符串计算时都要加上,否则update函数将始终在上次计算的结果上叠加新字符串计算。换句话说,如果字符串较长,可以多次使用update函数,若是分别计算两个字符串的值,则必须加入md5_2 = hashlib.md5()这句话。

6. 断点续爬

由于网络原因,爬虫很容易中断,对于需要爬取大量数据的爬虫来说,要是每次中断要重新开始,无疑会耗费大量的时间,因此,使用一些小trick来实现断点续爬是性价比很高的。

同样的,我们可以设置一个字典,在每个关键词下、每个问题收集完某页信息之后,记录下该页的的页码,并且利用pickle库存储成pkl文件,在爬虫每次启动时,将文件中的字典load进内存中,如果重新遇到这个问题,那么直接读出页码,令page=dic[keyword][question_num]+1,就可以继续接着上次的页面进行爬取了。当某个问题已经爬取完最后一页,可以将dic[keyword][question_num]设置为一个很大的值,后面如果读到dic[keyword][question_num]为这个值,则直接跳过。这个过程与前面第五条防止链接重复并不冲突,可以一起使用。在parse函数中,可加入以下代码实现:

from_breakingPoint = custom_settings['BREAKING_POINT']

if from_breakingPoint:
    if key in self.finish_dic and question_num in self.finish_dic[key]:
        if self.finish_dic[key][question_num] == 9999:
            continue
        page = self.finish_dic[key][question_num]+1
        logger.info("断点,问题{}从第{}页开始请求".format(question_num, page))
     else:
        page = 1
else:
     page = 1

在parse_zhihu函数中,每次解析完一页,更新finish_dic的值:

if self.from_breakingPoint:
    f = open(self.finish_file_path, 'wb')
    if keyword in self.finish_dic:
        self.finish_dic[keyword][question_num] = page
    else:
        self.finish_dic[keyword] = {}
        self.finish_dic[keyword][question_num] = page
    if not html_str.xpath('.//button[@class="Button PaginationButton PaginationButton-next Button--plain"]'):
        self.finish_dic[keyword][question_num] = 9999
    pickle.dump(self.finish_dic, f)
    f.close()

由于涉及到数据更新,这个断点操作我只想在一天之内有效,因此,不妨将finish_file_path的文件名加入当前日期,在每次爬虫启动时,查看是否存在今天生成的pkl文件:

finish_file_path = os.path.join(vlog_path, 'vlog_{}.pkl'.format(datetime.date.today().strftime('%Y_%m_%d')))
if os.path.isfile(finish_file_path):
    f = open(finish_file_path, 'rb')
    finish_dic = pickle.load(f)
    f.close()
else:
    finish_dic = {}

最后提一下logging的用法,来源于网络:

import logging  
class baiduzhihuSpider(scrapy.Spider):  
    def zhi_log(self):
        '''
        logger函数,在文件和控制台输出信息
        :return:
        '''
        # 创建logger,如果参数为空则返回root logger
        logger = logging.getLogger("zhihuLogger")
        logger.setLevel(logging.DEBUG)  # 设置logger日志等级​
        # 这里进行判断,如果logger.handlers列表为空,则添加,否则,直接去写日志
        if not logger.handlers:
            # 创建handler
            fh = logging.FileHandler("zhi.log", encoding="utf-8")
            ch = logging.StreamHandler()
            # 设置输出日志格式
            formatter = logging.Formatter(
                fmt="%(asctime)s %(name)s %(filename)s %(message)s",
                datefmt="%Y/%m/%d %X"
                )
            # 为handler指定输出格式
            fh.setFormatter(formatter)
            ch.setFormatter(formatter)
            # 为logger添加的日志处理器
            logger.addHandler(fh)
            logger.addHandler(ch)
        return logger  # 直接返回logger

    def parse(self, response):
        '''logger的使用'''
        logger = self.zhi_log()
        logger.info('---------------------------------zhihu Spider started {}----------------------------------------'.format(
            datetime.date.today().strftime('%Y-%m-%d')
        )
        )

好啦,以上就是本次selenium+scrapy爬取知乎问题页面的要点啦,这种方法速度算不上很快,好处在于没有硬去爬取有严厉反爬措施的知乎页面,不会对目标站服务器造成负担。接下来我将会继续爬取百度资讯页面的相关信息,敬请期待哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值