1. 서론
Relogging 프로젝트에서 뉴스 기사를 스크래핑한 후 ai를 통해 요약하고 원문을 보고 싶다면 원본 사이트로 리다이렉션 하는 기능을 구현하자는 아이디어가 나왔다. 토스 증권의 뉴스 3줄 요약 기능을 참고하여 아이디어를 얻었다.
크롤링과 스크래핑 차이
웹 크롤링은 웹 페이지의 링크를 타고 계속해서 탐색을 이어나가지만, 웹 스크래핑은 데이터 추출을 원하는 대상이 명확하여 특정 웹 사이트만을 추적한다는 차이점이 있다.
참고: https://blog.hectodata.co.kr/crawling_vs_scraping/
2. 리소스 Selector 경로 얻기
내가 이전에 작성했던 블로그 포스팅의 내용을 스크래핑해 보도록 하겠다. 리소스의 위치가 고정되어 있는 경우 이런 방법을 통해 스크래핑할 수 있다. 많은 뉴스 플랫폼의 웹 페이지도 같은 방식으로 구성되어 있으니 이 방법을 참고하면 된다.
수프래핑할 웹 페이지이다.
https://campus-coder.tistory.com/169
원하는 페이지에 들어가서 리소스의 경로를 얻어보자
크롬 창 오른쪽 위 점 세 개를 클릭 -> 도구 더 보기 -> 개발자 도구
원하는 리소스의 위치를 얻기 위하여 검색을 활용할 수 있다.
목차 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. 테스트
원하는 정보를 추출할 수 있었다.
'백엔드 > Kotlin + Spring' 카테고리의 다른 글
[Kotlin Spring] 스프링 스케줄러를 이용한 매일 오전 3시 작업 예약하기 (0) | 2024.11.16 |
---|---|
[Kotlin Spring] Relogging - AWS S3를 이용한 이미지 호스팅 (0) | 2024.11.10 |
[Kotlin Spring] Relogging - Spring AI 활용 구현 정리 (OpenAI, ChatGPT API 사용하기) (4) | 2024.11.07 |
[Kotlin Spring] Relogging - 파일 다중 업로드와 Http 415 오류 및 해결 (스웨거 오류, MultipartFile) (1) | 2024.10.16 |