[토이 프로젝트] 자동화된 게임 명부 만들기(1) - GAS 사용
0. 이번 단계 진행 아이디어
IMPORTXML 말고 GAS로 좀 깔끔하게 명부를 자동화해보자. 트리거 활용도 되니깐 더 좋다. 😀
1. apps script 설정하기
IMPORTXML 말고 GAS로 좀 깔끔하게 명부를 자동화해보자. 트리거 활용도 되니깐 더 좋다. 😀
기본적인 설정과 코드는
GAS 설정,
Cheerio 사용 두 개의 글 링크로 대체합니다.
위 글들을 바탕으로 메이플 명부 갱신용 기본 코드를 짜보겠습니다.
GetMapleData.gs
function getContent_(name) { // 캐릭터 검색 결과 페이지를 반환
const baseUrl = "https://maple.gg/u/";
const url = baseUrl + name;
try {
const response = UrlFetchApp.fetch(url)
if (response.getResponseCode() == 200) {
return response.getContentText();
}
else {
return null;
}
}
catch (error) {
Logger.log(error);
return null;
}
}
function getMapleData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("명부"); // 시트 선택
const datarange = sheet.getDataRange(); // 데이터 범위 가져오기
const numRows = datarange.getNumRows(); // 총 행의 수 가져오기
const namePos = 1; // 캐릭터명의 위치는 각 행의 1열임
const worlds = ["루나", "스카니아", "엘리시움", "크로아", "오로라", "베라", "레드", "유니온", "이노시스", "제니스", "아케인", "노바", "리부트1", "리부트2"];
for (var i = 3; i <= numRows; i++) { // 3행부터 데이터가 있음
const name = sheet.getRange(i, namePos).getValue(); // 캐릭터명 가져오기
if (name === "" || worlds.includes(name)) { // 빈칸이거나 캐릭터명이 아닌 월드명일 경우 거름
continue;
}
html = getContent_(name);
if (html === null) {
Logger.log(name + ": 스크래핑 오류 발생!");
continue;
}
const $ = Cheerio.load(html);
console.log(name);
}
}
function setSheetData(sheet, data){
}
getContent_(name)
: 캐릭터명을 인자로 받아 캐릭터 검색 결과 페이지를 반환하는 함수입니다. 오류 발생 시 null을 반환합니다.getMapleData()
: 결과 페이지로부터 명부에 입력할 데이터를 스크래핑하는 함수입니다. 데이터를 긁어내는 기능은 아래에서 추가할 예정입니다.setSheetData()
: 긁어낸 데이터를 각 캐릭터 행에 입력하는 함수입니다. 마찬가지로 아래에서 기능을 추가할 예정입니다.
2. maple.gg 스크래핑하기
1) robots.txt 확인
robots.txt를 확인한 결과 스크래핑에 아무제약이 없음을 확인할 수 있습니다.
2) element 추출하기
이제 앞서 작성한 스크립트에 element를 추출하는 코드를 추가하여 getMapleData()
를 완성해보겠습니다.
maple.gg의 결과페이지에서 selector 경로를 복사하여 cheerio를 통해 각 데이터를 추출하겠습니다.
GetMapleData.gs - getMapleData()
...
function getMapleData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("명부"); // 시트 선택
const datarange = sheet.getDataRange(); // 데이터 범위 가져오기
const numRows = datarange.getNumRows(); // 총 행의 수 가져오기
const namePos = 1; // 캐릭터명의 위치는 각 행의 1열임
const worlds = ["루나", "스카니아", "엘리시움", "크로아", "오로라", "베라", "레드", "유니온", "이노시스", "제니스", "아케인", "노바", "리부트1", "리부트2"];
let data = []; // 시트 입력용 데이터
for (var i = 3; i <= numRows; i++) { // 3행부터 데이터가 있음
const name = sheet.getRange(i, namePos).getValue(); // 캐릭터명 가져오기
if (name === "" || worlds.includes(name)) { // 빈칸이거나 캐릭터명이 아닌 월드명일 경우 거름
continue;
}
html = getContent_(name);
if (html === null) {
Logger.log(name + ": 스크래핑 오류 발생!");
continue;
}
const $ = Cheerio.load(html);
let level, job, guild, mureung, union; // 긁어올 데이터들
if ($('#character-card > div > ul.character-card-summary > li:nth-child(3) > span')) { // 레벨 데이터
level = $('#character-card > div > ul.character-card-summary > li:nth-child(3) > span').text();
}
else {
level = "NONE";
}
if ($('#character-card > div > ul.character-card-summary > li:nth-child(5) > span')) { // 직업 데이터
job = $('#character-card > div > ul.character-card-summary > li:nth-child(5) > span').text();
}
else {
job = "NONE";
}
if ($('#character-card > div > div:nth-child(3) > span')){
guild = $('#character-card > div > div:nth-child(3) > span').text();
}
else {
guild = "NONE";
}
if ($('#character-card > div > ul.character-card-additional > li:nth-child(1) > span')) { // 무릉도장 데이터
mureung = $('#character-card > div > ul.character-card-additional > li:nth-child(1) > span').text();
}
else {
mureung = "NONE";
}
if ($('#character-card > div > ul.character-card-additional > li:nth-child(2) > small')) { // 유니온 데이터
union = $('#character-card > div > ul.character-card-additional > li:nth-child(2) > small').text();
if (union === "") {
union = "정보없음";
}
}
else {
union = "NONE";
}
data.push([i, level, job, guild, mureung, union]); // 행 위치와 데이터를 함께 저장
Utilities.sleep(1000); // 스크래핑 사이에 딜레이 삽입
}
setSheetData(sheet, data) // 데이터 한번에 입력
}
...
let data = [];
: 추후setSheetData()
로 넘겨줄 캐릭터 데이터입니다. 행의 위치와 캐릭터 데이터 등이 담길 예정입니다.Utilities.sleep(1000)
: 서버에 무리를 주지 않기 위해 스크래핑 사이에 딜레이를 삽입합니다.setSheetData()
: 데이터를 시트에 쓰는 함수이며 바로 다음 차례에 구현합니다.
3) 데이터 시트에 쓰기
마찬가지로 앞서 작성했던 setSheetData()
를 완성해보겠습니다.
setValues()
함수를 이용합니다.
GetMapleData.gs - setSheetData()
...
function setSheetData(sheet, data){
var colPos = 2; // 입력을 시작할 열의 위치
var numRows = 1; // 입력할 행의 개수(범위)
var numCols = 5; // 입력할 열의 개수(범위)
for (var datum of data) {
var input = []; // 입력할 정보만 따로 추출
var rowPos = datum[0]; // 입력을 시작할 행의 위치 추출
input.push(datum.slice(1)); // rowPos를 제외한 정보를 추출하고,setValues() 인자에 맞게 변환
console.log(input);
sheet.getRange(rowPos, colPos, numRows, numCols).setValues(input); // setValues()가 object[][]를 인수로 받음
}
}
sheet.getRange(rowPos, colPos, numRows, numCols).setValues(input)
: 시트 입력 범위를 지정해 데이터를 한번에 입력합니다.
이제 GAS를 사용해 자동으로 maple.gg에서 데이터를 긁어올 수 있습니다.
3. 트리거 설정하기
트리거에 대한 자세한 설명은
트리거 사용하기 글 링크로 대체합니다.
메이플의 캐릭터 데이터는 오전 3~4시경에 1번만 갱신되므로 일 단위 타이머
, 실행 시간은 정기 점검 시간(보통 오전 10시까지)을 고려하여 오전 11시~정오 사이
로 설정합니다.
이제 트리거를 통해 손 대지 않더라도 자동으로 데이터가 갱신되는 명부가 완성되었습니다.
4. 다음 단계 고민
maple.gg 정보 갱신 버튼을 매일 눌러야 제대로 데이터 갱신이 될텐데, 이건 어떻게 자동화할까? 🤔
5. Reference
apps script refenrence - setValues
GAS Cheerio 라이브러리
Author And Source
이 문제에 관하여([토이 프로젝트] 자동화된 게임 명부 만들기(1) - GAS 사용), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@chaejm55/토이-프로젝트-자동화된-게임-명부-만들기1-GAS-사용
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
maple.gg 정보 갱신 버튼을 매일 눌러야 제대로 데이터 갱신이 될텐데, 이건 어떻게 자동화할까? 🤔
GAS Cheerio 라이브러리
Author And Source
이 문제에 관하여([토이 프로젝트] 자동화된 게임 명부 만들기(1) - GAS 사용), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@chaejm55/토이-프로젝트-자동화된-게임-명부-만들기1-GAS-사용저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)