-
[학교 내 빈 강의실] Step 1Project/개인 2020. 3. 8. 21:22
계기
동기 형이랑 이런저런 이야기를 나누다 학교 개강도 미뤄졌겠다 혼자 하기 괜찮은 프로젝트가 무엇이 있을까라는 주제가 나왔다. 요새 인터넷을 많이 하는 나에겐 웹 크롤링에 관한 프로젝트로 방향을 잡고 있었는데, 마침 형이 나한테 자기의 비밀 아이디어라면서 이렇게 주제를 던져주었다.
"학교 내 빈 강의실을 찾아주는 거 어때?"
형은 학교를 다니면서 310관에서 주로 활동하는 우리가 일부러 공강 시간에 맞춰 도서관가기도 귀찮고, 또 시험기간에는 한 개방 강의실에서 수업이라도 있으면 다른 강의실을 찾아가야 하는 게 귀찮아서 저런 생각이 떠올랐다고 하였다.
"음.. 웹 크롤링으로 일단 모든 강의 시간표를 가져오고.. 현재 시간과 비교하여 사용자가 몇 층인지만 입력하면 현재 층 기준으로 찾아주면 좋겠네."
나는 이러한 생각이 들었고, 형한테 고맙다고 말하고 다음 날부터 하나하나 프로젝트 진행을 시작했다.
시작
지금까지 노드로 크롤링을 해본 적이 없었기 때문에, 첫날엔 간단하게 웹 크롤링을 하는 방법에 대해 공부를 했다.
구글에 검색하니, 웹 크롤링에 필요한 모듈은 두 가지. Axios와 Cheerio였다. 이 두 가지 모듈을 이용해 네이버 뉴스를 크롤링 해 보았다.
네이버 뉴스처럼 학교 포탈 사이트의 데이터를 가져오려고 하니, 예상치 못한 문제가 발생하였다. 그건 바로 학교 포탈 사이트의 경우, 로그인을 해야 모든 기능을 제대로 수행할 수 있다는 것이다. 흠.. 이럴 땐 어떤 모듈을 사용해야하나 검색하니 puppeteer(이하 퍼펫티어)라는 모듈을 사용하면 headless chrome을 통해 로그인, 버튼 클릭 등 마치 매크로처럼 설정해서 html 데이터를 가져올 수 있다고 한다.
바로 퍼펫티어를 이용해 학교에 로그인하고, 강의 시간표 대신 내 시간표를 가져오는 코드를 작성해 테스트 해 보았다.
const puppeteer = require('puppeteer'); const cheerio = require('cheerio'); (async () => { const browser = await puppeteer.launch({headless : false}); const page = await browser.newPage(); const _id = "gusdn7236"; const _pw = "wh71437236."; await page.goto('https://mportal.cau.ac.kr/common/auth/SSOlogin.do?redirectUrl=/main.do',{waitUntil: 'networkidle2'}); await page.evaluate((id, pw) => { document.querySelector('#txtUserID').value = id; document.querySelector('#txtPwd').value = pw; }, _id, _pw); await page.click('.btn-login'); await page.waitFor(500); await page.goto('https://mportal.cau.ac.kr/main.do',{waitUntil: 'networkidle2'}); await page.waitForSelector('img') await page.content() .then (html => { let ulList = []; let day = ['월','화','수','목','금','토']; const $ = cheerio.load(html); const $timetable = $('div.nb-p-08-weeks div').children('ul'); $timetable.each(function(i, elem) { ulList[i] = { day: day[i] + '요일', title: $(this).find('em.ng-binding').text(), time_start: $(this).find('li.ng-scope').attr('data-st'), time_end: $(this).find('li.ng-scope').attr('data-ex'), }; }); const data = ulList.filter(n => n.title); return data; }) .then(res => { const response = { statusCode : 200, body: JSON.stringify(res) }; return response; }); await browser.close(); })();
6, 7번째 라인에 사용자의 id와 비밀번호를 입력하면, 로그인이 잘 진행되는 걸 확인할 수 있을 것이다.
만약 창이 뜨는 게 싫다면, puppeteer.launch({headless : false});에서 false를 false로 바꿔주면 된다.
이제 모든 강의 계획서 데이터를 가져오면 된다.
학교 사이트에 들어가보자.
학교 사이트에 들어가 강의계획서를 검색해보니, 검색 조건이 최소 2글자를 입력해야 하는 것이었다.
버프스위트를 통해 요청 값을 잡아보았다.
{"year":"2020","shtm":"1","choice":"sbjt","searchnm":"경영"}
searchnm 항목을 지워 요청을 보내니 모든 강의 계획서가 화면에 출력되는 걸 확인할 수 있었다.
그렇게 request body에서 searchnm을 제거하여 모든 데이터를 가져오는 걸 코드로 어떻게 구현하지라는 의문이 떠올랐고, 이를 위해 구글에 검색해보았다.
그 결과, 퍼펫티어를 이용해서 가져오는 방법보다는 request라는 모듈을 사용해서 학교 서버에 데이터를 요청하는 편이 더 나은 방법이란 것을 알게 됐다.
새로운 고비
처음엔 단순히 학교에 리퀘스트 모듈을 사용해서 POST 요청을 보내고, 그에 따른 응답을 가져오면 될 것 같았다.
허나 학교의 포탈같은 사이트는 세션과, 쿠키를 이용해 사용자 인증 과정을 거친다. 항성 달라지며, 서버측에서 관리하는 세션 값을 어떻게 알아내 요청해야할 지 감이 오질 않았다.
말이 너무 장황한데.. 정리하자면, 문제는 다음과 같다.
1. 현재 내 서버에서 request를 요청하면 세션값이 없기 때문에 요청은 실패한다.
2. 퍼펫티어를 사용해서 데이터를 가져오자니, request body 데이터를 수정해야 한다.퍼펫티어를 이용해서 로그인 하는 것까지는 좋지만, 그 이후에 어떻게 처리해야할 지 감이 전혀 오지 않았다.
차선책
그렇게 하루정도 계속 고민하고 방법을 찾다가, 차선책을 찾게 되었다. (얼마나 기뻤는 지 모른다..)
바로 학교의 옛날 사이트 중에 위와 같이 강의계획서를 조회할 수 있는 사이트가 있던 것이다.
강의계획서 이 사이트 또한 검색 조건이 두 글자 이상이었지만, 이 부분은 request를 이용해 쉽게 해결할 수 있었다.
const puppeteer = require('puppeteer'); const cheerio = require('cheerio'); const request = require('request'); const iconv = require('iconv-lite'); const fs = require('fs'); const formData ={ year: 2020, shtm: 1, choice:'sbjt' } const building_name = '310관' request .post({url:'https://campus.cau.ac.kr/servlet/UskLecPl10', form : formData}) .on('response', function(response) { console.log(response.statusCode) // 200 console.log(response.headers['content-type']) // 'image/png' }) .pipe(iconv.decodeStream('euc-kr')).collect(function(err, decodedBody) { const $ = cheerio.load(decodedBody); let lectureList = []; const $body = $('table tbody').children('tr'); $body.map((i, elem) => { let text = $(elem).children('td').text(); if(text.includes(building_name)){ text = text.split('\n\n\t\t\t'); if(text[7] && text[7] !== '재택'){ lectureList.push({ campus : text[0], course : text[1], code : text[2], sbj : text[3], faculty : text[4], major : text[5], professor : text[6], lecture : text[7] }); } } }) fs.writeFile('./timetable.json',JSON.stringify(lectureList),(err) => { if(err){ throw err; } console.log('완료') }) });
310관 데이터를 가져오는 코드
formdata 부분이 요청 바디가 되는 부분이다. 원래는 q_txt라고 과목명에 해당하는 key값이 존재하지만, 이 부분을 제거하고 요청을 보내면 데이터를 가져올 수 있다.
저 코드를 짜는데도 한 4시간 걸렸던 것 같다. 데이터를 가져오는 데는 성공했는데, 문제는 한글이 깨진다는 것이었다. 이에 대한 건 스트림을 살펴보면 된다.
한 3일을 거쳐 어째저째 해서 이렇게 프로젝트의 첫 단계인 데이터를 가져오는 데 성공했다.