模擬登錄某某大學圖書館系統
為什麼說是某某大學?往下看了就知道了 ??
對於爬蟲程序,如果需要抓取的頁面,需要登錄後才能訪問,這時一般就需要進行模擬登錄了。由於最近需要抓取登錄四川大學圖書館後的一些信息,所以以此為例詳細說明整個分析和編碼過程。
總的來說,對於一般系統的模擬登錄分為三大步驟:
- 分析頁面,得到登錄 URL 和所需要傳遞的數據
- 通過程序向所得 URL 發送數據
- 根據服務端的響應判斷是否登錄成功,若登錄成功,則保存返回的 cookie
只要得到了 cookie,當需要抓取登陸後才能訪問的頁面時,只需要發送 HTTP 請求時,在 HTTP Header 帶上 cookie 即可。
對於寫爬蟲程序,還有一些小技巧(其實目前就總結出來一個):
- 能抓取手機站就抓取手機站,因為手機網站一般比較容易
1. 分析四川大學移動圖書館
1.1. 分析
圖書館系統有一個手機網站,所以優先選擇手機站作為目標。其鏈接是 http://m.5read.com/395。
首先看到這個鏈接的時候,我也是比較奇怪,畢竟這個域名就比較奇怪,川大圖書館系統手機版的域名為什麼不是 scu.edu.cn 的子域名,而且域名的 PATH 部分為什麼是 395。
域名打開後是 四川大學移動圖書館:
然後我把 395 去掉,直接輸入了http://m.5read.com,打開也是川大圖書館首頁。但如果我在另一個沒有打開過該鏈接的瀏覽器中打開 http://m.5read.com/ ,則是 默認單位移動圖書館:
這個時候就得出兩個猜測:
- 一是 URL 中的 395 是學校的編號
- 二是打開http://m.5read.com/395 後,客戶端肯定會生成對應的 cookies ,表示當前客戶端訪問的是 395 這所大學的圖書館系統
為了驗證第一個猜測,我們把 395 改為任意一個其他數字。這樣大概有兩種情況,如 http://m.5read.com/1 則提示 對不起,還沒有開通手機業務!,http://m.5read.com/20 則是廈門大學的圖書館系統,見下圖:
接下來再看看是不是生成了對應的 cookie 信息。在 Chrome 的 開發者工具 -> Application 的左側菜單欄選中 Cookies,然後在右側選中某個 cookie,並點擊右鍵,選擇 Clear All 清除所有 cookies。也可以直接在左側菜單欄 Cookies 展開後的域名上,點擊右鍵,選擇 clear。
因為我的目的是模擬模擬登錄四川大學圖書館系統,所以我還是先訪問http://m.5read.com/395,再來看看 cookies:
可以看到,的確生成了 cookies,結構和之前廈門大學類似。為了弄清楚 cookies 是怎麼生成的,接下來要查看的就是 HTTP 請求的詳細內容了:
可以發現,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(訪問首頁失敗:
, error); return { error }; } const cookie = response.headers[set-cookie]; console.log(cookie:
, cookie);});
程序運行後,如果沒出錯,則會以標準輸出的形式輸出 cookies:
$ node index.jscookie: [ 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 請求:
可以發現,發送請求頭中的 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=1475781234Host: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.jsconst 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 失敗:
, error); return false; } const cookieHome = resHome.cookie; console.log(首頁cookie:
, cookieHome);});
3. 模擬登錄
3.1 分析
前面做了那麼多分析,主要就是為了登錄的時候,發送正確的 cookie。在最終模擬登錄之前,還需要做一點分析。
現在需要做的就是,通過學號和密碼登錄,並繼續查看 HTTP 請求,找到登錄認證的介面,並分析請求頭和響應頭。
下面是我輸入正確的學號和密碼之後,HTTP 請求:
請求頭:
響應頭:
數據:
從請求頭中可以發現,用戶登錄的 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.jspxMethod: POSTContent-Type:application/x-www-form-urlencodedCookie: ... // 訪問首頁時生成的 cookieForm Data: { schoolid:395, // 學校代碼 backurl: // 登錄後跳轉的 URL userType: 0, // 登錄時的賬號類型,0 表示學號密碼登錄 username: 000000000000, // 學號 password: 000000, // 密碼}
3.3 代碼
完整代碼如下:
// login.jsconst 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 失敗:
, errHome); return callback({ code: errHome.code, error: errHome.error, }); } console.log(首頁cookie:
, 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(登錄失敗:
, errLogin); return callback({ code: errLogin.code, error: errLogin.error, }); } console.log(登錄成功後的 cookie:
, cookieLogined); return callback(null, cookieLogined); }); });};module.exports = login;
4. 抓取借閱信息
為了驗證登錄後的 cookie 是不是最終正確,訪問一下需要登錄後才能訪問的頁面即可。所以下面就來抓取借閱信息。為了解析 HTML 文本,我們還需要用到 cheerio 這個包。cheerio 就相當於是服務端的 jQuery,可以像使用 jQuery 選擇器一樣從一個 HTML 文本中取出想要的內容。新建一個 get_books.js 文件,添加如下代碼:
// get_books.jsconst 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,將 number 和 password 改為正確的借閱證號和密碼就可以登錄成功,並獲取到該用戶的借閱列表了。
運行結果如下:
$ 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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxresponse.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 }, ]}
到目前為止,模擬登錄的程序就完成了,並且成功獲取到了用戶的借閱列表。
由於很多很多大學的移動圖書館都使用同一個系統,所以這個程序具有通用性,所以本文的標題是《模擬登錄某某大學圖書館系統》。
不信你試試,說不定就有你的學校。
本文首發於我的博客,歡迎捧場 ?? Crawler for SCU Libirary · nodejh
推薦閱讀:
※拿諾貝爾獎可以長壽?——從爬蟲到簡單數據分析
※Python3爬蟲(5)BeautifulSoup4庫爬取鏈家100頁面
※如何在神箭手上快速開發爬蟲——第二課 如何爬取JS動態生成的數據【豌豆莢遊戲排行榜】
※【爬蟲一】最簡單的爬蟲,零基礎教學