-
Notifications
You must be signed in to change notification settings - Fork 4
/
게임토라 스킬 긁기-1.0.0.user.js
268 lines (225 loc) · 9.56 KB
/
게임토라 스킬 긁기-1.0.0.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
// ==UserScript==
// @name 게임토라 스킬 긁기
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description try to take over the world!
// @author Ravenclaw5874
// @match https://gametora.com/ko/umamusume/skills
// @icon https://www.google.com/s2/favicons?sz=64&domain=gametora.com
// @grant GM_getResourceText
// @grant GM_registerMenuCommand
// @resource sheet file://D:\Downloads\스킬DB - 2.5주년 전.tsv
// ==/UserScript==
// tsv 데이터를 딕셔너리 배열로 변환하는 함수
function tsvToDictionaryArray(tsv) {
const lines = tsv.split(/\r?\n/); // 줄 단위로 분할
const headers = lines[0].split('\t'); // 헤더 정보 추출
const data = [];
// 각 줄을 딕셔너리로 변환하여 배열에 추가
for (let i = 1; i < lines.length; i++) {
// 빈 줄인지 확인
if (lines[i].trim() === '') {
continue; // 빈 줄이면 건너뜁니다.
}
const fields = lines[i].split('\t'); // 필드 분할
const entry = {};
// 각 필드를 헤더에 매핑하여 딕셔너리 생성
for (let j = 0; j < headers.length; j++) {
entry[headers[j]] = fields[j];
}
data.push(entry); // 딕셔너리를 배열에 추가
}
return data;
}
//큰원 -> 작은원
function changeSkillName(text) {
return text.replace("◯", "○");
}
//Xpath로 요소 찾기
Node.prototype.xpath = function (xpath) {
return document.evaluate(xpath, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
//href에서 html 가져오기
async function fetchPageDom(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const html = await response.text();
// HTML 문자열을 DOM으로 변환
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// DOM 처리
//console.log(doc);
return doc;
} catch (error) {
console.error('There has been a problem with your fetch operation:', error);
}
}
// n밀리초 동안 대기하는 함수
function wait(milliseconds) {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
function downloadDictionaryArrayAsTSV(dictionaryArray, filename) {
let keys = new Set();
dictionaryArray.forEach(dict => {
Object.keys(dict).forEach(key => {
keys.add(key);
});
});
keys = [...keys];
//const keys = Object.keys(longest);
const rows = [keys, ...dictionaryArray.map(obj => keys.map(key => obj[key]))];
const tsv = rows.map(row => row.join('\t')).join('\n');
const blob = new Blob([tsv], { type: 'text/tab-separated-values' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = `${filename}.tsv`;
link.href = url;
link.click();
}
//단일 노드를 클릭한 뒤 추가된 노드를 반환
function observeAddedsNodeAfterClick_(element) {
return new Promise(resolve => {
// MutationObserver 생성
const observer = new MutationObserver((mutations, observer) => {
// DOM의 변화를 관찰하고 처리
const addedNodes = mutations.flatMap(mutation => Array.from(mutation.addedNodes));
const filteredNodes = addedNodes.filter(node => node.id.includes('tippy'));
resolve(filteredNodes);
// 관찰 종료
observer.disconnect();
});
// MutationObserver 설정
const config = { childList: true, subtree: true };
observer.observe(document.body, config);
// 요소 클릭
element.click();
});
}
//여러 노드를 클릭한 뒤 추가된 노드를 반환.
async function observeAddedNodesAfterClick(elements) {
const addedNodes = [];
for (const element of elements) {
addedNodes.push(...await observeAddedsNodeAfterClick_(element));
await wait(1);
}
return addedNodes;
}
//더보기 요소를 주면 href 배열 반환
async function getHrefArray(moreNode) {
//moreNode.scrollIntoViewIfNeeded();
const detail = (await observeAddedsNodeAfterClick_(moreNode))[0];
await wait(1);
const imgs = detail.querySelectorAll("span > span > img");
const tippys = await observeAddedNodesAfterClick(imgs);
const hrefs = tippys.map(tippy => {
const a = tippy.querySelector("a");
const b = tippy.querySelector("b");
return (a === null && b !== null) ? b.textContent : a.href;
});
if (imgs.length !== tippys.length) {console.log("debug 차이", moreNode, imgs.length, tippys.length)}
await wait(1);
moreNode.click();
await wait(300);
return hrefs;
}
//날짜 텍스트 배열에서 가장 빠른 날짜를 반환
function getEarliestDate(dates) {
if (dates.length === 0) {dates = ["9999년 9월 9일"]}
let earliestDate = null;
// 정규식을 이용하여 "년 월 일" 형식의 날짜 문자열에서 숫자만 추출합니다.
const regex = /(\d{4})년 (\d{1,2})월 (\d{1,2})일/;
for (const date of dates) {
// 정규식을 사용하여 숫자를 추출합니다.
const [, year, month, day] = date.match(regex);
// 추출한 숫자를 이용하여 Date 객체를 생성합니다.
const currentDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
if (earliestDate === null || currentDate < earliestDate) {
earliestDate = currentDate;
}
}
return `${earliestDate.getFullYear()}년 ${earliestDate.getMonth() + 1}월 ${earliestDate.getDate()}일`;
}
//todo : 시나리오 출시일 미리 적어놓고 alt태그에서 가져와서 비교
// 고유기 데이터 계승기로 복사
async function downloadTSV() {
const namuSheet = tsvToDictionaryArray(GM_getResourceText("sheet"));
console.log(namuSheet);
const hrefDateDict = {
"URA 파이널즈" : "2021년 2월 24일",
"아오하루배" : "2021년 8월 30일",
"메이크 어 뉴 트랙" : "2022년 2월 24일",
"그랜드 라이브" : "2022년 8월 24일",
"그랜드 마스터즈" : "2023년 2월 24일",
"프로젝트 L'Arc" : "2023년 8월 24일"
};
for (const skillInfo of namuSheet) {
//이미 긁어왔으면 건너뜀
if (skillInfo['스킬 id'] && skillInfo['스킬명(한섭)']) {continue;}
console.log(skillInfo['스킬명(나무)'])
//일섭명에 해당하는 row 가져오기
skillInfo['스킬명(일섭)'] = changeSkillName(skillInfo['스킬명(일섭)']); //큰원 -> 작은원
const jpnNameNode = document.xpath(`/html/body/div/div[1]/div/main/main/div[2]/div/div[position()=2 and text()="${skillInfo['스킬명(일섭)']}"]`);
if (jpnNameNode === null) {
console.log('debug 검색 불가: ',skillInfo['스킬명(일섭)']);
continue;
}
const toraInfo = jpnNameNode.parentNode;
toraInfo.scrollIntoViewIfNeeded();
//id
if (!skillInfo['스킬 id']) {
skillInfo['스킬 id'] = toraInfo.xpath("div[4]/text()[2]").textContent.match(/\d+/)[0];
if (skillInfo['희귀'] === '계승') {skillInfo['스킬 id'] = "9" + skillInfo['스킬 id'].slice(1);}
}
//스킬명(한섭)
if (!skillInfo['스킬명(한섭)']) {
const skillNameKor = toraInfo.querySelector(":scope > div:nth-child(3)").textContent;
const japaneseAndChinesePattern = /[\u3040-\u30FF\u31F0-\u31FF\u4E00-\u9FFF\uFF65-\uFF9F]/;
const containsJapaneseOrChinese = japaneseAndChinesePattern.test(skillNameKor);
if (!containsJapaneseOrChinese) {
skillInfo['스킬명(한섭)'] = skillNameKor;
}
}
//보유 말딸
if (!skillInfo['보유 말딸']) {
if (toraInfo.querySelector(":scope > div:nth-child(4) > div") !== null) {
skillInfo['보유 말딸'] = toraInfo.querySelector(":scope > div:nth-child(4) > div").textContent.match(/.+?\)/)[0];
}
}
//예상 출시일
if (!skillInfo['일섭 출시일'] && !skillInfo['예상 출시일']) {
const moreNode = toraInfo.querySelector(":scope > div:nth-child(5) > span > span");
const hrefArray = await getHrefArray(moreNode);
const dateArray = [];
for (const href of hrefArray) {
//사전에 없으면 가져오기
if (!hrefDateDict.hasOwnProperty(href)) {
if (href.startsWith("http")) {
const doc = await fetchPageDom(href);
const date = doc.xpath("//span[contains(text(), '년') and contains(text(), '월') and contains(text(), '일')]").textContent;
hrefDateDict[href] = date;
}
//사전에도 없고, url도 아닌 경우 건너뜀
else {
console.log('debug 오류 텍스트: ', href);
continue;
}
}
dateArray.push(hrefDateDict[href]);
}
skillInfo['일섭 출시일'] = getEarliestDate(dateArray);
}
//console.log('완성된 skillInfo: ', skillInfo);
}
console.log(namuSheet);
downloadDictionaryArrayAsTSV(namuSheet, '게임토라');
}
(async function() {
'use strict';
// Your code here...
GM_registerMenuCommand("다운로드",downloadTSV);
})();