[Kotlin Spring] Relogging - 뉴스 기사 스크래핑하기 (크롤링과 스크래핑 차이)
1. 서론
Relogging 프로젝트에서 뉴스 기사를 스크래핑한 후 ai를 통해 요약하고 원문을 보고 싶다면 원본 사이트로 리다이렉션 하는 기능을 구현하자는 아이디어가 나왔다. 토스 증권의 뉴스 3줄 요약 기능을 참고하여 아이디어를 얻었다.
크롤링과 스크래핑 차이
웹 크롤링은 웹 페이지의 링크를 타고 계속해서 탐색을 이어나가지만, 웹 스크래핑은 데이터 추출을 원하는 대상이 명확하여 특정 웹 사이트만을 추적한다는 차이점이 있다.
참고: https://blog.hectodata.co.kr/crawling_vs_scraping/
2. 리소스 Selector 경로 얻기
내가 이전에 작성했던 블로그 포스팅의 내용을 스크래핑해 보도록 하겠다. 리소스의 위치가 고정되어 있는 경우 이런 방법을 통해 스크래핑할 수 있다. 많은 뉴스 플랫폼의 웹 페이지도 같은 방식으로 구성되어 있으니 이 방법을 참고하면 된다.
수프래핑할 웹 페이지이다.
https://campus-coder.tistory.com/169
[Kotlin Spring] Relogging - Spring AI 활용 구현 정리 (OpenAI, ChatGPT API 사용하기)
1. 서론프로젝트에서 뉴스 요약하기 기능을 구현하기 위해 OpenAI의 ChatGPT를 사용하기로 했다. 사실 이전 프로젝트에서도 GptAPI를 사용한 기능을 구현했었는데, 아래 사진처럼 클래스도 여러 개 만
campus-coder.tistory.com
원하는 페이지에 들어가서 리소스의 경로를 얻어보자

크롬 창 오른쪽 위 점 세 개를 클릭 -> 도구 더 보기 -> 개발자 도구

원하는 리소스의 위치를 얻기 위하여 검색을 활용할 수 있다.
목차 3-1-2. 내용을 얻기 위하여 개발자 도구의 요소 탭에서 crtl + f를 눌러 검색한다. 검색할 문자열로 "Spring 프로젝트에"를 입력하면 리소스의 위치를 확인할 수 있다.

추출할 리소스에 마우스 우클릭 -> 복사 -> selector 복사를 하면 된다.
추출한 티스토리 페이지 리소스의 selector 경로는 다음과 같았다.
#article > div.tt_article_useless_p_margin.contents_style > h4:nth-child(23)
뉴스 기사도 같은 방식으로 selector 경로를 얻을 수 있다.
3. 구현
앞선 목차에서 얻었던 selector 경로에 자원이 있으니 스크래핑을 위한 세팅과 코드를 작성해 보자.
3-1. setting
// build.gradle.kts
dependencies {
implementation("org.jsoup:jsoup:1.15.3")
}
의존성에 jsoup를 추가하자
3-2. 코드
@Service("newsArticleScrapingService")
class NewsArticleScrapingServiceImpl(
private val newsArticleService: NewsArticleService,
private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"),
) : ScrapingService {
override fun scrapESGEconomy(): List<NewsArticle> =
scrapNewsArticle(
// 크롤링할 페이지 base url
baseUrl = "https://www.esgeconomy.com/",
// 이미 DB에 있는 제목은 크롤링 하지 않기 위해 입력
existingTitles = newsArticleService.findAllTitles().toSet(),
// base url에서 몇 개의 뉴스 기사를 가져올건지
pageSize = 12,
// 앞선 목차에서 얻은 Selector 경로
titleSelector = "#skin-12 > div:nth-child(\$page) > div > a > strong",
searchUrlSelector = "#skin-12 > div:nth-child(\$page) > div > a",
contentSelector = "#article-view-content-div p",
authorSelector = "#article-view > div > header > div > article:nth-child(1) > ul > li:nth-child(1)",
publishedSelector = "#article-view > div > header > div > article:nth-child(1) > ul > li:nth-child(2)",
imageSelector = "#article-view-content-div > div:nth-child(1) > figure > div > img",
imageCaptionSelector = "#article-view-content-div > div:nth-child(1) > figure > div > img",
)
private fun scrapNewsArticle(
baseUrl: String,
existingTitles: Set<String>,
pageSize: Int,
titleSelector: String,
searchUrlSelector: String,
contentSelector: String,
authorSelector: String,
publishedSelector: String,
imageSelector: String,
imageCaptionSelector: String,
): List<NewsArticle> {
// Jsoup을 사용하여 베이스 URL에 연결하고 HTML 문서를 파싱
val rootDoc = Jsoup.connect(baseUrl).get()
// 스크래핑한 뉴스 기사들을 담을 리스트
val newsArticleList = mutableListOf<NewsArticle>()
// 지정된 페이지 수만큼 반복
for (page in 1..pageSize) {
// replace를 사용하여 $page를 실제 페이지 번호로 대체하고,
// select로 해당 selector에 매칭되는 요소의 텍스트를 가져옴
val title = rootDoc.select(titleSelector.replace("\$page", "$page")).text().trim()
// 이미 존재하는 제목이면 다음 페이지로 스킵
if (title in existingTitles) {
continue // 중복 크롤링 방지
}
// 기사 상세 페이지 URL 생성
// attr("href")로 링크 주소를 가져오고 baseUrl과 결합
val searchUrl =
baseUrl + rootDoc.select(searchUrlSelector.replace("\$page", "$page")).attr("href")
// 상세 페이지 HTML 파싱
val doc = Jsoup.connect(searchUrl).get()
// 본문 내용 추출: p 태그들의 텍스트를 줄바꿈으로 연결
val content = doc.select(contentSelector).joinToString("\n") { it.text() }
// 작성자 정보 추출
val author = doc.select(authorSelector).text().trim()
// 발행일 추출: substring으로 필요한 부분만 잘라내고 날짜 형식으로 파싱
val publishedAt =
LocalDate.parse(
doc
.select(publishedSelector)
.text()
.substring(3, 13),
dateFormatter,
)
// 수집한 정보로 NewsArticle 객체 생성
val newsArticle =
NewsArticle(
title = title,
content = content,
source = searchUrl,
author = author,
publishedAt = publishedAt,
aiSummary = null,
)
// 이미지 정보 추출
// attr("src")로 이미지 URL을, attr("alt")로 이미지 설명을 가져옴
val imageUrl = doc.select(imageSelector).attr("src").trim()
val imageCaption = doc.select(imageCaptionSelector).attr("alt").trim()
// 이미지가 존재하는 경우에만 Image 객체 생성
if (imageUrl.isNotEmpty()) {
val image =
Image(
url = imageUrl,
caption = imageCaption,
orderIndex = 0,
newsArticle = newsArticle,
)
newsArticle.imageList = listOf(image)
}
// 완성된 뉴스 기사 객체를 리스트에 추가
newsArticleList.add(newsArticle)
}
return newsArticleList
}
}
ESG 환경의 뉴스 기사를 수집해서 반환하는 코드이다. 주석을 달아놨으니 참고하면 된다.
주요 Jsoup 함수 설명
- Jsoup.connect(url).get()
- 지정된 URL에 HTTP GET 요청을 보내고 HTML 문서를 파싱 하여 Document 객체 반환
- 네트워크 연결이 필요하므로 예외처리가 필요할 수 있음
- document.select(selector)
- CSS 실렉터를 사용하여 HTML 문서에서 원하는 요소를 선택
- 반환값은 Elements 객체 (여러 요소의 컬렉션)
- element.text()
- HTML 요소의 텍스트 내용만 추출
- HTML 태그는 제거되고 순수 텍스트만 반환
- element.attr(attributeName)
- HTML 요소의 특정 속성값을 가져옴
- 예: attr("href")는 링크 주소, attr("src")는 이미지 URL
- joinToString()
- 컬렉션의 각 요소를 지정된 구분자로 연결하여 하나의 문자열로 만듦
- 여기서는 본문의 각 단락을 줄 바꿈(\n)으로 연결
3-3. 테스트


원하는 정보를 추출할 수 있었다.