도전과제
매일 오픈카톡으로 전송되는 “깡프로 스타트업 뉴스”, “아이보스 마케팅 뉴스클리핑”과 같은 뉴스 클리핑을 내 분야(의료관광)으로 만들어보자.
이왕이면, 단순한 텍스트보다 “츄유의 부자되는 습관” 같은 카드뉴스형태로 만들어보자.
목표
의료관광 분야의 최근 뉴스를 클리핑하여 오픈카톡방에 쉽게 공유한다.
클리핑된 정보를 카드뉴스 이미지로 만들어 블로그나 SNS으로 배포한다.
걸림돌1 - 최근뉴스 클리핑
생성형 AI를 이용하면 쉽게 최근 뉴스를 수집 할 수 있을 것이라 생각했지만
처음부터 난관에 봉착했다.
제미나이, ChatGPT, 뉴스서치 GPTs 까지
특정날짜, 특정분야의 뉴스를 가져오는 것은 쉽지 않았다.
그리고 같이 제공되는 링크는 기사링크가 아닌 전체 도메인을 제공하는 경우가 많았다.
특정분야 뉴스를 클리핑하는 것부터 난관에 봉착하니 멘붕.... 😰
어떻게 하면 해결할 수 있을까?
뉴스크롤링을 해볼까? 방법을 모르는데.. ㅠㅠ
GPT의 도움을 받으면 해결할 수 있을지도 몰라….
어찌 어찌해서 구글스프레드시트와 구글앱스스크립트를 이용해서
네이버 뉴스 API를 이용해서 크롤링해서 메일로 받아오는 것까지는 만들었다.
메일로 도착한 내용을 보니 얼마나 기쁘던지… ㅋㅋㅋ
Grimoire GPTs 의 도움을 받아 작성된 코드이기도 하고
혹시나 다른 분들께 도움이 될 수 있으니 공유해본다.
이 스크립트 만들때도 우여곡절이 많이 있었다.
구글뉴스 RSS가 가장 빠르고 쉽게 가져올 수 있었는데,
출처링크가 구글주소로 래핑(?)이 되어 있어서 요약분석이 잘 안되는 문제가 있었다.
그래서 결국은 네이버 뉴스검색 API를 이용했는데,
네이버 뉴스검색은 출처링크가 나오는 대신 중복된 기사가 너무 많아서
이를 처리하느라 시간을 좀 많이 썼다.
아 마 Grimoire GPTs가 없었다면 중간에 포기했을지도 모르겠다.
function NewsScraping(sheetname) {
//sheetname이 undefine이 되었을 때 처리
if (sheetname == null) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
} else {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetname);
}
// 시트에서 키워드, 시작일, 종료일 가져오기
var keywordString = sheet.getRange("B1").getValue(); // "의료관광, K메디컬, 메디컬코리아, 외국인환자유치, 환자유치"
var startDate = sheet.getRange("B2").getValue();
var endDate = sheet.getRange("B3").getValue();
// 콤마로 구분된 키워드 문자열을 배열로 변환
var keywords = keywordString.split(",").map(function(keyword) {
return keyword.trim(); // 공백 제거
});
// 모든 키워드에 대해 기사 수집
var allArticles = [];
keywords.forEach(function(keyword) {
var articles = fetchArticles(keyword);
// 기사 배열에 키워드 추가
var articlesWithKeyword = articles.map(function(article) {
return [keyword].concat(article); // 기사 앞에 키워드 추가
});
allArticles = allArticles.concat(articlesWithKeyword);
});
//Logger.log(allArticles);
// 시작 날짜와 종료 날짜 사이에 있는 기사만 필터링
var startDateObj = new Date(startDate);
var endDateObj = new Date(endDate);
endDateObj.setHours(23, 59, 59, 999); // 종료 날짜를 그 날의 끝으로 설정
//지정한 날짜에 해당하는 자료만 필터링
var filteredArticles = allArticles.filter(function(article) {
var pubDate = new Date(article[1].pubDate);
return pubDate >= startDateObj && pubDate <= endDateObj;
});
//Logger.log(filteredArticles);
//유니크한 링크 필터링
var uniqueLinkArticles = filterUniqueLinkArticles(filteredArticles);
//유사한 기사 제목 필터링
// var uniqueArticles = filterUniqueTitleArticles(uniqueLinkArticles);
//유사한 기사 제목 필터링
var uniqueArticles = filterUniqueDescriptionArticles(uniqueLinkArticles);
var recordArticles = uniqueArticles;
//시트에 출력
recordArticlesToSheet(recordArticles, sheetname);
//이메일 발송
sendArticlesByEmail(recordArticles);
}
function fetchArticles(keyword) {
var clientId = ""; // 클라이언트 ID
var clientSecret = ""; // 클라이언트 시크릿
var displayCount = 100; // 한 번에 출력할 결과 수를 100으로 설정
var start = 1; //검색 시작위치
var sort = "date"; // 정렬 기준 (date: 날짜별, sim: 유사도별)
var apiURL = `https://openapi.naver.com/v1/search/news.json?query=${encodeURI(keyword)}&display=${displayCount}&start=${start}&sort=${sort}`;
var options = {
"method": "get",
"headers": {
"X-Naver-Client-Id": clientId,
"X-Naver-Client-Secret": clientSecret
},
"muteHttpExceptions": true
};
var response = UrlFetchApp.fetch(apiURL, options);
var jsonResponse = JSON.parse(response.getContentText());
//Logger.log(response);
return jsonResponse.items;
}
//유사한 기사 제목 필터링
function filterUniqueTitleArticles(articles) {
var uniqueArticles = [];
articles.forEach(function(article) {
var isUnique = true;
// 기사 제목에서 키워드 추출
var articleKeywords = extractKeywords(article[1].title);
// 기존에 선택된 고유한 기사와 비교
uniqueArticles.forEach(function(uniqueArticle) {
var uniqueArticleKeywords = extractKeywords(uniqueArticle[1].title);
// 유사한 키워드가 일정 비율 이상 중복되면 유사한 기사로 판단
if (calculateKeywordSimilarity(articleKeywords, uniqueArticleKeywords) > 0.5) { // 유사성 임계값은 상황에 따라 조정
isUnique = false;
}
});
if (isUnique) {
uniqueArticles.push(article);
}
});
return uniqueArticles;
}
//유사한 리드문 제목 필터링
function filterUniqueDescriptionArticles(articles) {
var uniqueArticles = [];
articles.forEach(function(article) {
var isUnique = true;
// 기사 제목에서 키워드 추출
var articleKeywords = extractKeywords(article[1].description);
// 기존에 선택된 고유한 기사와 비교
uniqueArticles.forEach(function(uniqueArticle) {
var uniqueArticleKeywords = extractKeywords(uniqueArticle[1].description);
// 유사한 키워드가 일정 비율 이상 중복되면 유사한 기사로 판단
if (calculateKeywordSimilarity(articleKeywords, uniqueArticleKeywords) > 0.5) { // 유사성 임계값은 상황에 따라 조정
isUnique = false;
}
});
if (isUnique) {
uniqueArticles.push(article);
}
});
return uniqueArticles;
}
function extractKeywords(title) {
// 기사 제목에서 키워드 추출 (간단한 예시로 구두점 제거 및 소문자로 변환)
// return title.toLowerCase().replace(/[^\w\s]|_/g, "").split(/\s+/);
// 영문, 숫자, 한글 텍스트에서 특수 문자 및 구두점을 제거하고, 소문자로 변환 후 키워드 분리
// 한글에 대해서는 소문자 변환 적용하지 않음 (한글에는 대소문자 구분이 없음)
return title.replace(/[^\w\s가-힣]|_/g, "").split(/\s+/);
}
function calculateKeywordSimilarity(keywords1, keywords2) {
// 두 키워드 집합 간의 유사성 계산
var totalKeywords = new Set([...keywords1, ...keywords2]);
var commonKeywords = keywords1.filter(keyword => keywords2.includes(keyword)).length;
return commonKeywords / totalKeywords.size; // 유사성 비율 반환
}
//링크가 유니크한 기사만 추출
function filterUniqueLinkArticles(articles) {
var uniqueArticles = [];
articles.forEach(function(article) {
var exists = uniqueArticles.some(function(uniqueArticle) {
return uniqueArticle[1].link === article[1].link;
});
if (!exists) {
uniqueArticles.push(article);
}
});
return uniqueArticles;
}
function recordArticlesToSheet(articles, sheetname) {
//sheetname이 undefine이 되었을 때 처리
if (sheetname == null) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
} else {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetname);
}
articles.forEach(function(article) {
var keyword = article[0];
var title = stripHtml(article[1].title);
var description = stripHtml(article[1].description);
var pubDate = formatDate(article[1].pubDate);
var link = article[1].link
sheet.appendRow([keyword, title, pubDate, link, description]);
});
}
function sendArticlesByEmail(articles) {
// 이메일 주소를 가져오기 위한 시트 선택
var emailSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("_CODE_");
var emailTitle = emailSheet.getRange("B2").getValue();
var emailAddress = emailSheet.getRange("B3").getValue();
// 이메일 본문 구성
var emailBody = "최신 기사 목록:\n\n";
articles.forEach(function(article) {
var keyword = article[0];
var title = stripHtml(article[1].title);
var description = stripHtml(article[1].description);
var pubDate = formatDate(article[1].pubDate);
var link = article[1].link;
emailBody += "키워드: " + keyword + "\n";
emailBody += "제목: " + title + "\n";
emailBody += "발행일: " + pubDate + "\n";
emailBody += "링크: " + link + "\n";
emailBody += "설명: " + description + "\n\n";
});
// 이메일 전송
MailApp.sendEmail({
to: emailAddress,
subject: emailTitle,
body: emailBody
});
}
function stripHtml(html) {
var document = XmlService.parse("<body>" + html + "</body>");
var text = document.getRootElement().getText();
return text;
}
function formatDate(dateString) {
var date = new Date(dateString);
return Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy-MM-dd");
}
걸림돌2 - 기사요약
기사제목과 링크가 있으니 이제 요약만하면 되는데
링크 하나를 요약하는 것은 욱영표 3줄요약으로도 가능했는데
수집한 여러개의 링크를 모두 주고 기사별로 요약을 해주는 GPTs가 없어서
대니님의 GPTs 만들기 도우미 를 이용해서
기존에 만들었던 욱영표 3줄요약 GPTs를 업그레이드!!
그래서 탄생한 간추린 뉴스봇 ft. 카드뉴스 이다.
사용법
간추린 뉴스봇 사용방법은 간단하다.
뉴스 제목, 링크를 여러개주고 3줄요약을 해달라고 한다.
이 내용을 오픈채팅방에 올리면 되겠지? 😀
그러면 위와 같이 제목과 발행일, 출처, 3줄요약 형태로 출력이 된다.
만약 출처링크가 별도로 필요하다면
(그냥 위 내용을 카톡에 붙이면 링크가 사라지는 문제가 생긴다)
이렇게 프롬프트를 주면 링크가 풀텍스트로 제시된다.
카드뉴스 만들기
카드뉴스 이미지 제작도 GPT 만들 수 있을까? 하는 생각으로
돈도 줘보고, 협박도 해보고, 템플릿 구조를 파악해서 그대로
그려달라고도 해봤지만 내 능력밖에 문제였다.
(혹시 카드뉴스 이미지를 쉽게 만들어 낼 수 있는 방법을 알고 계시는 분은
밥 사드릴테니 댓글을 남겨주세요 ㅠㅠ)
그래서 결국에는 카드뉴스에 사용할
제목 인덱스 텍스트와 기사 본문에 들어갈 자료를 뽑아달라고 했다.
다른 카드뉴스 툴에서도 될지 모르지만
캔바에는 대량제작 앱이 있어서 자료를 표형태로 만들어서 복 붙하면
쉽게 다량의 내용을 빠르게 만들 수 있다.
(대량제작 방법은 나중에 한번 더 정리하는 것으로…)
간추린 뉴스봇 채팅내용은 간추린 뉴스봇 사용 채팅 보기 를 보면
누구나 쉽게 사용할 수 있지 않을까 싶다.
결과물
도움이 되셨다면
인스타 좋아요와 팔로잉 그리고 블로그 이웃설정까지 부탁해요. ㅎㅎ
GPTers 9기 교육이 끝나는 시점에 나만의 GTPs가 만들어 진것 같아서
나름 뿌듯하네요. 내가 도움 받은 만큼 다른 분들도 도움이 되었으면 하는 바람입니다.
앞으로 남은 과제
GPTs를 계속 사용하다보면 예시형태로 출력이 안되고 자기 마음대로 표현이 된다.
이건 다른 GPTs에도 비슷하게 일어나는 현상이긴 한데 개선할 수 있지 않을까 싶다.