결국 이글루스도 서비스 종료 되었다...
하지만 저 무서운 집념, 대체 누구이고 어떻게 한 거냐...
이글루스에서 제공한 공식 백업
예쁘게 잘 준거 같다... 특이한 태그 없이 그냥 이미지와 텍스트만 써서 그런지 내용물도 멀쩡히 뽑혔다
그렇지않은 사람은 조금 망가졌다고 한다...
카테고리 기능이나 댓글은 제공하지 않았으니 내 코드도 아주 헛된 짓은 아니었다...아닐 거다...
2편의 db저장 코드 이후 html파일로 백업 요청이 있어서 해당 부분을 추가로 개발했다.
자잘하게 기능이 추가되고 css를 적용하기 위한 html 템플릿도 만들고...
하여 길게 전체 코드를 붙여본다
이제 뭐 할 말도 없고 코드밖에 붙일게없다...
const axios = require('axios');
const cheerio = require('cheerio');
const mysql = require('mysql2/promise');
const fs = require('fs');
const path = require('path');
const dayjs = require('dayjs')
const downloadImage = async (url, filePath) => {
const writer = fs.createWriteStream(filePath);
const response = await axios({
method: 'get',
url: url,
responseType: 'stream',
}).then(res => res)
.catch(er => {
if (er.code === "ETIMEDOUT") {
downloadImage(url, filePath)
return null;
}
else return null
});
if (!response || response.status === 404 || !response.data) return
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
};
const downCss = async (address, cssLink) => {
const originCss = cssLink.split("?")[0]
const fileName = path.basename(originCss);
const imgPath = path.join(__dirname, `${address}/css`, fileName);
const response = await axios({ method: "get", url: originCss, responseType: 'stream' })
const writeStream = fs.createWriteStream(imgPath);
response.data.pipe(writeStream);
return fileName
}
const downJs = async (address, jsLink) => {
if (!jsLink) return;
const originJs = jsLink.split("?")[0]
const fileName = path.basename(originJs);
const jsPath = path.join(__dirname, `${address}/js`, fileName);
const response = await axios({ method: "get", url: originJs, responseType: 'stream' })
const writeStream = fs.createWriteStream(jsPath);
response.data.pipe(writeStream);
}
let count = 0;
const initAdd = ""; //이글루 주소
const initPost = 0 //시작할 포스트 넘버
const exitPost = null //종료될 포스트 넘버
const templateHtml = fs.readFileSync('template.html', 'utf8');
const imgSaveFlag = true; //이미지 저장 플래그
const htmlSave = true; //파일로 저장 플래그
const dbSave = true; //DB저장 플래그
const skipCategory = [] //스킵할 카테고리 배열
const reversCategoryFlag=false; // true시 skipCategory의 카테고리만 저장
let vt = null
const start = async (address, postNumber) => {
let cssFiles = "";
let jsFiles = "";
if (exitPost == postNumber) {
process.exit()
}
// Create MySQL Connection
const connection = dbSave ? await mysql.createConnection({
host: 'localhost',
user: '',
password: '',
database: 'egloos',
charset: 'utf8mb4'
}) : undefined
try {
if (vt === null) {
const login = await axios.post(`https://sec.egloos.com/login/sauthid.php`, {
returnurl: "http%3A%2F%2Fguramori.egloos.com%2Fpage%2F117",
vt: "7J",
userid: "",
userpwd: "",
userpwd_alt: "",
lbtn: "로그인"
}).then(res => res.headers)
.catch(er => null)
if (login) vt = login["set-cookie"][0]?.split(";")[0];
}
const headers = {
Accept: `text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`,
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "ko,en;q=0.9,en-US;q=0.8",
"Cache-Control": "max-age=0",
Connection: "keep-alive",
Cookie: `${vt};u=crv=e1E7rMSt3w5k4Md;`,
Host: `${address}.egloos.com`,
Referer: `http://${address}.egloos.com/page/${[postNumber]}`,
"Upgrade-Insecure-Requests": 1,
"User-Agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.41`
}
const url = `http://${address}.egloos.com/${postNumber}`;
const axiosCre = axios.create({ baseUrl: url, headers })
const response = await axiosCre.get(url).then(res => res.data).catch(er => {
console.log(er.response)
return null
});
if (response === null) {
process.exit()
}
const html = response;
const $ = cheerio.load(html);
const $template = cheerio.load(templateHtml);
const head = $('head link[type="text/css"]');
const postCategory = $('.post_title_category a').text().trim() || $('.post_info_category a').text().trim() || $('.category').text().trim();
let nextTag =null
if($(".post_navi .next > a").text().trim().length>0)
nextTag = $(".post_navi .next > a")
else {
$('p.page > a').map((i,e) => {
if( $(e).text().includes('다음'))nextTag=$(e)
});
}
console.log(nextTag.text())
const postTitle = $('.entry-title > a').text().trim() || $('.posttitle > a').text().trim() ;
let nextHref="";
if(nextTag){
if(nextTag.attr("href").indexOf(".com")>-1){
nextHref=nextTag.attr("href").split("/")[nextTag.attr("href").split("/").length-1]
}else{
nextHref=nextTag.attr("href").replace("/", "")
}
}
if ( (reversCategoryFlag ? !skipCategory.includes(postCategory) : skipCategory.includes(postCategory))) {
count++
if (count === 100) {
count=0;
setTimeout(() => {
start(address, nextHref);
}, 10000);
} else start(address, nextHref);
if (dbSave) connection.end()
return;
}
if (!fs.existsSync(`./${address}`)) {
fs.mkdirSync(`./${address}`);
}
if (!fs.existsSync(`./${address}/css`)) {
fs.mkdirSync(`./${address}/css`);
for (const css of head) {
const name = await downCss(address, $(css).attr("href"))
cssFiles += `<link type="text/css" rel="stylesheet" href="../css/${name}" media="screen">\n`;
}
} else {
for (const css of head) {
const originCss = $(css).attr("href").split("?")[0]
const name = path.basename(originCss);
cssFiles += `<link type="text/css" rel="stylesheet" href="../css/${name}" media="screen">\n`;
}
}
const headJs = $('head script[type="text/javascript"]');
if (!fs.existsSync(`./${address}/js`)) {
fs.mkdirSync(`./${address}/js`);
for (const js of headJs) {
await downJs(address, $(js).attr("src"))
}
}
// Find the post content
const titleImg = $('.entry-title > img').attr("src");
let state = 1;
if (titleImg && titleImg.indexOf("close") > -1) {
state = 0;
}
$('a[href="#신고"]').remove();
$('div[class="comment_write"]').remove();
const postContent = $('div.post_content .hentry').length>0? $('div.post_content .hentry'): $('div.post .content');
const $con = cheerio.load(postContent.html());
// Find the post info
//profile_usernick fn
const postInfoAuthor =$('span.author').text().trim()|| $(".profile_usernick").text().replace("by", "").trim() || $(".post_info_author").text().replace("by", "").trim() || $(".post_title_author").text().replace("by", "").trim();
const postInfoDate = $("a.time").text().trim()||$('.post_info_date').text().trim() || $('.post_title_date').text().trim();
const postInfoCmtCount = $(".post_tail_cmmt .count").text().trim() || $('.post_info_cmtcount').text().trim().replace(/\D/g, '') || 0;
const replyCompo =$("div.comment").length>0?$(".comment").html(): $(".post_comment").html() || null;
// Insert into category table
const imgs =$('div.post_content .hentry').length>0? $('div.post_content .hentry img'):$('div.post .content img')
if (!fs.existsSync(`./${address}/img`)) {
fs.mkdirSync(`./${address}/img`);
}
for (const img of imgs) {
const oldSrc = $(img).attr('src');
if (oldSrc?.includes('http://pds')) {
const newSrc = 'http://pds' + oldSrc.split('http://pds')[1];
const slashSplit = newSrc.split("/");
const fileName = slashSplit[slashSplit.length - 1]
const imgPath = path.join(__dirname, `${address}/img`, fileName);
$(img).attr('src', imgPath);
if (imgSaveFlag) await downloadImage(newSrc, imgPath);
}
}
$con("img").each((_i, el) => {
const src = $con(el).attr('src');
if (src?.includes('egloos')) {
const fileName = src.split('/').pop(); // "/" 기준으로 마지막 배열 추출
const newSrc = `${fileName}`;
$(el).attr('src', "img/" + newSrc);
}
})
const $css = cheerio.load(cssFiles);
if (htmlSave) {
$template("head").append($css.html())
$template(".content_wrap").append(postContent.html().replace(/E:\\egloos\\temp\\/g, "..\\")).append(replyCompo)
}
// fs.writeFileSync(`${postTitle}/${postInfoDate}.html`, $template.html());
const prevCateTitle = postCategory.length >= 1 ? postCategory : "미분류"
const cateTitle = prevCateTitle.replace(/\\/g, '_').replace(/\s/g, '-').replace(/[\\/:*?"<>|]/g, "")
const day = postInfoDate?dayjs(postInfoDate).format("YYYY-MM-DD"):dayjs().format("YYYY-MM-DD")
const fileTitle = `(${day})${postTitle.replace(/\\/g, '_').replace(/\s/g, '-').replace(/[\\/:*?"<>|]/g, "")}.html`;
if (htmlSave) {
if (!fs.existsSync(`./${address}/${cateTitle}`)) {
fs.mkdirSync(`./${address}/${cateTitle}`);
}
const filePath = path.join(__dirname, `${address}/${cateTitle}`, fileTitle);
fs.writeFileSync(filePath, $template.html());
}
if (dbSave) {
const [categoryRe] = await connection.query(
`INSERT INTO category (writer, title) VALUES (?, ?) ON DUPLICATE KEY UPDATE category_id=LAST_INSERT_ID(category_id)`,
[address, postCategory.length >= 1 ? postCategory : "미분류"]
);
// Get the category ID
const categoryId = categoryRe.insertId
// Insert into posts table
const insertDate = postInfoDate?postInfoDate.replace("at ", ""):day+" 00:00:00"
const [postRe] = await connection.query(
`INSERT INTO posts (category_id,address,origin_id, writer, title, content, reply_count, reply, state, reg_date) VALUES (?,?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE origin_id=LAST_INSERT_ID(origin_id)`,
[categoryId, address, postNumber.toString(), postInfoAuthor, postTitle, postContent.html(), postInfoCmtCount, replyCompo, state, insertDate]
);
// Get the post ID
const postId = postRe.insertId;
// Find the post tags
const postTagList = $('div.post_taglist');
const tagLinks = postTagList.find('a');
// Loop through the tag links and insert them into the tag table
for (let i = 0; i < tagLinks.length; i++) {
const tagTitle = $(tagLinks[i]).text().trim();
if (tagTitle) {
const [tagResult] = await connection.query(
`INSERT INTO tag (title) VALUES (?) ON DUPLICATE KEY UPDATE tag_id= LAST_INSERT_ID(tag_id)`,
[tagTitle]
);
// // Get the tag ID
const tagId = tagResult.insertId || tagResult.updateId;
// // Insert into post_and_tag table
await connection.query(
`INSERT INTO post_and_tag (posts_post_id, tag_tag_id) VALUES (?, ?)`,
[postId, tagId]
);
}
}
// Find the replies
const replies = $('ul.comment_list li');
let replyId = 0;
// Loop through the replies and insert them into the reply table
for (let i = 0; i < replies?.length; i++) {
const reply = $(replies[i]);
// const replyId = reply.attr('id').replace('c_', '');
const reFlag = reply.hasClass("comment_reply")
const writerId = reply.find(".comment_writer").find("a").attr("href");
const commentSecu = reply.find(".comment_security > img")
// Find the reply content, author, and reg_date
const replyContent = reply.children("div").html();
const replyAuthor = writerId ? reply.find('.comment_writer').text() + `(${writerId})` : reply.find('.comment_writer').text();
const replyRegDate = reply.find('.comment_datetime').text();
const replyState = commentSecu.attr("src") ? 0 : 1;
// Insert the reply into the database
const parentId = reFlag ? this.replyId : null;
const [replyResult] = await connection.query(
'INSERT INTO reply (post_id, parent_id, content, writer, state, origin_reg_date) VALUES (?, ?, ?, ?, ?, ?)',
[postId, parentId, replyContent || "비공개?", replyAuthor, replyState, replyRegDate]
);
this.replyId = replyResult.insertId;
}
}
if (dbSave) connection.end()
count++
if (nextTag.text()) {
if (count === 60) {
count = 0
setTimeout(() => {
start(address, nextHref);
}, 5000);
} else {
start(address,nextHref);
}
} else {
process.exit()
return;
}
} catch (er) {
console.log(er)
process.exit()
}
}
start(initAdd, initPost)
CSS와 JS까지 다운 받고 템플릿에는 해당 css,js를 적용하게 만들어 놨다.
이미지는 이글루스 경로로 되어있는 이미지만 다운받고 cheerio를 이용해 pc로컬 경로로 치환해줬는데
그렇게 하니 막상 태그에 박힐땐 현재 폴더 기준으로 절대경로를 박아버려서 내가 어거지로 replace를 해서 상대경로로 바꿔줬다...참으로 창피한 소스다
스킨이 옛날 스킨인 이글루의 경우는 태그 구조나, 클래스 명이 달라서 마지막에 가서 것도 어거지로 끼어 넣어줬다
자 이렇게 해서 완성된 코드까지 올리고 대충 끝...
꽤나 귀한 경험이었다 무언가 개발해서 누군가에게 무언가 제공한 경험은 이게 처음이었기에
누군가에겐 도움이 됐다면 좋을텐데...
사실 이글루스 종료 1시간전에 누군가 댓글 백업에 실패하여 아쉬워 하고 있었는데 사무실에서 급히 댓글만 떼오는 코드도 만들어서 제공했다
마지막까지 좋은 기억으로 남았다고 하니 나도 보람있었고 별 건 아니어도 뿌듯하기도 하다...
이글루스 종료에 대한 감상은 여기서든 저기서든 실컷 적었으니 그건 넘기고
해당 코드는 금방 용도를 다하고 사용처가 사라져 버렸지만 그래도 내게 특별함이 남는 코드이니 애끼고 애껴보겠다...
언젠가 마개조하여 다른 걸 긁어오게 되겠지...
글의 마무리는 언제나 어려운 것 같다... 뭘 배워야 하나?
다음엔 티스토리 이사 코드를 올려보자
'개발관련' 카테고리의 다른 글
북클럽 스킨 수정 - 서브 카테고리 숨김/펼침 (0) | 2023.06.26 |
---|---|
북클럽 스킨 수정 - 현재 페이지의 카테고리 구별 표시 (0) | 2023.06.23 |
이글루스 백업 코드 제작기 - 2 (with chat gpt...) (2) | 2023.06.22 |
곧장 기부 사이트 크롬 확장 프로그램 개발 (3) | 2023.06.21 |
이글루스 - 티스토리 이전 관련 한 문제점 (0) | 2023.06.21 |
댓글