효율적인 블로그 자동화를 위한 Python 스크립트
블로그 운영자라면 누구나 한 번쯤은 “어떻게 하면 생산성은 높이고 반복 적인 관리 업무는 줄일 수 있을까?” 라는 고민을 합니다. 저 역시 블로그 작성 이외에 정리하고 자동화할 수 있는 방법을 많이 고민합니다. 특히 정기적으로 포스팅을 이어나가려면 일정 관리와 콘텐츠 업데이트가 필수적이지만, 매번 손으로 직접 업로드하는 과정은 번거롭고 비효율적일 수 있다는 생각을 종종 합니다. 이러한 배경에서 Python과 Google Blogger API를 활용한 블로그 자동화 스크립트는 매우 매력적인 솔루션이 될 수 있다고 생각하여 이와 관련한 프로그램을 구현하고 블로그 글을 정리하였습니다.
이번 글에서는 제공된 Python 스크립트를 기반으로 해당 코드의 핵심 기능을 살펴보고, 이를 한층 더 발전시켜 운영 효율성을 극대화할 수 있는 방법들을 제안합니다. 단순히 코드를 따라 하는 데서 그치지 않고, 실제 운영 환경에 적용 가능한 경험적 통찰과 개선 아이디어를 함께 다뤄보겠습니다.
1. 블로그 자동화의 핵심 기능 이해
해당 스크립트는 주기적인 블로그 관리 프로세스를 간소화하는 데 초점을 맞추고 있습니다. 핵심은 Google Blogger API 인증부터 포스팅, 일정 관리, 스타일링까지 블로그 운영에 필요한 전 과정을 파이프라인으로 만드는 것입니다.
(1) Google Blogger API 인증 및 서비스 연결
Google API를 활용할 때는 OAuth 2.0 인증이 필수적입니다. 여기서는 InstalledAppFlow
를 통해 로컬 환경에서 인증한 뒤, auto_token.pickle
을 활용해 인증 토큰을 재사용할 수 있습니다. 이를 통해 매번 로그인할 필요 없이 안정적인 API 접근이 가능합니다.
if os.path.exists(TOKEN_FILE):
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES)
creds = flow.run_local_server(port=8081)
여기서 API 인증 상태와 토큰 만료 여부를 판단한 뒤, 필요하면 자동으로 갱신하는 로직은 안정적인 장기 운영을 위한 핵심 요소입니다.
(2) JSON 파일을 통한 포스트 데이터 로드
블로그 게시물의 제목, 내용, 태그 등의 정보는 JSON 파일을 통해 관리할 수 있습니다. 이는 데이터 관리 측면에서 매우 유용한 방식으로, 아카이빙이나 외부 CMS와의 연계를 용이하게 합니다.
JSON 파일 형식
title, content, labels 형태로 나눠지며 content의 내용은 html 형식으로 작성하여 좀 더 다양한 형태의 글이 작성 될 수 있도록 하였습니다.
다음 예시를 참고하십시오.
{
"title": "IRP(개인형퇴직연금) 계좌 개설 방법 및 특징",
"content": "<article class=\"irp-account-opening\">\n <header>\n <h1>IRP(개인형퇴직연금) 계좌 개설 방법 및 특징</h1></article>",
"labels": [
"IRP",
"개인형퇴직연금",
"퇴직금",
"노후준비",
"세액공제"
]
}
with open(file_path, 'r', encoding='utf-8') as file:
post_data = json.load(file)
이 과정을 통해 개발자는 모든 포스트 정보를 일관된 형식으로 처리하며, 포맷 불일치나 데이터 누락을 쉽게 점검할 수 있습니다.
(3) 스타일링 및 콘텐츠 생성
create_styled_content
함수는 HTML/CSS 기반으로 콘텐츠의 가독성과 시각적 매력도를 높여줍니다. Google Fonts나 반응형 디자인 요소를 활용해 독자의 경험을 개선할 수 있으며, 이를 통해 단순 텍스트 이상으로 브랜드 아이덴티티를 표현할 수 있습니다.
2. 활용 사례: 자동화 시스템의 실질적 이점
자동화 스크립트가 가져다주는 가장 큰 이점은 운영 효율성의 극대화와 관리 부담의 감소입니다. 단순히 “코드가 알아서 포스팅을 해준다”는 차원을 넘어, 다양한 시나리오에서 활용이 가능합니다.
(1) 콘텐츠 스케줄링
마케팅 캠페인 진행 시 특정 시간에 포스팅을 올리는 것이 효과적일 때, 예약 발행 기능은 필수입니다. 아래와 같이 코드를 통해 UTC 기반 ISO 포맷의 시간대를 고려한 예약 게시가 가능합니다.
schedule_time = datetime.utcnow() + timedelta(days=1)
post_data['published'] = schedule_time.isoformat() + 'Z'
이렇게 하면 시차나 불규칙한 근무 시간에 관계없이 정확한 시점에 콘텐츠를 노출할 수 있습니다.
(2) 대량 게시 관리
다수의 JSON 파일을 일괄 처리함으로써 한꺼번에 여러 개의 포스팅을 준비할 수 있습니다. 이는 연말정산 시즌이나 신제품 발표와 같이 집중적인 콘텐츠 수요가 발생할 때 특히 유용합니다. 또한 API 호출 빈도를 조절하여 Google API의 쿼터 제한을 피하고 안정적인 운영이 가능하도록 설계할 수 있습니다.
(3) 자동화 워크플로우와의 연계
스크립트는 별개의 자동화 도구나 파이프라인에 쉽게 통합할 수 있습니다. 예를 들어 사내 CMS에서 새 글이 발행될 때마다 자동으로 이 Python 스크립트를 호출하거나, 이메일 알림 시스템과 연계해 포스팅 상태를 모니터링하는 등 다양한 워크플로우 확장이 가능합니다.
3. 더 나은 자동화를 위한 개선 제안
코드의 기본 뼈대가 튼튼하다고 해서 개선 여지가 없는 것은 아닙니다. 오히려 꾸준한 개선을 통해 안정성과 유연성을 더욱 강화할 수 있습니다.
(1) 에러 처리 개선
현재 코드는 기본적인 예외 처리 로직만을 갖추고 있습니다. 운영 환경에서는 다양한 예외 상황(인증 실패, API 호출 실패, 네트워크 불안정 등)을 만날 수 있습니다. 이를 해결하기 위해 logging
모듈을 활용한 상세 로깅 또는 Sentry 같은 모니터링 툴 연계를 고려할 수 있습니다.
try:
response = blog_service.posts().insert(...).execute()
except Exception as e:
logging.error(f"Error during post creation: {e}")
(2) 데이터 검증 로직 추가
JSON 파일 내 필수 필드 누락, 형식 오류 등을 사전에 점검하는 검증 로직을 추가하면, 런타임 에러를 줄이고 코드의 신뢰성을 높일 수 있습니다.
required_fields = ['title', 'content']
if not all(field in post_data for field in required_fields):
logging.error(f"Error: Missing required fields in {file_path}")
continue
(3) 사용자 인터페이스 개선
개발자에게는 CLI가 편하지만, 비개발자나 마케터에게는 GUI가 유용할 수 있습니다. 간단한 웹 인터페이스나 Electron 기반 데스크톱 앱을 제공하면 비기술자도 손쉽게 스크립트를 활용할 수 있습니다.
결론
이 Python 스크립트는 블로그 자동화의 초석을 잘 다져 놓은 예시로, 인증, 데이터 관리, 스타일링, 스케줄링 등 핵심적인 기능 요소를 고루 갖추고 있습니다. 여기에 오류 처리, 데이터 검증, UI 개선 등 추가적인 개선을 더한다면, 단순한 자동화 스크립트가 아닌 완성도 높은 블로그 운영 관리 도구로 발전할 수 있습니다.
마지막은 해당 내용을 포함하는 총 코드를 첨부합니다. 참고하세요.
코드 보기
import os
import pickle
import json
import time
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from datetime import datetime, timedelta
BLOG_ID='SDFSDF3ESEFSEFSEFSF33S2DDD'
SCOPES = ['https://www.googleapis.com/auth/blogger']
TOKEN_FILE = 'auto_token.pickle'
CLIENT_SECRET_FILE = 'client_secret.json'
JSON_DIRECTORY = './jsonfile' # JSON 파일들이 있는 디렉토리
POST_INTERVAL = 10 # 포스트 간 간격 (초)
def load_post_data(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as file:
post_data = json.load(file)
return post_data
except FileNotFoundError:
print(f"Error: File not found at {file_path}")
return None
except json.JSONDecodeError:
print(f"Error: Invalid JSON in file {file_path}")
return None
def get_blogger_service():
creds = None
if os.path.exists(TOKEN_FILE):
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES)
creds = flow.run_local_server(port=8081)
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
return build('blogger', 'v3', credentials=creds)
def create_styled_content(title, content):
styled_content = f"""
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
:root {{
--primary-color: #3498db; /* 주요 색상 */
--secondary-color: #2c3e50; /* 보조 색상 */
--background-color: #ecf0f1; /* 배경 색상 */
--text-color: #34495e; /* 텍스트 색상 */
}}
body {{
font-family: 'Roboto', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 20px;
}}
.styled-container {{
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}}
.styled-container h1,
.styled-container h2,
.styled-container h3 {{
color: var(--primary-color);
}}
.styled-container h1 {{
font-size: 1.3em;
margin-bottom: 20px;
text-align: center;
}}
.styled-container p {{
margin-bottom: 15px;
}}
.styled-container img {{
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 20px 0;
}}
@media (max-width: 600px) {{
.styled-container {{
padding: 20px;
}}
}}
</style>
</head>
<body>
<div class="styled-container">
{content}
</div>
</body>
</html>
"""
return styled_content
# 사용 예시
def create_blog_post(blog_service, title, content, labels, schedule_time=None):
styled_content = create_styled_content(title, content)
post_data = {
'title': title,
'content': styled_content,
'labels': labels,
'blog': {'id': BLOG_ID}
}
# If a schedule time is provided, set the 'published' field
if schedule_time:
post_data['published'] = schedule_time.isoformat() + 'Z' # Set to ISO format
try:
response = blog_service.posts().insert(
blogId=BLOG_ID,
body=post_data,
isDraft=schedule_time is not None, # If schedule_time is set, make it a draft until the date arrives
fetchImages=True
).execute()
print("Blog post created successfully!")
print(f"Post ID: {response['id']}")
print(f"Post URL: {response['url']}")
except Exception as e:
print(f"An error occurred while creating the blog post: {e}")
def process_json_files(directory):
blog_service = get_blogger_service()
success_count = 0
failure_count = 0
json_files = [f for f in os.listdir(directory) if f.endswith('.json')]
total_files = len(json_files)
for index, filename in enumerate(json_files, 1):
file_path = os.path.join(directory, filename)
post_data = load_post_data(file_path)
if not post_data:
failure_count += 1
continue
title = post_data.get('title')
content = post_data.get('content')
labels = post_data.get('labels', [])
schedule_time = datetime.utcnow() + timedelta(days=0)
if not all([title, content]):
print(f"Error: Missing title or content in file {filename}")
failure_count += 1
continue
print(f"\nProcessing file {index} of {total_files}: {filename}")
if create_blog_post(blog_service, title, content, labels, schedule_time):
success_count += 1
else:
failure_count += 1
if index < total_files: # 마지막 파일이 아니라면 대기
print(f"Waiting {POST_INTERVAL} seconds before next post...")
time.sleep(POST_INTERVAL)
print(f"\nProcessing complete. Successful posts: {success_count}, Failed posts: {failure_count}")
def main():
try:
process_json_files(JSON_DIRECTORY)
except Exception as e:
print(f"An unexpected error occurred: {e}")
if __name__ == "__main__":
main()