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()
: 긁어낸 데이터를 각 캐릭터 행에 입력하는 함수입니다. 마찬가지로 아래에서 기능을 추가할 예정입니다.robots.txt를 확인한 결과 스크래핑에 아무제약이 없음을 확인할 수 있었습니다.
이제 앞서 작성한 스크립트에 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()
: 데이터를 시트에 쓰는 함수이며 바로 다음 차례에 구현합니다.마찬가지로 앞서 작성했던 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~4시경에 1번만 갱신되므로 일 단위 타이머
, 실행 시간은 정기 점검 시간(보통 오전 10시까지)을 고려하여 오전 11시~정오 사이
로 설정합니다.
이제 트리거를 통해 손 대지 않더라도 자동으로 데이터가 갱신되는 명부가 완성되었습니다.
maple.gg 정보 갱신 버튼을 매일 눌러야 제대로 데이터 갱신이 될텐데, 이건 어떻게 자동화할까? 🤔