본문 바로가기
풀스택 개발/개발일지

티스토리 크롤링 & 마크다운 파일 만들기, 해시노드 업로드

by act2 2025. 2. 8.
728x90

티스토리 크롤링하여 마크다운 파일 만들기

 

티스토리 블로그를 크롤링하여, 깃허브 블로그 Jekyll 스킨에 맞도록 마크다운 파일을 만드는 코드입니다.

티스토리 스킨은 '#1' 스킨을 적용한 후 크롤링을 진행하였습니다.

 

깃허브 블로그에 올라가는 파일은 YYYY-MM-DD-제목.md 형식으로 저장됩니다.

마크다운 파일을 만들면서 제목, 작성일, 카테고리, 태그 등이 자동으로 저장되도록 하였습니다.

 

import requests
from bs4 import BeautifulSoup
import os
import time
from datetime import datetime
import re

def get_page_content(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
    try:
        response = requests.get(url, headers=headers, timeout=10)
        return response.text if response.status_code == 200 else None
    except Exception as e:
        print(f"Error fetching {url}: {e}")
        return None

def html_to_markdown(html_content):
    soup = BeautifulSoup(html_content, 'html.parser')

    title_element = soup.find('meta', property='og:title')
    title = title_element['content'] if title_element else '제목 없음'

    date_element = soup.find('meta', property='article:published_time')
    if date_element:
        raw_date = date_element['content'][:19]
        formatted_date = datetime.strptime(raw_date, "%Y-%m-%dT%H:%M:%S").strftime("%Y-%m-%d %H:%M:%S")
    else:
        formatted_date = "날짜 정보 없음"

    category_element = soup.select_one('.area_title strong.tit_category a')
    category = category_element.text.strip() if category_element else '분류 없음'

    tag_container = soup.find('dl', class_='list_tag')
    tags = []
    if tag_container:
        tags = [tag.text.strip() for tag in tag_container.find_all('a', rel='tag')]

    content = soup.find('div', class_='tt_article_useless_p_margin')
    markdown_content = ''
    if content:
        for element in content.find_all(['p', 'h2', 'h3', 'h4', 'img', 'ul']):
            if element.name == 'img':
                img_url = element.get('src')
                markdown_content += f"![이미지]({img_url})\n\n"
            elif element.name in ['h2', 'h3', 'h4']:
                level = element.name[1]
                markdown_content += f"{'#'*int(level)} {element.get_text().strip()}\n\n"
            elif element.name == 'ul':
                for li in element.find_all('li'):
                    markdown_content += f"- {li.get_text().strip()}\n"
                markdown_content += "\n"
            else:
                text = element.get_text().strip()
                if text:
                    markdown_content += f"{text}\n\n"

    return title, formatted_date, category, tags, f"""---
layout: single
title: "{title}"
date: "{formatted_date}"
categories: "{category}"
tags: {tags}
---

{markdown_content.strip()}"""

def save_markdown(content, filename):
    base, extension = os.path.splitext(filename)
    counter = 1
    while os.path.exists(filename):
        filename = f"{base}_{counter}{extension}"
        counter += 1
    with open(filename, 'w', encoding='utf-8') as f:
        f.write(content)

base_url = "https://abc.tistory.com"
output_dir = "markdown_output"
os.makedirs(output_dir, exist_ok=True)

page_number = 1
empty_page_count = 0
max_empty_pages = 10

while True:
    url = f"{base_url}/{page_number}"
    html_content = get_page_content(url)

    if not html_content:
        print(f"페이지 {page_number}에서 내용을 찾을 수 없습니다.")
        empty_page_count += 1
        if empty_page_count >= max_empty_pages:
            print(f"{max_empty_pages}개의 연속된 빈 페이지 발견. 크롤링 종료")
            break
    else:
        empty_page_count = 0
        title, formatted_date, category, tags, markdown_content = html_to_markdown(html_content)

        try:
            file_date = datetime.strptime(formatted_date, "%Y-%m-%d %H:%M:%S").strftime("%Y-%m-%d")
        except ValueError:
            file_date = datetime.now().strftime("%Y-%m-%d")

        safe_title = re.sub(r'[^\w\-_ ]', '', title)[:50]
        safe_title = safe_title.replace(' ', '-')  # 빈 공간을 '-'로 대체
        filename = f"{file_date}-{safe_title}.md"

        save_markdown(markdown_content, os.path.join(output_dir, filename))
        print(f"성공: {filename}")

    page_number += 1
    time.sleep(1)

print("크롤링 완료")

 

 

깃허브를 통해 블로그를 만들고, Jekyll 스킨을 적용해봤는데...

글은 잘 업로드되는데, 스킨을 꾸미는 게 쉽지가 않군요.

그래서 Netify를 이용하려고 했는데, 

역시 아직 제 프런트앤드 실력으로는 무리라는 생각이 들었습니다.

 

다시 연구...

 

다운로드한 마크다운 파일을 압축해서 해시노드에 업로드해 봤습니다. 

유레카!!

해시노드에 글이 완벽하게 올라갔습니다. 

 

그동안 구글 블로그, 워드프레스 등에 백업한 글을 업로드해 봤는데, 

완벽하게 글이 올라간다는 게 쉬운 게 아니었습니다. 

구글은 하루 100개로 제한, 워드프레스는 이미지가 드문드문 펑크가 나고...

 

그런데 해시노드에 마크다운 파일을 업로드하니, 완벽하게 글이 작성됩니다. 

대략 7,000개의 글을 수작업으로 손보려고 했으면... 생각만 해도 끔찍합니다.

 

앞으로 해야 할 일은,

1. 해시노드의 글들을 깃허브로 백업하기 (해시노드에서 쉽게 설정할 수 있습니다.)

2. 해시노드를 Headless CMS로 바꾸기 / 도메인 설정

3. 구글 애드센스 설정

4. 각 글을 SEO에 맞춰 수정하기

이렇게 5가지입니다. 

 

PageSpeed Insights에서 테스트를 해 보니...

짜잔! 거의 평균 95점 이상이 나온다는!!!

티스토리 블로그로는 도저히 얻을 수 없는 결과입니다. 

사이트를 이전하면 이런 결과를 얻을 수 있다고 생각하니, 벌써부터 기대가 됩니다.

 

깃허브 블로그, Netify, 해시노드를 통해 블로그 이전 문제를 해결하려고 하다 보니, 

저절로 Next.js에 대해 공부하게 되었습니다. 

CSR이 아닌 SSR이 무슨 의미인지 알게 되었고, 이게 SEO에 어떤 영향을 미치는지도 알게 되었습니다. 

그래서 SEO에 있어 React보다 Next.js 우수하다는 점... 이거 몰랐으면 크게 손해 봤을 것 같습니다.

 

티스토리에서 해시노드로의 사이트 이전,

성공하면 일련의 과정을 다시 한번 포스팅하기로 약속하고, 오늘은 그만 물러갑니다. 

728x90