利用 Python 实现自动抢报告

发布于 2025-02-19 00:51:03 字数 8823 浏览 7 评论 0

上了研究生才知道北化的研究生每年需要听 15 个报告,而且最重要的是这些报告都是算分 数的,而且更重要的是你的分数是和最后的奖学金评定挂钩的。这也就决定了大家爲了那麽 点报告分数挤教务网都挤破了头(北化的服务器大家都懂),虽然场面不如开学抢课那麽火 爆,却需要长时间挂着教务网疯狂刷新,爲了交换个讲座也需要大半夜起来,生怕被别人截 胡。爲了解放生产力,同时熟悉爬虫技术、神经网络技术、装饰器等进阶内容,写了这个爬 虫来练练手。

0.1 声明

该爬虫是本人 (Cycoe) 练习 python 编程技巧和神经网络所编写,使用造成的任何责任与 本人无关

项目地址

0.2 问题分解

想要顺利的拿到抢讲座的 session,需要如下的步骤:

  1. 处理登陆问题,包括处理验证码、页面表单和 cookies
  2. 获取报告列表
  3. 获取抢报告的地址
  4. 提交表单数据

0.3 框架设计

好的框架应具有良好的可维护性和扩展性,目前正朝着这个方向努力

  1. 採用交互式的命令行设计,分离 login, robSpeech 等方法
  2. 将 login, robSpeech, robClass 等方法封装成 Robber 对象
  3. 将底层的 requests 封装成 Spider 对象

0.4 逐步解决

0.4.1 构造 Headers

通过 firefox 或 chrome 的 debug 模式可以查看在访问网页时的 request 和 response。仿照浏览器在访问教务网时提交的 headers 构造如下字典

  class Spider(object):
      @staticmethod
      def formatHeaders(referer=None, contentLength=None, originHost=None):
          """
          封装请求的 headers

          :param referer: 跳转标记,告诉 web 服务器自己是从哪个页面跳转过来的
          :param contentLength: 作用未知
          :param originHost: 原始主机地址
          :returns: headers 字典
          """
          headers = {
              'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
              'Accept-Encoding': 'gzip, deflate',
              'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
              'Cache-Control': 'max-age=0',
              'Connection': 'keep-alive',
              'Content-Type': 'application/x-www-form-urlencoded',
              'DNT': '1',
              'Host': 'graduate.buct.edu.cn:8080',
              'Upgrade-Insecure-Requests': '1',
              'User-Agent': 'Mozilla/5.0 (X11;Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36',
              'Referer': referer,
              'Content-Length': contentLength,
              'Origin': originHost,
          }

0.4.2 构造请求对象

封装 request 请求需要的 prepareBody 对象, response = session.send(prepareBody) ,response 就是我们拿到的服务器的响应对象,可通过 response.text 得到网页内容, response.status_code 得到状态码, response.url 得到响应的地址。

  def prepare(self, referer=None, originHost=None, method='GET',
              url=None, data=None, params=None):
      """ 生成用于请求的 prepare
      :param referer: 跳转标记,告诉 web 服务器自己是从哪个页面跳转过来的
      :param originHost: 原始主机地址
      :param method: 请求方法 in ['GET', 'POST']
      :param url: 请求的 url 地址
      :param data: 封装的 post 数据
      :param params: post 参数
      :return: prepare 对象
      """
      headers = self.formatHeaders(referer=referer, originHost=originHost)
      req = Request(method, url, headers=headers, data=data, params=params)
      return self.session.prepare_request(req)

  prepareBody = prepare(
      referer=None, originHost=None, method='GET',
      url=UrlBean.jwglLoginUrl, data=None, params=None
  )
  response = session.send(prepareBody)

0.4.3 请求登录

请求登录网址,提交表单数据。根据 response 返回的响应体内容判断是否登陆成功。

  def getVIEWSTATE(self):
     """ 正则获取页面的 __VIEWSTATE

     :returns: 页面的 __VIEWSTATE
     """
     VIEWSTATE = re.findall('<.*name="__VIEWSTATE".*value="(.*)?".*/>', self.response.text)
     if len(VIEWSTATE) > 0:
         return VIEWSTATE
     else:
         return None


  def getEVENTVALIDATION(self):
     """ 正则获取页面的 __EVENTVALIDATION

     :returns: 页面的 __EVENTVALIDATION
     """
     EVENTVALIDATION = re.findall('<.*name="__EVENTVALIDATION".*value="(.*)?".*/>', self.response.text)
     if len(EVENTVALIDATION) > 0:
         return EVENTVALIDATION
     else:
         return None

  def login(self):
      """ 登录教务网 """

      # 在登录前请求一次登录页面,获取网页的隐藏表单数据
      prepareBody = self.prepare(referer=None,
                                  originHost=None,
                                  method='GET',
                                  url=UrlBean.jwglLoginUrl,
                                  data=None,
                                  params=None)

      # 登陆主循环
      while True:
          self.response = self.session.send(prepareBody)
          self.VIEWSTATE = self.getVIEWSTATE()
          self.EVENTVALIDATION = self.getEVENTVALIDATION()
          if self.VIEWSTATE is not None and self.EVENTVALIDATION is not None:
              break
          Logger.log("Retrying fetching login page viewState...", level=Logger.warning)

      reInput = True      # 是否需要重新输入用户名和密码
      while True:
          # 输入用户名和密码
          if reInput:
              if Config.checkUserFile():
                  Config.readUserInfo()
              else:
                  Config.userName = input("> UserName: ")
                  Config.password = input("> Password: ")
              reInput = False

          prepareBody = self.prepare(referer=UrlBean.jwglLoginUrl,
                                      originHost=None,
                                      method='GET',
                                      url=UrlBean.verifyCodeUrl,
                                      data=None,
                                      params=None)

          while True:
              codeImg = self.session.send(prepareBody)  # 获取验证码图片
              if codeImg.status_code == 200:
                  break
              else:
                  Logger.log("retrying fetching vertify code...", level=Logger.warning)

          with open('check.gif', 'wb') as fr:  # 保存验证码图片
              for chunk in codeImg:
                  fr.write(chunk)

          print_vertify_code()
          verCode = input("input verify code:")
          # verCode = self.classifier.recognizer("check.gif")  # 识别验证码

          # 构造登陆表单
          postData = {
              '__VIEWSTATE': self.VIEWSTATE,
              '__EVENTVALIDATION': self.EVENTVALIDATION,
              '_ctl0:txtusername': Config.userName,
              '_ctl0:txtpassword': Config.password,
              '_ctl0:txtyzm': verCode,
              '_ctl0:ImageButton1.x': '43',
              '_ctl0:ImageButton1.y': '21',
          }
          prepareBody = self.prepare(referer=UrlBean.jwglLoginUrl,
                                      originHost=UrlBean.jwglOriginUrl,
                                      method='POST',
                                      url=UrlBean.jwglLoginUrl,
                                      data=postData,
                                      params=None)

          # 获取登陆 response
          while True:
              self.response = self.session.send(prepareBody)
              if self.response.status_code == 200:
                  break

          # 根据返回的 html 判断是否登录成功
          if re.search('用户名不存在', self.response.text):
              Logger.log('No such a user!', ['Cleaning password file'], level=Logger.error)
              print(OutputFormater.table([['No such a user!'], ['Cleaning password file']], padding=2))
              Config.cleanUserInfo()
              reInput = True

          elif re.search('密码错误', self.response.text):
              Logger.log('Wrong password!', ['Cleaning password file'], level=Logger.error)
              print(OutputFormater.table([['Wrong password!'], ['Cleaning password file']], padding=2))
              Config.cleanUserInfo()
              reInput = True

          elif re.search('请输入验证码', self.response.text):
              Logger.log('Please input vertify code!', ['Retrying...'], level=Logger.error)
              print(OutputFormater.table([['Please input vertify code!'], ['Retrying...']], padding=2))

          elif re.search('验证码错误', self.response.text):
              Logger.log('Wrong vertify code!', ['Retrying...'], level=Logger.error)
              print(OutputFormater.table([['Wrong vertify code!'], ['Retrying...']], padding=2))

          else:
              Logger.log('Login successfully!', ['UserName: ' + Config.userName, 'Password: ' + Config.password], level=Logger.warning)
              print(OutputFormater.table([['Login successfully!']], padding=2))
              Config.dumpUserInfo()
              break

0.4.4 拿到 session

拿到已登陆的 session 后,抢课和抢报告都是非常方便的,只要按照浏览器提交的数据构造 headers 和表单数据后就可以获得正常的 response

0.5 暗坑总结

  1. 刚开始抓到的网页内容中文都是乱码,后来 google 解决,发现是 python 的编码和 asp 框架的编码问题造成的,python 中的编码问题真的是让人头大
  2. 由于网站的防爬虫设计,会在 html 源码中插入很多隐藏的表单数据,如此处的 __VIEWSTATE__EVENTVALIDATION ,这两个是非常重要的参数。否则无法成功登陆
  3. 两次访问之间要有一定的时间间隔,如此处用了一个随机函数的闭包来获得随机时间的间隔
  4. 使用装饰器解决了在访问抢课网页前判断登录的问题
  5. 接下来将循环封装成函数,加入最大循环次数和超时
  6. 完善边界检查和异常处理

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

↘紸啶

暂无简介

文章
评论
27 人气
更多

推荐作者

笑脸一如从前

文章 0 评论 0

mnbvcxz

文章 0 评论 0

真是无聊啊

文章 0 评论 0

旧城空念

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文