수정삭제날짜 : 2024. 2. 14 11:56

[GNU BOB] 2. 서버 및 크롤러 구현 0

포스트 썸네일 사진

https://github.com/ikhyeons/gnuBob


그누밥 위젯에서 사용할 학식 데이터를 얻기 위해서 경상국립대 학식 홈페이지의 데이터를 가져오면 좋겠다고 생각하였다. 이때 Selenium이라는 크롤링 라이브러리를 이용하여 구현하였다.

selenium


패키지 설치

서버로 사용할 Express, selenium을 설치한다.

npm install express

npm install selenium-webdriver

1. Selenium

셀레니움은 웹 테스트를 수행할 수 있도록 해 주는 프레임워크이다. 사람이 직접 웹에 접속해서 해야 하는 동작을 자동화 할 수 있다.


builder의 build메서드를 이용하여 드라이버를 생성하고

해당 드라이버에 url을 입력해서 초기에 진입할 페이지를 지정한다.

let driver = await new Builder().forBrowser(Browser.CHROME).build();

const url =
    "https://www.gnu.ac.kr/main/ad/fm/foodmenu/selectFoodMenuView.do?mi=1341";
await driver.get(url);

// 이렇게 웹페이지를 띄우면 find element로 찾고자 하는 html Element를 지정할 수 있다.
const chilamBtn = await driver.findElement(By.xpath(xpath));

//찾은 html Element에 클릭 이벤트를 작동시키고 싶으면
await chilamBtn.click();

//이벤트 동작 이후 페이지 로딩을 기다려야 한다.
await driver.manage().setTimeouts({ implicit: 500 });

내가 접속해야 하는 페이지는 위 rul로 들어갔을 때, 칠암캠퍼스 버튼 → 학생식당 버튼을 눌러서 얻을 수 있는데,

버튼의 동작을 실행시키기 위해서는 해당 findElement로 받은 요소에서 click이벤트를 사용하여 구현할 수 있다.

2. 데이터의 제공 및 크롤러 구현

서버에서 제공할 내용은 2가지 이다.

크롤링된 데이터를 전달하고, app을 배포하기 위한 다운로드 링크를 제공한다.


다운로드 링크는 파일의 경로를 받아서 제공한다. 

app.get("/download", async (req: Request, res: Response) => {  
    const filepath = `${__dirname}/releaseFile/GNU_BOB_v1.3.5.zip`;  
    const filename = "GNU_BOB_v1.3.5.zip";
    console.log("다운했습니다."); 
    if (fs.existsSync(filepath)) {
    res.download(filepath, filename);
  } else {
    res.status(403).send("해당 파일이 존재하지 않음");
  }
});

 

크롤링한 데이터를 제공하는 것은 처음에는 해당 크롤링 로직을 그대로 api에 작성하여 배포하였으나, 몇 가지 문제가 발생했다.

위젯에서 데이터가 필요할 때마다 계속 요청이 날아오게 될 텐데, 이렇게 되면 서버에서는 계속 driver를 생성하고.. 버튼을 누르고.. 이런 과정을 모두 거치고 난 뒤 json을 제공하게 된다.

그러면 라우터가 응답하는 시간이 매우 오래 걸리고, 서버에 가해지는 부담이 커질 것이다.

그래서 크롤링은 하루에 한 번만 진행하고, 크롤링된 json데이터를 db에 저장한 뒤 요청이 들어왔을 경우에 db의 데이터를 조회하여 제공하게끔 구현하였다.

//크롤링한 데이터를 db에 저장하기!
import { FieldPacket, RowDataPacket } from "mysql2";
import getConnection from "./connection";
import getBobFunc from "./getBobFunc";
import schedule from "node-schedule";

interface DayInBob extends RowDataPacket {  day: string;}
type DataSet<T> = [T[], FieldPacket[]];

async function getBob() {
  const conn = await getConnection();
 //확인로직 
  let today = new Date();
  let year1 = today.getFullYear(); // 년도
  let month1 = today.getMonth() + 1; // 월
  let date1 = today.getDate(); // 날짜
  console.log("오늘날짜 : ", year1, month1, date1);

  const getQuery = "SELECT day FROM bob ORDER BY day DESC LIMIT 1";
  const [thatDayData]: DataSet<DayInBob> = await conn.query(getQuery);
  let thatDay = new Date(thatDayData[0].day);
  let year2 = thatDay.getFullYear(); // 년도
  let month2 = thatDay.getMonth() + 1; // 월
  let date2 = thatDay.getDate(); // 날짜
  if (year1 == year2 && month1 == month2 && date1 == date2) {
    console.log("이미 이번 주 밥을 저장했습니다.");
    conn.release();
  } else {
    //입력 로직
    const bobData = await getBobFunc();
    const bobString = JSON.stringify(bobData);
    const query = "INSERT INTO bob values(DEFAULT, DEFAULT, ?)";
    await conn.query(query, [bobString]);
    conn.release();
  }
  conn.release();
}

getBob();

// 매일 홈페이지에 밥이 갱신되는 시간에 자동으로 크롤링을 진행
const job = schedule.scheduleJob("0 30 1 * * *", async () => {
  getBob();
});


// 저장된 json데이터를 내려주기!
app.get("/", async (req: Request, res: Response) => {
  const conn = await getConnection();
  try {
    const getQuery = "SELECT menu, day FROM bob ORDER BY day DESC LIMIT 1";
    const [thatDayData]: DataSet<BobInfo> = await conn.query(getQuery);
    conn.release();
    const returnData = JSON.parse(thatDayData[0].menu);
    return res.json(returnData);
  } catch (e) {
    console.log(e);
    conn.release();
  }
});


서버에서 할 수 있는 일은 얼추 완성했다.

이제 위젯을 생성하고 위젯에서 데이터만 받아오면 된다.

댓글 펼치기 0