Authenticated page Scraping in Node.js

Issue

  • Node.js 로 웹 스크래핑 하기.
  • Authentication이 필요한 페이지 스크래핑 하기.
  • Xml data parsing, crawling하기.
  • Phantom, headless brower를 이용하여 scraping하기.

아주대 공지봇 공드리를 만들며 파이썬을 이용한 스크래핑을 해본 적이 있다. 하지만 공드리에서도 이슈로 남아 있는 것이 바로 Authentication이 필요한 페이지를 스크래핑 하는 것이다. Node.js를 통한 스크래핑을 알아보면서 Authentication Issue도 해결해보고자 한다. 또한 Xml을 parsing하고 crawling하는 법도 알아본다. 우리학교 홈페이지 중에서 Authentication이 필요한 부분이 바로 학교 포탈이다. 그런데 이 학교 포탈의 클라이언트 상당부분이 java script로 dynamic하게(쓸데 없이) 구성되어 있고,  Response로 html이나 json이 아닌 xml값을 반환해 준다. 이 세가지 이슈를 해결하여 Node.js를 통한 Authentication이 필요한 페이지에서의 xml parsing을 통한 크롤링과 스크래핑을 공부한다.


Modules

이 이슈를 해결하려고 여러가지 시도를 해봤다. 우선 내 실력이 아직 부족해서 우왕좌왕했기 때문이다. 결론적이고 중요한 부분만 정리해본다. http request를 보내기 위한 모듈로 Requests를 사용한다. 그리고 html parsing을 위한 모듈로 Cheerio를 사용한다. 또한 이번 이슈에서 xml를 parsing해야 하기 때문에 xml2js를 사용한다.

  • Request
  • Cheerio
  • xml2js

Scraping in Node.js

학교 포탈 메인 페이지에 접근해서 페이지의 소스를 받아오는 코드이다.

var request = require('request');
var cheerio = require('cheerio');
var options = {
    url: 'http://portal.ajou.ac.kr',
    method:'POST',
    headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100' + randomInt(1, 10)
        + randomInt(1, 3) + randomInt(1, 10) + ' Firefox / 24.0',
        
};

request(options, function (err, response, body) {
 //if(err) throw new Error("something wrong");
 //TODO: indexOf 와 search의 차이점?
 console.log(body);
 var $ = cheerio.load(body);
 var scriptExps = $("script").text().split('\n');
 var i;
 //TODO: 좀 더 빠르게 href(url) parsing 하는 방법은?
 for(i=0; i0){
 var href = scriptExps[i].substring(scriptExps[i].indexOf('"')+1, scriptExps[i].lastIndexOf('"'));
 break
 }
 }
 options.url = options.url+href;
 request(options, function (err, response, body) {
 console.log(body);
 });

request를 인자값의 변화에 따라 이용할 수 있는 방법 중에 가장 기본적인 방법은 request(url, callback function)의 방법이다. 하지만 학교 홈페이지가 크롤러 방지등의 목적으로 웹브라우저를 통한 접근만 허용했기 때문에 우리는 브라우저인 척을 해야할 필요가 있다. 따라서 request(options, callback function)의 형태로 request를 보내고 이를 위해 options 객체를 만들어 준다. 이때 options.headers[‘User agent’] 값을 통해 브라우저인 척을 할 수 있다. 우리는 지금 파이어폭스인 척을 하고 있다.

request(options, function (err, response, body)
request(url, function (err, response, body)

Request에 대한 Response와 그 body 값이 callback function 의 인자로 전달된다. callback functions 에서 response 를 이용해 어떤 작업을 결정해줄지 정하면 된다. 그런데 그리고 response의 body로 온 html소스를 분석(Parsing)하기 위해 cheerio를 사용한다. cheerio.load(body)를 통해 우리가 작업할 소스에 대한 객체를 만들어 준 후에 사용한다. cheerio는 css, jquery selector를 그대로 사용할 수 있는 것이 장점이다.

var $ = cheerio.load(body);

Subissue # jQuery에 대한 기초공부와 이를 바탕으로한 cheerio의 활용은 조금 더 공부해둘 필요가 있을 것 같다!

여기서 cheerio를 통해 script를 뽑아내는 이유는 위 코드의 response로 내가 원하는 메인페이지의 소스가 오지 않고, 그 redirection 주소값이 java script 코드에 포함되어 왔기 때문이다.

var scriptExps = $("script").text().split('\n');

따라서 에서 redirection address를 찾아내고, 그 주소에 다시 request를 보내 드디어 우리가 원하는 메인페이지의 소스값을 받아 출력하는 모습이다.

options.url = options.url+href;
 request(options, function (err, response, body) {
 console.log(body);

Authentication page scraping in Node.js

로그인 인증이 필요한 페이지에 접근해서 스크래핑을 할 수 있는 방법은 다음과 같이 두 가지가 있다.

  • Login 에 필요한 Username과 password를 담아 POST Request 보내기.
  • 크롬 개발자 도구를 통해서 Logged-in 상태에서 보낸 Request의 header값을 이용해 request 보내기

먼저, 더 쉬운 방법인 두번째 방법을 통해서 학교 포탈사이트의 정보를 긁어온다.

스크린샷 2016-10-11 오후 12.26.30.png

  1. 원하는 페이지를 열기 전에 개발자도구를 켜고 Network 탭에 들어간다.
  2. 원하는 페이지를 열면 왼쪽 Name이라 써있는 윈도우에 서버로부터 Response로 받은 데이터들이 표시된다.
  3. 이 중 본인이 원하는 파일을 찾아 우클릭하면 Header를 쉽게 복사할수 있다.

POST /eclass/eclass/findMain.action?taskId=F_STU_LECTDATA&sysType=ECLS&menuCode=2002 HTTP/1.1

Host: eclass.ajou.ac.kr

Connection: keep-alive

Content-Length: 0

Origin: https://eclass.ajou.ac.kr

accept-language: ko

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36

Content-Type: application/x-www-form-urlencoded

Accept: */*

Referer: https://eclass.ajou.ac.kr/eclass/findMainStud.action

Accept-Encoding: gzip, deflate, br

Cookie:PHAROS_VISITOR=g42BKBcooJS;JSESSIONID=aI8DeLfmzBO1TCg6teS7e4EctKmj28aCmkC5KK2gTxVYVlhbQOsfaHjikXikwAid.daechung_servlet_engine2;SSOGlobalLogouturl=get^http://portal.ajou.ac.kr/com/sso/logout.jsp$;ssotoken=0d2UOUi%2FBmFFfIfOWDNVSgm%2FKZg%2Fx5LOt73BMlG%2BSQdcqOGlFXdX8uRzqXnW8AFY8Yl6TJ6Pn%2BYthGkbANxuq0KKJ91VwmI7bS2Hq37EIMsnDI8GbnFN714QoykVjnAssA8AVquaE4T0QjUN8V5f2w%3D%3D;USER_KEY3=afa5a45ab51a8ed5a32d9c9f9ffd3263;AUDIT9_ID=201120901;USER_KEY=afa5a45ab51a8ed5a32d9c9f9ffd3263;USER_KEY2=afa5a45ab51a8ed5a32d9c9f9ffd3263

위와 같이 복사된 헤더를 아래와 같이 코드상의 options.header에 Json 형식으로 넣어준다. 이 때 크롤러 감지 회피를 위해 User-Agent를 위의 코드와 같이 바꿔주고, url의 https://를 http://로 바꿔준다. 이는 https://의 보안이슈를 해결하기위한 정보를 코드에서 보내주는 Request가 담고있지 않기 때문이다.

Subissue # https://와 http://의 차이 정확히 알기.

var options = {
    url: 'http://eclass.ajou.ac.kr/eclass/eclass/findMain.action?taskId=F_STU_LECTDATA&sysType=ECLS&menuCode=2002',
    method:'POST',
    headers: {
        'Host': 'eclass.ajou.ac.kr',
        'Connection': 'keep-alive',
        'Content-Length': 0,
        'Origin': 'https://eclass.ajou.ac.kr',
        'accept-language': 'ko',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Accept: */*',
        'Referer': 'https://eclass.ajou.ac.kr/eclass/findMainStud.action',
        'Accept-Encoding': 'gzip, deflate, br',
        'Cookie': 'PHAROS_VISITOR=g42BKBcooJS; JSESSIONID=aI8DeLfmzBO1TCg6teS7e4EctKmj28aCmkC5KK2gTxVYVlhbQOsfaHjikXikwAid.daechung_servlet_engine2; SSOGlobalLogouturl=get^http://portal.ajou.ac.kr/com/sso/logout.jsp$; ssotoken=0d2UOUi%2FBmFFfIfOWDNVSgm%2FKZg%2Fx5LOt73BMlG%2BSQdcqOGlFXdX8uRzqXnW8AFY8Yl6TJ6Pn%2BYthGkbANxuq0KKJ91VwmI7bS2Hq37EIMsnDI8GbnFN714QoykVjnAssA8AVquaE4T0QjUN8V5f2w%3D%3D; USER_KEY3=afa5a45ab51a8ed5a32d9c9f9ffd3263; AUDIT9_ID=201120901; USER_KEY=afa5a45ab51a8ed5a32d9c9f9ffd3263; USER_KEY2=afa5a45ab51a8ed5a32d9c9f9ffd3263',
    }
};
request(options, function (err, response, body) {
   console.log(body);
});

이 헤더의 정보 중에서 Cookie가 Authentication과 관련된 부분이다. 이 쿠키를 통해서 이 유저가 로그인 된 유저인지 아닌지, 어떤 유저인지를 파악한다. 따라서 Request header를 따옴으로써 Authentication 이 필요한 정보에 접근을 할 수 있다. 그러나 발생한 문제점은 시간이 지남에 따라 쿠키가 변경되고 기존의 쿠키의 효력이 사라진다는 것이다. 따라서 이 포스트에서 사용한 방법으로는 크롤러나 스크래퍼를 만드는데는 한계가 있다. 나는 같은 페이지를 켜놓고 아무런 작업을 하지 않았는데 어떻게 다음 작업을 요청할때 혼자 쿠키가 바뀌어 Request가 전송되는 것일까?

결과로 아래와 같이 학교 포탈에서 강의자료 탭의 정보들을 얻어올 수 있다.

Related issue # 강의자료 xml로 str data만 response로 표현된 형태에서 첨부파일은 어떻게 가져올 수 있을까?

<code>

<?xml version=”1.0″ encoding=”utf-8″?>
<root>
<dataset id=”DS_LECTDATA”>
<colinfo id=”openYear” size=”4″ type=”STRING” />
<colinfo id=”openSmst” size=”8″ type=”STRING” />
<colinfo id=”clubNm” size=”4000″ type=”STRING” />
<colinfo id=”sysType” size=”4″ type=”STRING” />
<colinfo id=”clubId” size=”50″ type=”STRING” />
<colinfo id=”menuCode” size=”10″ type=”STRING” />
<colinfo id=”menuNo” size=”5″ type=”STRING” />
<colinfo id=”noteSeq” size=”22″ type=”DECIMAL” />
<colinfo id=”regiNum” size=”22″ type=”DECIMAL” />
<colinfo id=”title” size=”1000″ type=”STRING” />
<colinfo id=”fileYn” size=”1″ type=”STRING” />
<colinfo id=”drctOpenYn” size=”1″ type=”STRING” />
<colinfo id=”openStDt” size=”10″ type=”STRING” />
<colinfo id=”noteCont” size=”4000″ type=”STRING” />
<colinfo id=”folderDt” size=”50″ type=”STRING” />
<colinfo id=”readFg” size=”1″ type=”STRING” />
<record>
<openYear>2016</openYear>
<openSmst>2</openSmst>
<clubNm>웹시스템설계 (X549-1)</clubNm>
<sysType>ECLS</sysType>
<clubId>S201622016079271</clubId>
<menuCode>2002</menuCode>
<menuNo>1</menuNo>
<noteSeq>3765841</noteSeq>
<regiNum>14</regiNum>
<title>12 Connect Express</title>
<fileYn>Y</fileYn>
<drctOpenYn>Y</drctOpenYn>
<openStDt>2016/10/11</openStDt>
<noteCont><![CDATA[<P>&nbsp;</P>]]></noteCont>
<folderDt></folderDt>
<readFg>Y</readFg>
</record>
<record>
<openYear>2016</openYear>
<openSmst>2</openSmst>
<clubNm>선형대수1 (X605-3)</clubNm>
<sysType>ECLS</sysType>
<clubId>S201622016073743</clubId>
<menuCode>2002</menuCode>
<menuNo>1</menuNo>
<noteSeq>3764224</noteSeq>
<regiNum>6</regiNum>
<title>Quiz1 풀이</title>
<fileYn>Y</fileYn>
<drctOpenYn>Y</drctOpenYn>
<openStDt>2016/10/09</openStDt>
<noteCont><![CDATA[Quiz1 풀이와 문제입니다.<br>]]></noteCont>
<folderDt></folderDt>
<readFg>Y</readFg>
</record>
<record>
<openYear>2016</openYear>
<openSmst>2</openSmst>
<clubNm>웹시스템설계 (X549-1)</clubNm>
<sysType>ECLS</sysType>
<clubId>S201622016079271</clubId>
<menuCode>2002</menuCode>
<menuNo>1</menuNo>
<noteSeq>3764119</noteSeq>
<regiNum>13</regiNum>
<title>11 REST Form</title>
<fileYn>Y</fileYn>
<drctOpenYn>Y</drctOpenYn>
<openStDt>2016/10/09</openStDt>
<noteCont><![CDATA[<P></P>]]></noteCont>
<folderDt></folderDt>
<readFg>Y</readFg>
</record>
<record>
<openYear>2016</openYear>
<openSmst>2</openSmst>
<clubNm>과학기술과 법 (X506-2)</clubNm>
<sysType>ECLS</sysType>
<clubId>S201622016076682</clubId>
<menuCode>2002</menuCode>
<menuNo>1</menuNo>
<noteSeq>3763952</noteSeq>
<regiNum>4</regiNum>
<title>강의자료</title>
<fileYn>Y</fileYn>
<drctOpenYn>Y</drctOpenYn>
<openStDt>2016/10/08</openStDt>
<noteCont><![CDATA[<p></p>]]></noteCont>
<folderDt></folderDt>
<readFg>Y</readFg>
</record>
<record>
<openYear>2016</openYear>
<openSmst>2</openSmst>
<clubNm>선형대수1 (X605-3)</clubNm>
<sysType>ECLS</sysType>
<clubId>S201622016073743</clubId>
<menuCode>2002</menuCode>
<menuNo>1</menuNo>
<noteSeq>3763024</noteSeq>
<regiNum>5</regiNum>
<title>선형 대수 Sage</title>
<fileYn>Y</fileYn>
<drctOpenYn>Y</drctOpenYn>
<openStDt>2016/10/06</openStDt>
<noteCont><![CDATA[<p>선형 대수의 공학적 도구 입니다.<br></p>]]></noteCont>
<folderDt></folderDt>
<readFg>Y</readFg>
</record>
</dataset>
<dataset id=”DS_SOS_PARAM”>
<colinfo id=”taskId” size=”255″ type=”STRING” />
<colinfo id=”sysType” size=”255″ type=”STRING” />
<colinfo id=”menuCode” size=”255″ type=”STRING” />
<colinfo id=”S_USER_ID” size=”255″ type=”STRING” />
<colinfo id=”S_IP_ADDR” size=”255″ type=”STRING” />
<record>
<taskId>F_STU_LECTDATA</taskId>
<sysType>ECLS</sysType>
<menuCode>2002</menuCode>
<S_USER_ID>201120901</S_USER_ID>
<S_IP_ADDR>59.18.200.105</S_IP_ADDR>
</record>
</dataset>
<dataset id=”DS_PAGE_LINK”>
<colinfo id=”totalPageno” size=”255″ type=”STRING” />
<colinfo id=”pageNavi” size=”255″ type=”STRING” />
<record>
<totalPageno>11</totalPageno>
<pageNavi><![CDATA[<table border=’0′ cellspacing=’0′><tr height=’7′><td></td></tr><tr><td> </td> <td width=’12’ align=’center’><span class=’nowPage’>1</span></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(2);” class=’pageClick’> 2 </a></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(3);” class=’pageClick’> 3 </a></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(4);” class=’pageClick’> 4 </a></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(5);” class=’pageClick’> 5 </a></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(6);” class=’pageClick’> 6 </a></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(7);” class=’pageClick’> 7 </a></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(8);” class=’pageClick’> 8 </a></td> <td width=’12’ align=’center’><a href=”javascript:PageClick(9);” class=’pageClick’> 9 </a></td> <td width=’17’ align=’center’><a href=”javascript:PageClick(10);” class=’pageClick’> 10 </a></td> <td>&nbsp;<a href=”javascript:PageClick(11);”><img src=’/sos_images/comm/ne1.gif’ alt=’Next’/></a> <a href=”javascript:PageClick(11);”‘><img src=’/sos_images/comm/ne2.gif’ alt=’End’/></a></td></tr></table>]]></pageNavi>
</record>
</dataset>
</root>

</code>

Xml parsing

  • xml?
  • <![CSDM[…]]>
  • xsl

xml의 주요한 특징들은 아래와 같다.

XML Does Not DO Anything

The Difference Between XML and HTML

  • XML was designed to carry data – with focus on what data is
  • HTML was designed to display data – with focus on how data looks
  • XML tags are not predefined like HTML tags are

XML Does Not Use Predefined Tags

XML Simplifies Things

  • It simplifies data sharing
  • It simplifies data transport
  • It simplifies platform changes
  • It simplifies data availability

XML is Often a Complement to HTML

XML Separates Data from HTML

XML Tree Structure

Response로 받은 위의 xml 코드에서 우리가 필요한 부분을 얻어내보자. 우선 xml2js를 이용해서 xml 소스를 JavaScript 객체로 반환한다.

request(options, function (err, response, body) {
    if (body){
        console.log(body);
        var parser = new xml2js.Parser();
        parser.parseString(body, function (err, result) {
             //do something
        }
     }
}

여기서 parser가 parseString 함수를 실행하면 body에 들어 있는 xml소스를 javascript 객체로 바꾸어 callback 함수의 인자로 전달한다. xml의 각 노드가 하나 객체로 구성되어 있다. 우리가 찾는 데이터는 xml tree structure에서 <root> 하위에 있는 <record>  하위에 있는 <dataset> 하위에 <record>들 중 첫번째 <record>이다. 다음과 같이 이 정보에 접근할 수 있다.(자바스크립트 객체니까!)

var lectureNotesInXml = result.root.dataset[0].record;

그 중에서 우리는 강의명과 글 제목, 글 내용만 따오기로 한다.

var lectureNotes = [];
var i;
for (i=0; i<lectureNotesInXml.length; i++){
    var newLectureNote = {};
    newLectureNote.lecture = lectureNotesInXml[i].clubNm[0];
    newLectureNote.title = lectureNotesInXml[i].title[0];
    newLectureNote.description = lectureNotesInXml[i].noteCont[0];

여기서 description 부분이 html코드로 감싸져 있기 때문에 이 부분을 플레인 텍스트로 전환하는 과정을 거쳐야 하는데, cheerio를 사용한다.

var $ = cheerio.load(newLectureNote.decription);
newLectureNote.decription = $.text();

보너스로 결과물을 Json 형식으로 바꿔 출력하는 부분 까지

for (i=0; i<lectureNotes.length; i++){
    console.log(lectureNotes[i]);
    console.log(JSON.stringify(lectureNotes[i]));
}

{ title: ‘[과학기술과 법 (X506-2)] 중간고사’,
description: ‘학생여러분께중간고사관련 사항을 공지합니다.  중간고사는 10월 21일 (금)에 진행됩니다.시험범위는 시험 전 배운 곳까지시험형식은 단답형과 O/X가 포함된 형식으로 50문제시험시간은 10:30 – 11:20 (50분) 입니다. 시험시간을 반드시 준수해주시기 바랍니다.   송선미’ }
{“title”:”[과학기술과 법 (X506-2)] 중간고사”,”description”:”학생여러분께중간고사관련 사항을 공지합니다.  중간고사는 10월 21일 (금)에 진행됩니다.시험범위는 시험 전 배운 곳까지시험형식은 단답형과 O/X가 포함된 형식으로 50문제시험시간은 10:30 – 11:20 (50분) 입니다. 시험시간을 반드시 준수해주시기 바랍니다.   송선미”}

짠!

하지만 위에도 설명했듯이 현재의 Authentication방법으로는 정기적으로 스크래핑을 하는 스크래퍼, 크롤러를 만들 수 없기 때문에  더 연구를 해봐야 겠다!


Scrapping in client side.

정기적인 스크래핑이 어려웠던 이유는 우리가 Authentication을 필요로 하는 페이지에 접근하기 위해 사용하는 쿠키가 계속 바뀌었던 점이다. 서버측에선 같은 세션을 아직 파괴하지 않은 유저라도 정기적으로 쿠키 값을 바꾸는 것 같았다. 도대체 웹 브라우저들이 내가 아무런 요청을 하지 않아도 이런 서버가 바꾸는 쿠키값에 따라 세션을 유지하는지는 모르겠으나, 웹 브라우저들이 이런 것을 잘해준다면, 웹브라우저를 사용하는 방법도 있다.

Tampers Monkey

Tampermonkey is a free browser extension and the most popular userscript manager. It’s available for Chrome, Microsoft Edge, Safari, Opera Next, and Firefox.

Even though some of the supported browsers have native userscript support, Tampermonkey will give you much more convenience in managing your userscripts. It provides features like easy script installation, automatic update checks, a simple overview what scripts are running at a tab, a built-in editor and there is a good chance that incompatible scripts run fine with Tampermonkey.

쉽게 말해 탬퍼스 멍키에 java script를 등록하고 이 코드를 실행할 url을 등록하면, 해당 url에 브라우저가 접근했을 때 해당 java script 코드를 실행시켜준다. 이를 검색해보면 보통 유튜브에서 동영상이나 음악을 쉽게 다운받기 위한 도구로 사용한다.

Client side에서 Ajax를 갖고 있기 때문에 꼭 서버에서 요청을 보내서 response를 분석할 것이 아니라, Client side에서 데이터를 분석한 뒤 필요한 정보를 Ajax를 통해 서버에 보내주면 되는 것이다. 그리고 이러한 코드가 특정 사이트에서 실행될 수 있도록 탬퍼스 멍키를 사용하는 것이다.

// ==UserScript==
// @name Eclass notice scrapper
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match http://eclass.ajou.ac.kr/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js
// @require https://raw.githubusercontent.com/sparkbuzz/jQuery-xml2json/master/src/xml2json.js
// @grant none
// ==/UserScript==

탬퍼스 멍키에 들어가 새로운 스크립트 파일을 연 후 위와 같이 설정파일을 작성한다. 중요한 부분은 match와  require 부분이다. @match 는 어떤 url에서 해당 스크립트를 실행할 것인지 명시한다. @require를 통해 ajax와 jquery를 사용할수 있도록 한다.

(function($, undefined) {
    $.fn.exists = function() {
        return this.length > 0 ? this : false;
    };

    // Reload the page and run this script every 10 minutes
    //setTimeout(function(){ this.location.reload(); }, 60000);

    (function(send) {
        XMLHttpRequest.prototype.send = function(data) {
            var reLecture = /\[.*\)\]/;
            var reInnerHtmlTag = /(<\/?p[^<]*>)|(<!\[CDATA\[)|(<br>)|(<\/?div[^<]*>)/gi;
            var reLectureCode = /\([A-Z][0-9]*-[0-9]\)/;

            this.addEventListener("readystatechange", function() {
                if(this.readyState == 4 && this.responseURL.match(/eclass\/findMain.action.*flag=class/) ) {
                    /* eClass 패치 */
                    var doc = $.xml2json(this.responseText);
                    //<a href="/eclass/board/findBoard.action?taskId=FILE_DOWN&amp;fileSeq=6061933&amp;regiId=200711108&amp;noteSeq=3776516" style="text-decoration: underline;">17_Angular2(3).pdf</a>

                    var lectureNoticesInXml= [].concat(doc.root.dataset[0].record);
                    var lectureNotieces = [];

                    for (var i = 0; i < lectureNoticesInXml.length; i++) {

                        // Get information about a notice
                        var lecture = lectureNoticesInXml[i].title.match(reLecture)[0];
                        var newLectureNotice = {};
                        newLectureNotice.lectureName = lecture.split(reLectureCode)[0].split('[').join("");
                        newLectureNotice.lectureCode = lecture.match(reLectureCode)[0].split('(').join("").split(')').join("");
                        newLectureNotice.title = lectureNoticesInXml[i].title[0].split(reLecture)[1];
                        newLectureNotice.description = lectureNoticesInXml[i].noteCont.replace(reInnerHtmlTag, ' ');

                        lectureNotieces.push(newLectureNotice);
                    }

                    for (i = 0; i < lectureNotieces.length; i++) {
                        console.log(JSON.stringify(lectureNotieces[i]));
                    }
                } else if (this.readyState == 4 && this.responseURL.match(/eclass\/findMain.action.*flag=homeWork/)){
                    /* eClass 패치 */
                    var doc = $.xml2json(this.responseText);
                    (...)
                    // 같은 원리의 파싱 코드이므로 생략.
                }
            });
            send.call(this, data);
        };
    })(XMLHttpRequest.prototype.send);
})(window.jQuery.noConflict(true));

이클래스에 한번 요청을 보내면 여러파일로 쪼개져 응답이 오는 것이 서버 사이드에서의 이슈 중 하나였다. 그래서 특정 부분의 url을 찾아 요청을 보냈는데, 클라이언트 사이드에선 이클래스 서버에서 응답으로 보내준 것들이 완전히 업로드 됐을 때, 즉 readyState==4일 때 해당 업로드된 파일이 우리가 원하던 파일인지 확인한 후 맞다면 파싱하는 작업을 계속한다. 이런 작업이 XMLHttpRequest.send.call이 발생할때 마다 일어날 수 있도록, function을 만들어 XMLHttpRequest.prototype.send를 파라미터를 준 후, 이 프로토타입을 원하는 파일 색인, 파싱 코드로 바꿔버린 후, 마지막 줄에 파라미터로 받은 send(XMLHttpRequest.prototype.send)를 실행시킨다. 이로써 일종의 decorator를 만든 것이다. 이렇게 함에 따라 해당 url에 접근해 새로운 페이지를 로드하기 위한 request를 보낼 때 마다 그 페이지는 readystatechange(readyState==4)에 대한 event handler로서 파일 색인, 파싱 스크립트를 갖게 되는 것이다.  이 event handler의 마지막에 다시 Ajax(XMLHttpRequest)를 통해 우리 서버에 파싱한 데이터를 보내면 우리가 원하던 스크래퍼가 완성된다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

search previous next tag category expand menu location phone mail time cart zoom edit close