美文网首页
Python Scrapy 爬取PAT网站数据(1.0 爬取题

Python Scrapy 爬取PAT网站数据(1.0 爬取题

作者: AlexSun1995 | 来源:发表于2017-06-05 11:12 被阅读0次

出于了解HTTP和爬虫的目的,于是就有了一个自己动手实现一个爬虫,并在此之上做一些简单的数据分析的想法。有两种选择,一种是完全自己用Python的urllib再配合一个html解析(beautifulsoup之类的)库实现一个简单的爬虫,另一种就是学习一个成熟而且功能强大的框架(比如说scrapy)。综合考虑之下,我决定选择后者,因为自己造的轮子肯定没有别人造的好,以后真的需要用上爬虫,使用scrapy也更加靠谱。
爬什么呢? 第一次爬虫实践,我想爬一个数据格式比较工整的、干净的,最好是一条一条数据的网站,这样我就想到了PAT的题库。
github地址

我理解的爬虫

简单的说,我们在浏览一个网页的时候,其实是向网页的服务器发送一个请求(Request),网页服务器在收到请求之后返回数据(Response),这些数据中包括HTML数据(最早期的http协议只能返回HTML数据,现在当然不是了),我们的浏览器再将这些HTML数据展示出来,就是我们看到的网页了。爬虫忽略了浏览器的存在,通过自动化的方式去发送请求,获取服务器的响应数据。
真实去做一个复杂的爬虫的时候当然不会这么简单了,还需要去考虑cookie、反爬虫技巧、模拟登陆等等,不过这个项目只是一个入门,以后接触的多了再慢慢了解也不急。

scrapy使用

对于scrapy安装、介绍这里就不复述了,我觉得网上有很多很棒的资源。

 scrapy startproject patSpider

就表示我们创造了这个叫做patSpider的scrapy项目,tree 一下,可以发现项目的结构是这个样子的:

tree,项目结构

在spider文件夹下,创建一个python文件,继承crawlSpider类,这就是一个爬虫了(要注意的是,一个scrapy项目可以创造不止一个爬虫,你可以用它来创造多个爬虫,不过每个爬虫都有一个独一无二的name加以区分,在项目的文件下使用spracy crawl 爬虫的name 就可以启动这个爬虫了 )

首先观察一下pat登录界面的network数据(使用chrome开发者模式),因为要模拟登陆,其实登陆也就是在request的表单里把服务器需要的数据提交过去(用户名、密码等),注意这里还有一个authenticity_token数据项,我们在第一次的response数据中将这一项数据提取出来,然后在下一次提交上去(其实直接复制也可以,但是就失去了代码的重用性,假如一段时间后服务器端把这个值改了怎么办?)


Screenshot from 2017-06-04 20-08-11.png

观察一下from_data中的数据项,这就是我们要提交的所有数据项
然后观察一下我们要爬取的pat甲级题库的html数据格式,因为我们就是要按照这个格式来解析html数据的;我们发现<td><tr> 下面的六行就是一个题目的信息(有没有通过, 题目编号, 题目名称, 提交次数,通过次数,通过率),我们等会就按照这个规律来解析HTML数据

image.png

patSpider/patSpider/spiders/problem_info_spider.py

from scrapy import FormRequest
from scrapy import Request
from scrapy.loader import ItemLoader
from scrapy.spiders import CrawlSpider
from patSpider.items import *
import pickle
from patSpider.pipelines import *

class pat_Spider(CrawlSpider):
    name = "pat"
    items = []
    call_times = 0
    # allowed_domains = []
  #这个是爬虫需要爬取的url,因为只有两页,所以就直接把第二页的url放上去了 
    start_urls = ["https://www.patest.cn/contests/pat-a-practise",
                  "https://www.patest.cn/contests/pat-a-practise?page=2"
                  ]
    #想网页发送请求,注意这些函数不需要显示地调用,启用爬虫的时候就自动调用了
    #使用post_login这个回调函数来提交表单数据,所谓 request 回调函数,就是一个request 获取(也可以说是下载)了一个
    # response
    # post_login 
    # 参见: callback https://doc.scrapy.org/en/1.3/topics/request-response.html#topics-request-response-ref-request-callback-
    # arguments
    #  def start_requests(self) 这个函数是重写crawlSpider 中的函数,这个函数是自动执行的,不用管在
    # 哪里去调用它,在这一段代码中,这个函数的执行顺序是最前的
    # 这三个函数的逻辑是: 首先请求登录界面,获取到第一个response 之后,把表单数据提交了,这时候就有网站的cookie了
    # 之后就把cookie作为request的参数提交,这样就能保持登录状态了。
    # 关于 cookie登录 ,这篇文章介绍的不错 http://www.jianshu.com/p/887af1ab4200
    def start_requests(self):
        return [Request("https://www.patest.cn/users/sign_in", meta={'cookiejar': 1}, callback=self.post_login)]

    def post_login(self, response):
        post_headers = {
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
            "Accept-Encoding": "gzip, deflate",
            "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6",
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "Content-Type": "application/x-www-form-urlencoded",
            "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36",
            "Referer": "https://www.patest.cn/users/sign_in",
            "Upgrade-Insecure-Requests": 1

        }
        authenticity_token = response.xpath('//input[@name="authenticity_token"]/@value').extract()[0]
        # print authenticit-y_token
        return [FormRequest.from_response(response,
                                          url="https://www.patest.cn/users/sign_in",
                                          meta={'cookiejar': response.meta['cookiejar']},
                                          headers=post_headers,
                                          formdata={
                                              'utf8': '✓',
                                              'authenticity_token': authenticity_token,
                                              'user[handle]': 'suncun',
                                              # 我把密码隐藏了
                                              'user[password]': '********',
                                              'user[remember_me]': '0',
                                              'commit': "登录"
                                          },
                                          callback=self.after_login,
                                          dont_filter=True
                                          )]


    def after_login(self, response):
        for url in self.start_urls:
            yield Request(url, meta={'cookiejar': response.meta['cookiejar']})

    # 注意,这个方法是自动调用的,通常有多少个请求url,parse就会执行多少次
    # 当这段代码执行到这个地方的时候 ,已经获取到了一个登录系统后返回的response响应
    # 对这个response中的数据进行提取,就能够获取到我们需要的结果
    #  尤其注意xpath的语法规范,selector对象selectorlist对象
    def parse(self, response):
        print response.body
        self.call_times += 1
        data_selector = response.xpath('//tr/td')
        i = 0
        while i < len(data_selector):
            six_lines = data_selector[i:i+6 ]
            i += 6
            item = PatspiderItem()
            if len(six_lines[0].xpath('.//span/text()').extract()) == 0:
                item['does_pass'] = 'Not submit'
            else:
                item['does_pass'] = six_lines[0].xpath('.//span/text()').extract()[0]
            item['id'] = six_lines[1].xpath('.//a/text()').extract()[0]
            item['title'] = six_lines[2].xpath('.//a/text()').extract()[0]
            item['pass_times'] = six_lines[3].xpath('./text()').extract()[0]
            item['submit_times'] = six_lines[4].xpath('./text()').extract()[0]
            item['pass_rate'] = six_lines[5].xpath('./text()').extract()[0]
            self.items.append(item)
            # do not use 'return' cause the item is piped to 'pipelines'
            # when the Spider is working. yield can make data collecting and
            # processing at the same time.
            yield item
        # 在最后一次调用这个parse()方法的时候,将对象序列化,以供数据分析的时候再来使用
        if self.call_times == len(self.start_urls):
            with open('items_list', 'wb') as tmp_f:
                pickle.dump(self.items, tmp_f)

简单的数据分析

分析了最难的几道题(通过率最低的)、我一共通过了多少题,多少题没有做等等...

import json
import matplotlib.pyplot as plt
import pickle

def total_submit_data(items):
    '''
    :param items: all the data of pat type:list of dic
    :return: (cnt_submit, cnt_pass)
    '''
    cnt_submit = 0
    cnt_pass = 0
    for item in items:
        cnt_submit += int(item['submit_times'])
        cnt_pass += int(item['pass_times'])
    print 'total submit times: %d, total pass times: %d' %(cnt_submit, cnt_pass)
    print 'rate: %f' %(cnt_pass * 1.0/ cnt_submit)
    return cnt_submit,cnt_pass

def top_k_hard(items, k):
    '''
    :param items: all the data of pat, type: list of dic
    :param k: self defined number, ex: if k = 10, the function will return
    information of top 10 most hard problems
    :return: list(dic)
    '''
    size = len(items)
    if k > size:
        k = size
        print 'since k is too large, now we smaller k to:', k
    new_items = sorted(items, key=lambda x:float(x['pass_rate']))
    # print new_items[0:k]
    return new_items[0:k]

def self_practice_data(items):
    '''
    user: suncun(myself)
    pass_word: ***********
    this function aim to show, number of problems I've passed,
    # of problems tried but not passed yet,# of problems never tried
    :param items: all the data of pat, type: list of dic
    :return:
    '''
    print items
    cnt_pass = 0
    cnt_not_try = 0
    cnt_not_pass = 0
    total_problems = len(items)
    for item in items:
        situation = item['does_pass']
        if situation == 'Not submit':
            cnt_not_try += 1
        elif situation == 'Y':
            cnt_pass += 1
        else:
            cnt_not_pass += 1
    print 'there a totally %d problems, and I\'ve passed %d problems' %(total_problems, cnt_pass)
    print 'tried but not passed %d problems, still %d problems not tried yet' %(cnt_not_pass, cnt_not_try)


if __name__ == '__main__':
    items = {}
    with open('../items_list', 'r') as f:
        items = pickle.load(f)
    # total_submit_data(items)
    # print top_k_hard(items, 10)
    self_practice_data(items)

部分分析结果截图:

image.png

相关文章

网友评论

      本文标题:Python Scrapy 爬取PAT网站数据(1.0 爬取题

      本文链接:https://www.haomeiwen.com/subject/ugjbfxtx.html