Python 下用 Scrapy 采集知网期刊数据(二)

需求

本次采集需要采集期刊 2000~2010 年间的数据,相关的数据首先是期刊中各文献的数据:

文献数据
文献数据

主要包括 title(篇名)author(作者)journalName(刊名)publicationDate(发表时间)referenceNum(被引数) 以及 downloadNum下载数

其次是每篇文献的引证文献数据:

引用文献数据
引用文献数据

主要包括 paper_title(篇名)paper_author(作者)paper_journalName(刊名)paper_time(发表时间)以及quote_title(引用的文献篇名)

搜索期刊

  1. 抓包分析 在采集数据之前首先需要获取到期刊的搜索结果,而本次采集主要和下列的搜索参数相关:

    检索条件
    • 起始年份
    • 模糊或精确搜索
    • 期刊名称 抓包之后发现搜索过程主要和下图标有红线的两个请求相关,第一个请求用于得到搜索结果,第二个请求则是将搜索的结果显示出来。 抓包数据 对第一个请求进行分析之后发现了与搜索条件相对应的搜索参数。 搜索参数 第二个请求大部分是固定参数值 显示结果
  2. 发送请求
    1. 从 Excel 读取参数: 由于需要采集每本期刊 11 年的数据,所以为了减少搜索结果从而尽可能避免需要输入验证码,所以将所有的搜索条件保存在一个 Excel 文件里面: 查询条件 读取参数过程如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      def read_excel(self):
      # 打开文件
      workbook = xlrd.open_workbook(r'/Users/firstdream/PycharmProjects/cnkiSprider/查询条件.xlsx')
      # 获取所有sheet

      # 根据sheet索引或者名称获取sheet内容
      sheet1 = workbook.sheet_by_index(0) # sheet索引从0开始
      search_setting = []
      for row in range(sheet1.nrows):
      setting = ""
      for col in range(sheet1.ncols):
      if isinstance(sheet1.row(row)[col].value, float):
      setting = setting + str(int(sheet1.row(row)[col].value)) + ","
      else:
      setting = setting + str(sheet1.row(row)[col].value)
      search_setting.append(setting)
      return search_setting

      将每行的搜索条件拼接起来并保存在一个 list 当中。

    2. 生成请求 由于请求的参数较多,所以采用词典进行请求的参数配置,再用 get_url() 获取最终的链接:
      生成第一个请求:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      def createUrls(self):
      search_setting = self.read_excel()
      urls = []
      for setting in search_setting:
      year_from = setting.split(",")[0]
      year_to = setting.split(",")[1]
      magazine_setting = setting.split(",")[2]
      search_parameters = {'action': '', 'ua': '1.21',
      'PageName': 'ASP.brief_result_aspx', 'DbPreFix': 'CJFQ',
      'DbCatalog': '中国学术期刊网络出版总库', 'ConfigFile': 'CJFQ.xml',
      'db_opt': 'CJFQ', 'db_value': '中国学术期刊网络出版总库',
      'magazine_value1': magazine_setting, 'year_type': 'echar',
      'year_from': year_from, 'year_to': year_to,
      'magazine_special1': '='
      }
      origin_url = "http://kns.cnki.net/kns/request/SearchHandler.ashx"
      url = self.get_url(origin_url, search_parameters)
      urls.append(url)
      return urls

      将 Excel 文件中读取的搜索条件通过 createUrls() 方法生成可迭代的请求列表。 生成第二个请求:

      1
      2
      3
      4
      5
      result_parameters = {'pagename': 'ASP.brief_result_aspx', 'DbPreFix': 'CJFQ',
      'Research': 'off', 'KeyValue': '', 'S': '1', 'recordsperpage': '50'
      , "sorttype": "(被引频次,'INTEGER') desc"}
      origin_url = "http://kns.cnki.net/kns/brief/brief.aspx"
      act_url = self.get_url(origin_url, result_parameters)

      第二个请求中增加了 recordsperpagesorttype 两个参数,前者代表每页显示的记录条数(Sprider 每采集 13 页左右,知网会提示输入验证码,所以设置为最大值 50 减少结果的总页数),后者代表结果的排序方式(采集引证文献时,由于一些文献是没有被引用的,所以采用按被引量递减的方式排序去过滤掉无用的请求,从而缩短采集时间)。
      get_url() 方法:

      1
      2
      3
      4
      5
      6
      def get_url(self, origin_url, parameters):
      act_url = origin_url + "?"
      for key in parameters.keys():
      act_url = act_url + key + "=" + urllib.request.quote(str(parameters[key])) + "&"
      act_url = act_url.strip("&")
      return act_url

      urllib.request.quote() 中文字符进行编码处理。

    3. 发送

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      def start_requests(self):
      headers_parameters = {
      'Host': 'kns.cnki.net',
      'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36',
      'Referer': 'http://kns.cnki.net/kns/brief/result.aspx?dbPrefix=CJFQ'
      }
      urls = self.createUrls()
      for i, url in enumerate(urls):
      yield scrapy.Request(url, meta={'cookiejar': i},
      callback=self.result_page, headers=headers_parameters)
      def result_page(self, response):
      result_parameters = {'pagename': 'ASP.brief_result_aspx', 'DbPreFix': 'CJFQ',
      'Research': 'off', 'KeyValue': '', 'S': '1', 'recordsperpage': '50'
      , "sorttype": "(被引频次,'INTEGER') desc"}
      origin_url = "http://kns.cnki.net/kns/brief/brief.aspx"
      act_url = self.get_url(origin_url, result_parameters)
      return scrapy.Request(act_url,
      meta={'cookiejar': response.meta['cookiejar']},
      callback=self.parse, dont_filter=True)

      • meta={'cookiejar': i} 保存的是 cookie 用于保持持久性连接。

        There is support for keeping multiple cookie sessions per spider by using the cookiejar Request meta key. By default it uses a single cookie jar (session), but you can pass an identifier to use different ones. For example:

        1
        2
        3
        for i, url in enumerate(urls):
        yield scrapy.Request(url, meta={'cookiejar': i},
        callback=self.parse_page)

      • start_requests(self) 在获取到搜索结果之后回调 result_page(self, response) 进行进一步处理。

      • meta={'cookiejar': response.meta['cookiejar']} 取出 cookie 值向下传递。

        Keep in mind that the cookiejar meta key is not “sticky”. You need to keep passing it along on subsequent requests. For example:

        1
        2
        3
        4
        5
        def parse_page(self, response):
        # do some processing
        return scrapy.Request("http://www.example.com/otherpage",
        meta={'cookiejar': response.meta['cookiejar']},
        callback=self.parse_other_page)

      • dont_filter=True 由于第二次请求的链接相同,Scrapy 默认会过滤到这些请求,所以必须设置为 True。

配置访问异常重试

为了尽可能保证采集数据的完整性,需要配置 setting.py 文件加入如下代码:

1
2
3
4
5
6
# 当访问异常时是否进行重试
RETRY_ENABLED = True
# 当遇到以下 http 状态码时进行重试
RETRY_HTTP_CODES = [500, 502, 503, 504, 400, 403, 404, 408]
# 重试次数
RETRY_TIMES = 5


参考资料

Scrapy 文档