紧接前一篇分析zhihu反爬方法的博文,经过好几天的折腾,最终我还是选择通过百度搜索相关的问题,直接对问题详情页进行解析。这样做的好处在于知乎问题详情页是可以使用selenium爬取的,不必与反爬斗智斗勇,也就不用担心万一很快进一步升级反爬策略后爬虫失效,不足之处在于爬取速度肯定比不上requests请求,不过对我来说影响不是很大,所以也算找到了一种可行的方法啦。
今天这篇文章将详细记录一下scrapy+selenium+mongodb爬取zhihu某主题问题与答案的方法。
爬虫的总体流程很简单:使用百度搜索“【关键词】+知乎”->从检索结果链接中提取符合知乎问题格式的链接->访问提取到的链接->对问题和答案信息进行解析->存入mongodb。
在爬虫的具体实现中,有几个比较重要的部分:
目录
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个回答的代码。具体思路如下:
- 在确定问题编号后,构造正则表达式为'(https://www.zhihu.com/question/)(\d+)'的url,其中数字为问题编号
- 获取页面内容,首先通过'.//div[@class="Card Answers-none"]'判断该问题是否没有回答,如存在该元素,说明确实没有回答,直接返回None,否则则继续转下一步,解析该问题的回答数;
- 分析我们获取的页面,发现可以通过如下代码获得该问题的回答数:
-
ans_num = br.find_element_by_xpath('.//h4/span').text s = re.search('([0-9,]+)', ans_num).group(1).replace(',','')
- 到此为止,我们没有用到需要异步加载的情况。将s转换为int类型,若s<=20,我们需要重新构造url,按照默认排序访问问题页,并且寻找“写回答”标签作为页面结束,这里重新定义了一个is_element_present函数,用来判断此时页面是否含有特定元素。此处有两种情况:
- 一是回答数量较少,此时“写回答”标签是直接存在的,不需要进一步加载,并且也不会出现登录框子页面;
- 还有一种情况是回答数较多,“写回答”标签不能一次加载出来,这时我们就需要模拟下拉页面、等待登录框出现、消除登录框、拉至页面底部,最后再获取“写回答”标签
- 同理,如果回答数大于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爬取知乎问题页面的要点啦,这种方法速度算不上很快,好处在于没有硬去爬取有严厉反爬措施的知乎页面,不会对目标站服务器造成负担。接下来我将会继续爬取百度资讯页面的相关信息,敬请期待哦。