기술 덕후의 아침 루틴 만들기: Synology, Python, Obsidian 연동기
문제 정의
내가 가지고 있는 Synology NAS에서 서버 처럼 news를 요약해서 정리하는 것을 만들고 싶었다. Discord와 연동도 좋지만, Obsidian을 개인 지식 관리로 쓰고 있고 이 Obsidian의 저장소를 OneDrive로 하고 있어서 이 부분을 활용하고 싶었다.
"Synology NAS에서 매일 아침에 news crawling을 해서, markdown file로 특정 폴더 파일에 요약 정보 및 링크를 요약해서 저장하는 기능을 넣고 싶어. 어떻게 구성할지 몇가지 방법을 제안해줘."
우선 방안에 대해서 알고 싶어서 위와 같이 Prompting을 하였다. 그리고, 제안한 방법들의 요약을 정리해 보면 다음과 같다.
- Synology Task Scheduler + Python 스크립트
- Docker 컨테이너 활용
- Crawlab 활용 (Docker 기반 크롤링 플랫폼)
- Obsidian 플러그인 활용
2번 3번은 알고 있는 지식이 거의 없어서 난이도가 높았다. 4번은 자세히 읽었을 때에는 Synology로 하는 방법이 아니었다. 1번이 requests, BeautifulSoup, markdownify와 같은 들어 본적이 있는 python package를 사용하는 방법이지 시도해보기로 했다.
처음 코드 생성
다음과 같은 Prompting으로 첫 코드를 얻었다.
"방법 1에서 Ptyno 스크립트 실행하는 방법을 detail하게 설명해줘. 어떤 python script를 사용해야 하는지 알려주고, 실재로 Synology에서 python script 테스트 하는 방법도 알려줘. 그리고, Task Scheduler에 해당 script 등록하고 테스트해 보는 방법도 상세하게 알려줘."
생성해준 코드를 적용하기 위한 환경 설정도 물어 보면서, pip라든과 관련된 package를 설치하고 테스트를 했다. 한번에 동작하지 않았다. 처음 제공한 파일들은 관련된 method만 제공하고 실제로 그 method를 호출하는 main 코드가 없어서 추가 요청을 했다.
디버깅
추가적인 동작확인을 위해서 다음과 같이 Prompting을 추가로 진행했다.
"위에서 이야기 한데로 스크립트를 실행했지만, 결과물은 나오지 않았어. 예를 들어 naver의 경우에도 링크를 만들어서 browser에서 열어 보면 결과물도 잘 나왔어. 하지만, 추출된 뉴스는 하나도 없는데 어떻게 디버깅을 할지 알려줘."
관련해서 중간 생성하는 debugging용 html 파일을 저장하게 했다. 이 html을 넣으면서 naver, daum, google의 분석을 하는 것을 디버깅했다. daum은 뉴스를 추출하지 못했고, naver는 링크를 잘 가져오지만 역시 dynamic이어서 요약까지 잘 하지 못했다. 그래도, google은 잘 처리해 주어서 최종 코드를 얻을 수 있었다.
최종 코드
지금까지 정리된 코드를 다음과 같이 얻었다.
# news_summary.py
import requests
from bs4 import BeautifulSoup
from openai import OpenAI
from datetime import datetime
import os
# 전역 디버그 설정
DEBUG_MODE = False
DEBUG_PATH = "/volume2/backups/scripts/debug/"
headers = {'User-Agent': 'Mozilla/5.0'}
def get_naver_news(query, max_results=3):
url = f"https://search.naver.com/search.naver?where=news&query={query}"
res = requests.get(url, headers=headers)
# 디버깅 HTML 저장
if DEBUG_MODE:
with open(f"{DEBUG_PATH}naver_debug.html", "w", encoding="utf-8") as f:
f.write(res.text)
soup = BeautifulSoup(res.text, 'html.parser')
items = soup.select('a[href^="https://n.news.naver.com/"]')[:max_results]
# 디버깅 뉴스 갯수 출력
if DEBUG_MODE:
print(f"[NAVER] 뉴스 개수: {len(items)}")
return [(a.text.strip(), a['href']) for a in items]
def get_google_news(query, max_results=3):
url = f"https://news.google.com/search?q={query}&hl=ko&gl=KR&ceid=KR:ko"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/113.0.0.0 Safari/537.36"
}
res = requests.get(url, headers=headers)
# DEBUG 모드 설정
if DEBUG_MODE:
with open(os.path.join(DEBUG_PATH, "google_debug.html"), "w", encoding="utf-8") as f:
f.write(res.text)
soup = BeautifulSoup(res.text, "html.parser")
items = soup.select("a.JtKRv")[:max_results]
news_list = []
for a in items:
title = a.text.strip()
href = a.get("href", "")
# 상대경로를 절대경로로 변경
if href.startswith("./"):
href = "https://news.google.com" + href[1:]
news_list.append((title, href))
if DEBUG_MODE:
print(f"[Google] 뉴스 개수: {len(news_list)}")
return news_list
def fetch_article_content(url):
try:
res = requests.get(url, headers=headers, timeout=5)
soup = BeautifulSoup(res.text, 'html.parser')
paragraphs = soup.select('article p')
text = '\n'.join([p.text.strip() for p in paragraphs if p.text.strip()])
return text[:1000]
except Exception as e:
return f"(본문 수집 실패: {e})"
def fetch_naver_article_content(url):
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Referer': 'https://www.naver.com'
}
res = requests.get(url, headers=headers, timeout=5)
soup = BeautifulSoup(res.text, 'html.parser')
article = soup.select_one('article')
if article:
paragraphs = article.find_all(['p', 'div'])
text = '\n'.join([p.get_text(strip=True) for p in paragraphs if p.get_text(strip=True)])
return text[:2000]
else:
return "(기사 본문을 찾을 수 없음)"
except Exception as e:
return f"(본문 수집 실패: {e})"
def summarize_with_gpt(title, url):
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY") or "YOUR KEY")
if "n.news.naver.com" in url:
content = fetch_naver_article_content(url)
prompt = f"다음은 뉴스 기사입니다. 기사의 제목을 첫 줄에 포함해주세요. 그리고, 핵심 내용을 3줄 이내로 요약해 주세요.\n\n제목: {title}\n본문:\n{content}"
else:
content = fetch_article_content(url)
prompt = f"다음은 뉴스 기사입니다. 핵심 내용을 3줄 이내로 요약해 주세요:\n\n제목: {title}\n내용: {content}"
try:
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content.strip()
except Exception as e:
return f"(요약 실패: {e})"
def run_news_summary(query):
result = {"네이버": [], "구글": []}
for portal, func in [("네이버", get_naver_news), ("구글", get_google_news)]:
try:
for title, url in func(query):
summary = summarize_with_gpt(title, url)
result[portal].append((title, url, summary))
except Exception as e:
result[portal].append(("수집 실패", "#", str(e)))
return result
def save_all_queries_to_markdown(all_results, all_queries, output_dir="/volume2/backups/Obsidian/Newbie/0. Inbox"):
today = datetime.now().strftime('%Y-%m-%d')
filename = os.path.join(output_dir, f"multi_news_ALL_{today}.md")
with open(filename, 'w', encoding='utf-8') as f:
f.write(f"# 📰 {today} - 전체 키워드 뉴스 요약\n\n")
for query in all_queries:
f.write(f"\n---\n\n## 🔍 키워드: {query}\n\n")
result = all_results[query]
for portal, items in result.items():
f.write(f"### 🔎 {portal} 뉴스\n")
for title, url, summary in items:
f.write(f"- [{title}]({url})\n > {summary}\n\n")
return filename
if __name__ == "__main__":
query_file = "/volume2/backups/Obsidian/Newbie/news_query_temrs.md"
if not os.path.exists(query_file):
print(f"❌ 쿼리 파일 없음: {query_file}")
else:
with open(query_file, 'r', encoding='utf-8') as f:
content = f.read()
queries = [q.strip() for q in content.split(',') if q.strip()]
all_results = {}
for q in queries:
print(f"▶️ 수집 시작: {q}")
result = run_news_summary(q)
all_results[q] = result
final_path = save_all_queries_to_markdown(all_results, queries)
print(f"✅ 전체 결과 저장 완료: {final_path}")
마무리
위의 코드를 Task Scheduler에 등록하고 매일 아침에 관련된 폴더에 위치하게 하고 이 폴더의 파일을 One Drive와 싱크해서 Mobile의 Obidian에서 접근하도록 했다. 여전히 naver는 링크가 잘 못되기도 하고, 제목도 제데로 뽑지 못하기도 하다. 결국은 Selenium을 쓰라고 하는데, Synology는 X11 GUI를 미지원 하기 때문에 Docker 사용을 제안 받았다. 이 부분은 현재로는 허들이 높아서 단계별로 시도해보기로 한다.