구글 시트로 나만의 주식 뉴스 자동화 시스템 만들기 (최종 코드 공유)
매일 쏟아지는 주식 관련 뉴스, 일일이 찾아보기 힘드셨죠? 내가 관심 있는 종목의, 내가 원하는 키워드가 포함된 뉴스만 자동으로 수집하고, 보기 좋게 정리해 주는 구글 시트 자동화 스크립트를 공유합니다. 이 스크립트 하나면, 여러분의 소중한 시간과 노력을 아끼고 시장의 흐름을 놓치지 않을 수 있습니다.
단순한 수집을 넘어, Gemini AI와 연동하여 뉴스 요약까지 가능한 강력한 시스템을 지금 바로 만들어 보세요!
✨ 이 스크립트의 강력한 기능들
완전 자동화: 매일 4번(오전 7시, 11시, 오후 2시, 7시) 정해진 시간에 자동으로 뉴스를 수집합니다.
일별 시트 생성:
250821과 같이 그날의 날짜로 새로운 시트를 만들어 뉴스를 자동으로 정리하고, 이전 날짜의 데이터는 그대로 보존합니다.맞춤형 키워드 필터링: '종목리스트' 시트 B열에 원하는 키워드('실적', '계약', '신제품' 등)를 입력하면, 모든 종목에 대해 해당 키워드가 포함된 뉴스만 선별하여 수집합니다.
정교한 중복 제거: 완전히 똑같은 제목의 뉴스는 물론, '외국인 순매수'와 '외국인 순매도'처럼 일부 단어만 다른 유사 제목 뉴스까지 걸러내어 중복을 최소화합니다.
사용자 정의 정렬: 수집된 뉴스는 '종목리스트' 시트에 나열된 순서를 그대로 따라 정렬되며, 각 종목 내에서는 최신 뉴스가 항상 가장 위로 오도록 자동 정렬됩니다.
Gemini 요약 지원: 수집된 뉴스 옆에 원문을 붙여넣고 Gemini AI로 요약할 수 있는 전용 칸을 미리 만들어 드립니다.
🛠️ 사용 방법 (Step-by-Step)
1단계: '종목리스트' 시트 준비하기
가장 먼저, 스크립트가 정보를 읽어올 기준 시트를 준비해야 합니다.
구글 시트 파일 하단의 시트 탭 중 하나의 이름을 **
종목리스트**로 정확히 변경해 주세요.A열에는 뉴스를 수집할 종목명을 A2 셀부터 차례대로 입력합니다.
(선택 사항) B열에는 모든 종목에 공통으로 적용할 키워드를 B2 셀부터 입력합니다. 이 칸을 비워두면 모든 뉴스를 수집합니다.
2단계: 스크립트 설치하기
구글 시트 상단 메뉴에서 확장 프로그램 > Apps Script로 이동합니다.
스크립트 편집기가 열리면, 기존에 있던
function myFunction() { ... }코드를 모두 지웁니다.아래에 있는 **'최종 스크립트 전체 코드'**를 복사하여 편집기 창에 그대로 붙여넣습니다.
상단의 디스켓(💾) 아이콘을 눌러 스크립트를 저장하고, 프로젝트 이름을 '주식 뉴스 자동 수집' 등으로 지정합니다.
3단계: 자동 수집 설정 실행하기 (최초 1회만!)
다시 구글 시트 창으로 돌아와 **웹페이지를 새로고침(F5)**합니다.
시트 상단 메뉴에
🚀 매일 뉴스 자동화라는 새로운 메뉴가 생성된 것을 확인합니다.해당 메뉴를 클릭한 후, **
✅ 자동 수집 설정 (최초 1회 실행)**을 선택합니다.권한 승인:
스크립트 실행에 필요한 권한을 요청하는 창이 나타나면 **'권한 검토'**를 클릭합니다.
본인의 구글 계정을 선택합니다.
'이 앱은 Google에서 확인하지 않았습니다'라는 경고가 표시되면, **'고급'**을 클릭하고 **'(프로젝트 이름)(으)로 이동(안전하지 않음)'**을 선택합니다. (내가 직접 만든 스크립트이므로 안전합니다.)
마지막으로 **'허용'**을 눌러 모든 권한을 부여합니다.
"매일 자동 뉴스 수집 설정이 완료되었습니다." 라는 알림이 뜨면 모든 설정이 끝납니다. 이제부터는 스크립트가 알아서 모든 것을 처리합니다.
4. (선택 사항) Gemini로 뉴스 요약하기
매일 자동으로 생성되는 날짜 시트(
YYMMDD)에서 요약하고 싶은 기사의 링크(C열)를 클릭합니다.기사 원문 전체를 복사하여 해당 행의 **F열('원문 붙여넣기')**에 붙여넣습니다.
**G열('Gemini 요약')**에
=GEMINI("F2 셀의 내용을 세 문장으로 요약해줘")와 같은 명령어를 입력하여 요약 결과를 확인합니다. (Gemini for Workspace 유료 구독자만 사용 가능)
📜 최종 스크립트 전체 코드
/**
* 스크립트 전역 설정
*/
const CONFIG = {
STOCK_LIST_SHEET_NAME: '종목리스트', // 종목명이 있는 시트 이름
ITEMS_PER_STOCK: 20, // 종목당 최대로 가져올 뉴스 개수 (이후 시간 및 중복 필터링됨)
BATCH_SIZE: 20, // 한 번의 실행(5분)에 처리할 종목 개수
TRIGGER_FUNCTION_NAME: 'processNewsBatch' // 분할 처리를 위한 트리거 함수 이름
};
/**
* 스프레드시트가 열릴 때 메뉴를 생성합니다.
*/
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('🚀 매일 뉴스 자동화')
.addItem('✅ 자동 수집 설정 (최초 1회 실행)', 'setupDailyTriggers')
.addSeparator()
.addItem('🛑 모든 자동 수집 중단', 'deleteAllTriggers')
.addToUi();
}
/**
* 사용자가 실행하는 메인 설정 함수. 모든 일일 트리거를 생성합니다.
*/
function setupDailyTriggers() {
deleteAllTriggers(false); // 기존 트리거 초기화
// 각 시간대에 맞춰 트리거를 생성합니다.
ScriptApp.newTrigger('start7amCollection').timeBased().everyDays(1).atHour(7).create();
ScriptApp.newTrigger('start11amCollection').timeBased().everyDays(1).atHour(11).create();
ScriptApp.newTrigger('start2pmCollection').timeBased().everyDays(1).atHour(14).create();
ScriptApp.newTrigger('start7pmCollection').timeBased().everyDays(1).atHour(19).create();
SpreadsheetApp.getUi().alert('매일 자동 뉴스 수집 설정이 완료되었습니다.');
}
/**
* 이 스크립트로 생성된 모든 트리거를 삭제하는 함수
*/
function deleteAllTriggers(showAlert = true) {
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
const funcName = trigger.getHandlerFunction();
if (funcName.startsWith('start') || funcName === CONFIG.TRIGGER_FUNCTION_NAME) {
ScriptApp.deleteTrigger(trigger);
}
}
PropertiesService.getScriptProperties().deleteAllProperties();
if (showAlert) {
SpreadsheetApp.getUi().alert('모든 자동 수집 설정이 중단 및 삭제되었습니다.');
}
}
// --- 각 시간대별 수집 시작 함수들 ---
function start7amCollection() {
const now = new Date();
const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 7, 0, 0);
const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 19, 0, 0); // 어제 저녁 7시
startBatchCollection(startTime, endTime);
}
function start11amCollection() {
const now = new Date();
const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 11, 0, 0);
const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 7, 0, 0);
startBatchCollection(startTime, endTime);
}
function start2pmCollection() {
const now = new Date();
const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 0, 0);
const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 11, 0, 0);
startBatchCollection(startTime, endTime);
}
function start7pmCollection() {
const now = new Date();
const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 19, 0, 0); // 저녁 7시
const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 0, 0);
startBatchCollection(startTime, endTime);
}
// --- 수집 시작 함수 끝 ---
/**
* 뉴스 분할 수집을 위한 환경을 설정하고 5분짜리 임시 트리거를 생성하는 함수
*/
function startBatchCollection(startTime, endTime) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const stockListSheet = spreadsheet.getSheetByName(CONFIG.STOCK_LIST_SHEET_NAME) || spreadsheet.getSheets()[0];
const stockNames = stockListSheet.getRange('A2:A' + stockListSheet.getLastRow()).getValues()
.flat().filter(name => name.trim() !== '');
const globalKeywords = stockListSheet.getRange('B2:B' + stockListSheet.getLastRow()).getValues()
.flat().filter(keyword => keyword.trim() !== '');
if (stockNames.length === 0) return;
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === CONFIG.TRIGGER_FUNCTION_NAME) {
ScriptApp.deleteTrigger(trigger);
}
}
const properties = PropertiesService.getScriptProperties();
properties.setProperties({
'stockList': JSON.stringify(stockNames),
'globalKeywords': JSON.stringify(globalKeywords),
'currentIndex': '0',
'startTime': startTime.getTime().toString(),
'endTime': endTime.getTime().toString()
});
ScriptApp.newTrigger(CONFIG.TRIGGER_FUNCTION_NAME).timeBased().everyMinutes(5).create();
processNewsBatch();
}
/**
* 두 제목이 유사한지 확인하는 헬퍼 함수
* @param {string} title1 - 첫 번째 제목
* @param {string} title2 - 두 번째 제목
* @param {number} threshold - 유사하다고 판단할 공통 문자열의 최소 길이
* @returns {boolean} - 유사하면 true, 아니면 false
*/
function areTitlesSimilar(title1, title2, threshold) {
const shorter = title1.length < title2.length ? title1 : title2;
const longer = title1.length < title2.length ? title2 : title1;
for (let i = 0; i <= shorter.length - threshold; i++) {
const substring = shorter.substring(i, i + threshold);
if (longer.includes(substring)) {
return true;
}
}
return false;
}
/**
* 5분 트리거에 의해 주기적으로 실행되는 핵심 뉴스 처리 함수
*/
function processNewsBatch() {
const properties = PropertiesService.getScriptProperties();
const props = properties.getProperties();
if (!props.stockList) {
deleteAllTriggers(false);
return;
}
const stockList = JSON.parse(props.stockList);
const globalKeywords = JSON.parse(props.globalKeywords || '[]');
let currentIndex = parseInt(props.currentIndex, 10);
const startTime = new Date(parseInt(props.startTime, 10));
const endTime = new Date(parseInt(props.endTime, 10));
if (currentIndex >= stockList.length) return;
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const targetSheet = getOrCreateDailySheet(spreadsheet);
const lastRow = targetSheet.getLastRow();
const existingTitles = lastRow > 1
? targetSheet.getRange(2, 2, lastRow - 1, 1).getValues().flat().map(title => title.trim())
: [];
const potentialNewsData = [];
const endIndex = Math.min(currentIndex + CONFIG.BATCH_SIZE, stockList.length);
for (let i = currentIndex; i < endIndex; i++) {
const stockName = stockList[i];
try {
const rssUrl = `https://news.google.com/rss/search?q=${encodeURIComponent(stockName)}&hl=ko&gl=KR&ceid=KR:ko`;
const xml = UrlFetchApp.fetch(rssUrl).getContentText();
const document = XmlService.parse(xml);
const items = document.getRootElement().getChild('channel').getChildren('item');
for (const item of items) {
const title = item.getChildText('title').trim();
const pubDate = new Date(item.getChildText('pubDate'));
let keywordMatch = (globalKeywords.length === 0) || globalKeywords.some(k => title.toLowerCase().includes(k.toLowerCase()));
if (pubDate >= startTime && pubDate <= endTime && keywordMatch) {
potentialNewsData.push({
stock: stockName,
title: title,
link: item.getChildText('link'),
date: pubDate, // Date 객체로 유지
source: item.getChildText('source')
});
}
}
} catch (e) {
console.error(`'${stockName}' 뉴스 수집 중 오류: ${e.toString()}`);
}
}
const uniqueNewsData = [];
const allKnownTitles = [...existingTitles];
for (const newsItem of potentialNewsData) {
let isDuplicate = false;
for (const knownTitle of allKnownTitles) {
if (areTitlesSimilar(newsItem.title, knownTitle, 20)) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
uniqueNewsData.push([
newsItem.stock,
newsItem.title,
newsItem.link,
Utilities.formatDate(newsItem.date, "GMT+9", "yyyy-MM-dd HH:mm:ss"),
newsItem.source
]);
allKnownTitles.push(newsItem.title);
}
}
if (uniqueNewsData.length > 0) {
targetSheet.getRange(targetSheet.getLastRow() + 1, 1, uniqueNewsData.length, 5).setValues(uniqueNewsData);
}
sortDailySheet();
properties.setProperty('currentIndex', endIndex.toString());
if (endIndex >= stockList.length) {
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === CONFIG.TRIGGER_FUNCTION_NAME) {
ScriptApp.deleteTrigger(trigger);
}
}
properties.deleteAllProperties();
}
}
/**
* 지정된 시트의 데이터를 정렬하는 함수 ('종목리스트' 순서 반영)
*/
function sortDailySheet() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const dailySheet = getOrCreateDailySheet(spreadsheet);
const lastRow = dailySheet.getLastRow();
if (lastRow < 2) return;
const stockListSheet = spreadsheet.getSheetByName(CONFIG.STOCK_LIST_SHEET_NAME) || spreadsheet.getSheets()[0];
const stockOrderList = stockListSheet.getRange('A2:A' + stockListSheet.getLastRow()).getValues().flat().filter(name => name.trim() !== '');
const stockOrderMap = stockOrderList.reduce((map, stock, index) => {
map[stock] = index;
return map;
}, {});
const range = dailySheet.getRange(2, 1, lastRow - 1, 5);
const data = range.getValues();
data.sort((a, b) => {
const stockNameA = a[0];
const stockNameB = b[0];
const dateA = new Date(a[3]);
const dateB = new Date(b[3]);
const orderA = stockOrderMap[stockNameA] !== undefined ? stockOrderMap[stockNameA] : Infinity;
const orderB = stockOrderMap[stockNameB] !== undefined ? stockOrderMap[stockNameB] : Infinity;
if (orderA !== orderB) {
return orderA - orderB;
}
return dateB.getTime() - dateA.getTime();
});
range.setValues(data);
}
/**
* 'YYMMDD' 형식의 오늘 날짜 시트를 가져오거나 생성하는 도우미 함수
*/
function getOrCreateDailySheet(spreadsheet) {
const sheetName = Utilities.formatDate(new Date(), "GMT+9", "yyMMdd");
let sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
sheet = spreadsheet.insertSheet(sheetName, 0);
const headers = ['검색 종목', '뉴스 제목', '링크', '발행일', '출처', '원문 붙여넣기', 'Gemini 요약'];
const headerRange = sheet.getRange(1, 1, 1, headers.length);
headerRange.setValues([headers]).setFontWeight('bold');
sheet.getRange("A1:E1").setBackground("#f3f3f3");
sheet.getRange("F1:G1").setBackground("#e8f0fe");
sheet.getDataRange().createFilter();
for (let i = 1; i <= 5; i++) {
sheet.autoResizeColumn(i);
}
sheet.setColumnWidth(6, 350);
sheet.setColumnWidth(7, 350);
}
return sheet;
}
이제 여러분만의 강력한 주식 뉴스 자동화 시스템을 마음껏 활용해 보세요!