Skip to content

模拟登录某某大学图书馆系统 #6

Description

@nodejh

本文详细讲述如何模拟登录某某大学图书馆系统

为什么说是某某大学?往下看了就知道了 😉

对于爬虫程序,如果需要抓取的页面,需要登录后才能访问,这时一般就需要进行模拟登录了。由于最近需要抓取登录四川大学图书馆后的一些信息,所以以此为例详细说明整个分析和编码过程。

总的来说,对于一般系统的模拟登录分为三大步骤:

  • 分析页面,得到登录 URL 和所需要传递的数据
  • 通过程序向所得 URL 发送数据
  • 根据服务端的响应判断是否登录成功,若登录成功,则保存返回的 cookie

只要得到了 cookie,当需要抓取登陆后才能访问的页面时,只需要发送 HTTP 请求时,在 HTTP Header 带上 cookie 即可。

对于写爬虫程序,还有一些小技巧(其实目前就总结出来一个):

  • 能抓取手机站就抓取手机站,因为手机网站一般比较容易

1. 分析四川大学移动图书馆

1.1. 分析

图书馆系统有一个手机网站,所以优先选择手机站作为目标。其链接是 http://m.5read.com/395

首先看到这个链接的时候,我也是比较奇怪,毕竟这个域名就比较奇怪,川大图书馆系统手机版的域名为什么不是 scu.edu.cn 的子域名,而且域名的 PATH 部分为什么是 395

域名打开后是 四川大学移动图书馆

crawler-for-scu-lib-1-com-scu-lib.png

然后我把 395 去掉,直接输入了 http://m.5read.com,打开也是川大图书馆首页。但如果我在另一个没有打开过该链接的浏览器中打开 http://m.5read.com/ ,则是 默认单位移动图书馆

crawler-for-scu-lib-1-com-default-lib.png

这个时候就得出两个猜测:

  • 一是 URL 中的 395 是学校的编号
  • 二是打开 http://m.5read.com/395 后,客户端肯定会生成对应的 cookies ,表示当前客户端访问的是 395 这所大学的图书馆系统

为了验证第一个猜测,我们把 395 改为任意一个其他数字。这样大概有两种情况,如 http://m.5read.com/1则提示 对不起,还没有开通手机业务!http://m.5read.com/20 则是厦门大学的图书馆系统,见下图:

crawler-for-scu-lib-1-com-1-not-dredge.png

crawler-for-scu-lib-1-com-20-xmu.png

接下来再看看是不是生成了对应的 cookie 信息。在 Chrome 的 开发者工具 -> Application 的左侧菜单栏选中 Cookies,然后在右侧选中某个 cookie,并点击右键,选择 Clear All 清除所有 cookies。也可以直接在左侧菜单栏 Cookies 展开后的域名上,点击右键,选择 clear

crawler-for-scu-lib-1-clear-cache.png

因为我的目的是模拟模拟登录四川大学图书馆系统,所以我还是先访问 http://m.5read.com/395,再来看看 cookies:

crawler-for-scu-lib-1-cookies-scu.png

可以看到,的确生成了 cookies,结构和之前厦门大学类似。为了弄清楚 cookies 是怎么生成的,接下来要查看的就是 HTTP 请求的详细内容了:

crawler-for-scu-lib-1-http-request.png

可以发现,Response Headers 里面有很多 Set-Cookie 字段。请求头中没有特殊的字段。所以访问 http://m.5read.com/395 的大致流程是:

  • 浏览器(客户端)发起 HTTP 请求,请求的 URL 地址是 http://m.5read.com/395
  • 服务端接收到请求,并根据 URL 中的 395 参数,分析得出访问的是 四川大学移动图书馆
  • 服务端根据 395 在响应头信息中加入对应的 set-cookie 字段
  • 浏览器接收到服务端的响应,并根据响应头中的 Set-Cookie 字段,生成对应的 cookies

如果要访问 四川大学移动图书馆 的其他页面,必然也要带上这些 cookies,不然系统无法区分访问的是那个大学的移动图书馆。

1.2 结论

根据以上分析,得出结论如下:

  • http://m.5read.com/395 表示某个学校移动图书馆的首页,URL 中的 395 参数表示学校代码
  • 访问不同学校的移动图书馆首页,会生成对应的 cookies
  • 当需求访问某学校移动图书馆系统的其他页面时,必须带上访问首页时生成的 cookies

1.3 代码

我用的 Node.js 的 request 这个包来发送 HTTP 请求。在使用前,需要先安装: npm install request --save

具体的代码如下:

const request = require('request');
const options = {
  url: 'http://m.5read.com/395'
};
request(options, (error, response, body) => {
  if (error) {
    console.error('访问首页失败: \n', error);
    return { error };
  }
  const cookie = response.headers['set-cookie'];
  console.log('cookie:\n ', cookie);
});

程序运行后,如果没出错,则会以标准输出的形式输出 cookies:

$ node index.js
cookie:
  [ 'JSESSIONID=E2741DEB3D5296EF15A1F8914E92EE77.irdmblhome72b; Path=/; HttpOnly',
  'DSSTASH_LOG=C%5f4%2dUN%5f395%2dUS%5f%2d1%2dT%5f1475793477551; Domain=.5read.com; Path=/',

  'mgid=274; Domain=.5read.com; Expires=Sat, 05-Nov-2016 22:37:57 GMT; Path=/',
  'maid=395; Domain=.5read.com; Expires=Sat, 05-Nov-2016 22:37:57 GMT; Path=/',
  'msign_dsr=1475793477609; Domain=.5read.com; Expires=Wed, 01-Oct-2036 22:37:57 GMT; Path=/',
  'mduxiu=musername%2c%3dblmobile%2c%21muserid%2c%3d1000086%2c%21mcompcode%2c%3d1009%2c%21menc%2c%3d26546915E1F9381939EA005CB06A28F6; Domain=.5read.com; Expires=
Sat, 05-Nov-2016 22:37:57 GMT; Path=/',
  'xc=6; Domain=.5read.com; Expires=Sat, 05-Nov-2016 22:37:57 GMT; Path=/' ]

2. 分析登录页面

2.1 分析

接下来需要寻找的就是对应的登录页面。登录页面的 URL是 http://mc.m.5read.com/user/login/showLogin.jspx

打开该页面,再看看 HTTP 请求:

crawler-for-scu-lib-1-cookies-send-scu.png

可以发现,发送请求头中的 Cookie 为

DSSTASH_LOG=C%5f4%2dUN%5f395%2dUS%5f%2d1%2dT%5f1475781232585; mgid=274; maid=395; msign_dsr=1475781232606; mduxiu=musername%2c%3dblmobile%2c%21muserid%2c%3d1000086%2c%21mcompcode%2c%3d1009%2c%21menc%2c%3d13A4F68ACE9126AA111D239F62C09038; xc=5; Hm_lvt_d2fe4972d5c5737ef70e82fe0c8deaee=1475781234; Hm_lpvt_d2fe4972d5c5737ef70e82fe0c8deaee=1475781234
Host:mc.m.5read.com

其中不包含 JSESSIONID,而响应头中返回了一个新的 Set-Cookie:JSESSIONID=9C04830620D2783E63E852BC67AE031D.irdmbl72a; Path=/; HttpOnly 字段。

JSESSIONID 是 Tomcat 中的 SESSIONID,主要作用是用来标识当前请求对应的用户。SESSIONID 是唯一的。当客户端访问服务器时,服务器(这里是 Tomcat)会生成一个唯一的 SESSIONID(这里是 JSESSIONID),并返回给客户端,客户端将 SESSIONID 保存在 cookie 中。之后客户端再发送 HTTP 请求时,就会在 HTTP Headers 中以 cookie 的形式发送 SESSIONID 到服务器。服务器接收到 SESSIONID 后,就可以根据 SESSIONID 来判断是哪一个客户端发送的请求。

对于该图书馆系统,访问首页 http://m.5read.com/395 和访问登录页 http://mc.m.5read.com/user/login/showLogin.jspx 是生成的不同的 JSESSIONID,说明该系统认为访问这两个页面是不同的用户,即使事实上是同一个用户访问的。

JSESSIONID 的作用来看,JSESSIONID 和用户登录没有直接关系。所以模拟登录的时候,依旧只需要使用访问首页时生成的 cookie 即可。怎么验证呢?可以在 Chrome 开发者工具的 Application 面板中,找到 cookie 里面的 JSESSIONID 字段,并删除,然后刷新页面,会发现又生成了另一个新的 JSESSIONID。所以不论 JSESSIONID 是什么值,我们都可以登录。所以 JSESSIONID 不会影响模拟登录。

2.2 结论

  • 进行模拟登录,和用户登录有关的 cookie 信息是访问首页时生成的 cookie
  • 访问首页和登录页面时,JSESSIONID 虽然会发生变化,但 JSESSIONID 并不会影响用户通过账号和密码进行认证

2.3 代码

这部分没有直接的代码。但因为接下来要进行模拟登录,所以肯定又会再写一个 request 发送 HTTP 请求,所以现在可以把之前的代码结构优化一下:

// login.js

const request = require('request');


const errorText = {
  account: '用户名或密码错误',
  emptyPassword: '借阅证密码不能为空',
  emptyNumber: '借阅证号不能为空',
};
const url = {
  // 图书馆手机首页
  home: 'http://m.5read.com/395',
  // 登陆 URL
  login: 'http://mc.m.5read.com/irdUser/login/opac/opacLogin.jspx',
};
const regexp = {
  number: number: /^\d+$/,
};


/**
 * 获取 cookie
 * @method getCookie
 * @param  {object}   options  HTTP请求设置信息
 * @param  {Function} callback
 * @return {string}   {error, HTTP响应中的cookie}
 */
const getCookie = (options, callback) => {
  request(options, (error, response) => {
    if (error) {
      return callback({ error, code: 1018 });
    }
    const cookie = response.headers['set-cookie'].join();
    return callback(null, cookie);
  });
};


getCookie({url: url.home}, (error, resHome) => {
  if (error) {
    console.error('获取首页 cookie 失败: \n', error);
    return false;
  }
  const cookieHome = resHome.cookie;
  console.log('首页cookie:\n ', cookieHome);
});

3. 模拟登录

3.1 分析

前面做了那么多分析,主要就是为了登录的时候,发送正确的 cookie。在最终模拟登录之前,还需要做一点分析。

现在需要做的就是,通过学号和密码登录,并继续查看 HTTP 请求,找到登录认证的接口,并分析请求头和响应头。

下面是我输入正确的学号和密码之后,HTTP 请求:

请求头:

crawler-for-scu-lib-2-scu-request-header.png

响应头:

crawler-for-scu-lib-2-scu-response-header.png

数据:

crawler-for-scu-lib-2-scu-request-data.png

从请求头中可以发现,用户登录的 URL 是 http://mc.m.5read.com/irdUser/login/opac/opacLogin.jspx,HTTP Method 是 POST,需要传递的数据是 schoolid=学校编号&backurl=&userType=0&username=xxxxxx&password=xxx,并且是通过表单的方式传递的数据:Content-Type: application/x-www-form-urlencoded。当然,发送 HTTP 请求时,请求头中还有 cookie。除了 JSESSIONID 是新生成之外,其余 cookie 都是访问首页时生成的。

登录成功后,再去查看 cookie ,就会发现 cookie 已经更新为响应头中 set-cookie 中的字段和值了。

3.2 结论

URL: http://mc.m.5read.com/irdUser/login/opac/opacLogin.jspx
Method: POST
Content-Type:application/x-www-form-urlencoded
Cookie: ...               // 访问首页时生成的 cookie
Form Data: {
  schoolid:395,           // 学校代码
  backurl: ''             // 登录后跳转的 URL
  userType: 0,            // 登录时的账号类型,0 表示学号密码登录
  username: 000000000000, // 学号
  password: 000000,        // 密码
}

3.3 代码

登录模块的完整代码如下:

// login.js

const request = require('request');


const errorText = {
  account: '用户名或密码错误',
  emptyPassword: '借阅证密码不能为空',
  emptyNumber: '借阅证号不能为空',
};
const schoolid = 395;
const url = {
  // 图书馆手机首页
  home: 'http://m.5read.com/395',
  // 登陆 URL
  login: 'http://mc.m.5read.com/irdUser/login/opac/opacLogin.jspx',
};
const regexp = {
  number: /^\d+$/,
};


/**
 * 获取 cookie
 * @method getCookie
 * @param  {object}   options  HTTP请求设置信息
 * @param  {Function} callback
 * @return {string}   {error, HTTP响应中的cookie}
 */
const getCookie = (options, callback) => {
  request(options, (error, response) => {
    if (error) {
      return callback({ error, code: 1018 });
    }
    const cookie = response.headers['set-cookie'].join();
    return callback(null, cookie);
  });
};


/**
 * 模拟登录操作
 * @method doLogin
 * @param  {object}   options  HTTP 请求信息
 * @param  {string}   cookie   cookie
 * @param  {Function} callback 回调函数
 * @return {object}   {error, 登录成功后的cookie}
 */
const doLogin = (options, callback) => {
  request(options, (error, response, body) => {
    if (error) {
      return callback({ error });
    }
    if (body.indexOf(errorText.account) !== -1) {
      return callback({
        error: errorText.account,
        code: 1019,
      });
    }
    if (body.indexOf(errorText.emptyPassword) !== -1) {
      return callback({
        error: errorText.emptyPassword,
        code: 1020,
      });
    }
    if (body.indexOf(errorText.emptyNumber) !== -1) {
      return callback({
        error: website.errorText.emptyNumber,
        code: 1021,
      });
    }
    const cookieLogined = response.headers['set-cookie'].join();
    return callback(null, cookieLogined);
  });
};


/**
 * 模拟登录
 * @method login
 * @param  {string}   number   学号(借阅证号)
 * @param  {string}   password 密码
 * @param  {Function} callback 回调函数
 * @return {object}   登录成功后的cookie
 */
const login = (number, password, callback) => {
  // 验证 number
  if (!regexp.number.test(number)) {
    return callback({ code: 1016, error: '登录移动图书馆学号格式错误' });
  }
  // 验证 password
  if (!regexp.number.test(password)) {
    return callback({ code: 1017, error: '登录移动图书馆密码格式错误' });
  }
  // 获取图书馆首页 cookie
  getCookie({ url: url.home }, (errHome, cookieHome) => {
    if (errHome) {
      console.log('获取图书馆首页 cookie 失败: \n', errHome);
      return callback({
        code: errHome.code,
        error: errHome.error,
      });
    }
    console.log('首页cookie:\n ', cookieHome);
    // 模拟登录
    const options = {
      url: url.login,
      form: {
        schoolid: schoolid,
        backurl: '',
        userType: 0,
        username: number,
        password,
      },
      headers: {
        Cookie: cookieHome,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      method: 'POST',
    };
    doLogin(options, (errLogin, cookieLogined) => {
      if (errLogin) {
        console.log('登录失败: \n', errLogin);
        return callback({
          code: errLogin.code,
          error: errLogin.error,
        });
      }
      console.log('登录成功后的 cookie:\n ', cookieLogined);
      return callback(null, cookieLogined);
    });
  });
};


module.exports = login;

4. 抓取借阅信息

为了验证登录后的 cookie 是不是最终正确,访问一下需要登录后才能访问的页面即可。

所以下面就来抓取借阅信息。为了解析 HTML 文本,我们还需要用到 cheerio 这个包。cheerio 就相当于是服务端的 jQuery,可以像使用 jQuery 选择器一样从一个 HTML 文本中取出想要的内容。

新建一个 get_books.js 文件,添加如下代码:

// get_books.js
const request = require('request');
const cheerio = require('cheerio');
const login = require('./login');

const url = {
  books: 'http://mc.m.5read.com/cmpt/opac/opacLink.jspx?stype=1',
}
const errorText = {
  cookieTips: '请确认您的浏览器Cookie开启和正常访问移动图书馆首页'
};

const number = '0000000000000'; // 学号(借阅证号)
const password = '000000';  // 密码

const fetchBooks = (cookie, callback) => {
  const options = {
    url: url.books,
    headers: {
      Cookie: cookie,
    },
  };

  request(options, (error, response, body) => {
    if (error) {
      console.log('获取图书借阅列表失败: ', error);
      return callback({
        code: 1025,
        error: '获取图书借阅列表失败',
        detail: error,
      });
    }
    console.log('response.statusCode: ', response.statusCode);
    if (response.statusCode !== 200) {
      return callback({
        code: 1026,
        error: '获取图书借阅列表失败',
        detail: response,
      });
    }
    return callback(null, body);
  });
};


const parseBooks = (html, callback) => {
  if (html.indexOf(errorText.cookieTips) !== -1) {
    console.log(errorText.cookieTips);
    return {
      code: 1027,
      error: '移动图书馆系统 cookie 信息过期,请重新登录',
      detail: html,
    };
  }
  const $ = cheerio.load(html, {
    ignoreWhitespace: true,
    xmlMode: false,
    lowerCaseTags: false,
  });
  const domBooks = $('.boxBd').find('.sheet');
  const booksNumber = domBooks.length; // 借阅数量
  // console.log(domBooks.length);
  const books = [];
  domBooks.each(function () {
    const barCodeValue = $(this).find('td').eq(5).find('form input')
        .eq(0)
        .attr('value');
    const borIdValue = $(this).find('td').eq(5).find('form input')
        .eq(1)
        .attr('value');
    books.push({
      // 作者
      author: $(this).find('td').eq(0).text(),
      // 书名
      name: $(this).find('td').eq(1).text(),
      // 应还日期
      expiredate: $(this).find('td').eq(2).text(),
      // 分馆
      libraryBranch: $(this).find('td').eq(3).text(),
      // 索书号
      number: $(this).find('td').eq(4).text(),
      borId: borIdValue,
      barCode: barCodeValue,
    });
  });
  return callback(null, {
    booksNumber,
    books,
  });
};

login(number, password, (error, cookie) => {
  if (error) {
    return console.log(error);
  }
  // 获取借阅列表页面html
  fetchBooks(cookie, (errFetch, resFetch) => {
    if (errFetch) {
      return console.log(errFetch);
    }
    // 解析借阅列表html
    parseBooks(resFetch, (errParse, resParse) => {
      if (errParse) {
        console.log('errParse: ', errParse);
        return console.log(errParse);
      }
      return console.log(null, { books: resParse });
    });
  });
});

安装 npm install cheerio --save,将 numberpassword 改为正确的借阅证号和密码就可以登录成功,并获取到该用户的借阅列表了。

运行结果如下:

$ node get_books.js
首页cookie:
  JSESSIONID=6EA8121AB1E4B3045A331198321F8ADC.irdmblhome72a; Path=/; HttpOnly,DSSTASH_LOG=C%5f4%2dUN%5f395%2dUS%5f%2d1%2dT%5f1475865139485; Domain=.5read.com; Path=/,mgid=274; Domain=.5read.com; Expires=Sun, 06-Nov-2016 18:32:19 GMT; Path=/,maid=395; Domain=.5read.com; Expires=Sun, 06-Nov-2016 18:32:19 GMT; Path=/,msign_dsr=1475865139507; Domain=.5read.com; Expires=Thu, 02-Oct-2036 18:32:19 GMT; Path=/,mduxiu=musername%2c%3dblmobile%2c%21muserid%2c%3d1000086%2c%21mcompcode%2c%3d1009%2c%21menc%2c%3d26A35FCD85F5A5677706DC7CE503113A; Domain=.5read.com; Expires=Sun, 06-Nov-2016 18:32:19 GMT; Path=/,xc=6; Domain=.5read.com; Expires=Sun, 06-Nov-2016 18:32:19 GMT; Path=/
登录成功后的 cookie:
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
response.statusCode:  200

{ booksNumber: 2,
  books:
   [ { author: '李刚',
       name: '疯狂Swift讲义',
       expiredate: '20161010',
       libraryBranch: 'JZLKS',
       number: 'TP312SW/4072',
       borId: 'U13014748',
       barCode: '90577318' },
     { author: '杨宏焱',
       name: 'iOS 8 Swift编程指南',
       expiredate: '20161010',
       libraryBranch: 'JZLKS',
       number: 'TP312SW/4739',
       borId: 'U13014748',
       barCode: '90597040' },
    ]
}

到目前为止,模拟登录的程序就完成了,并且成功获取到了用户的借阅列表。

由于很多很多大学的移动图书馆都使用同一个系统,所以这个程序具有通用性,所以本文的标题是《模拟登录某某大学图书馆系统》。

不信你试试,说不定就有你的学校。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions