Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,36 @@ public class AiPrompt {

// 내용 요약, 태그 추출, 카테고리 선정 프롬프트
public static final String SUMMARY_TAG_CATEGORY = """
너는 뉴스, 블로그 등 내용 요약 및 분류 AI야. 아래의 규칙에 따라 답변해.
[규칙]
1. 주어진 content를 50자 이상 100자 이하로 간단히 요약해라.
2. 아래 Category 목록 중에서 content와 가장 적절한 카테고리 하나를 정확히 선택해라.
- POLITICS("정치")
- ECONOMY("경제")
- SOCIETY("사회")
- IT("IT")
- SCIENCE("과학")
- CULTURE("문화")
- SPORTS("스포츠")
- ENVIRONMENT("환경")
- HISTORY("역사")
- WORLD("세계")
3. 내가 제공하는 태그 목록을 참고해서, content와 관련된 태그를 3~5개 생성해라.
- 제공된 태그와 중복 가능하다.
- 필요하면 새로운 태그를 만들어도 된다.
4. 출력은 반드시 아래 JSON 형식으로 해라. Markdown 문법(```)은 쓰지 마라.
- 해당 정보가 없으면 null말고 무조건 빈 문자열로 출력해줘라.
[출력 JSON 형식]
{
"summary": "내용 요약 (50~100자)",
"category": "선택된 카테고리 ENUM 이름",
"tags": ["태그1", "태그2", "태그3", ...]
}
[입력 데이터]
content: %s
existingTags: %s
너는 뉴스, 블로그 등 내용 요약 및 분류 AI야. 아래의 규칙에 따라 답변해.

[규칙]
1. 주어진 content를 50자 이상 100자 이하로 간단히 요약해라.
2. 아래 Category 목록 중에서 content와 가장 적절한 카테고리 하나를 정확히 선택해라.
- POLITICS("정치")
- ECONOMY("경제")
- SOCIETY("사회")
- IT("IT")
- SCIENCE("과학")
- CULTURE("문화")
- SPORTS("스포츠")
- ENVIRONMENT("환경")
- HISTORY("역사")
- WORLD("세계")
3. 내가 제공하는 태그 목록을 참고해서, content와 관련된 태그를 3~5개 생성해라.
- 제공된 태그와 중복 가능하다.
- 필요하면 새로운 태그를 만들어도 된다.
4. 출력은 반드시 아래 JSON 형식으로 해라. Markdown 문법(```)은 쓰지 마라.
- 해당 정보가 없으면 null말고 무조건 빈 문자열로 출력해줘라.

[출력 JSON 형식]
{
"summary": "내용 요약 (50~100자)",
"category": "선택된 카테고리 ENUM 이름",
"tags": ["태그1", "태그2", "태그3", ...]
}

[입력 데이터]
content: %s
existingTags: %s
""";
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import org.jsoup.nodes.Document;
import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.CrawlerResult;

import java.io.IOException;
import java.time.LocalDate;

public interface Crawler {
boolean supports(String domain);
CrawlerResult<?> extract(Document doc);
CrawlerResult<?> extract(Document doc) throws IOException;
LocalDate transLocalDate(String rawDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public boolean supports(String url) {
@Override
public CrawlerResult<?> extract(Document doc) {
// 불필요한 태그 제거
doc.select("script, style, noscript, iframe, nav, header, footer, form, aside, meta, link").remove();
doc.select("script, style, noscript, meta, link").remove();

// 본문만 가져오기 (HTML)
String cleanHtml = doc.body().html();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.tuna.zoopzoop.backend.domain.datasource.crawler.service;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.CrawlerResult;
import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.SpecificSiteDto;

import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class NaverBlogCrawler implements Crawler {
private static final SupportedDomain DOMAIN = SupportedDomain.NAVERBLOG;
private static final DateTimeFormatter NAVERBLOG_FORMATTER =
DateTimeFormatter.ofPattern("yyyy. M. d. HH:mm");

@Override
public boolean supports(String domain) {
return domain.contains(DOMAIN.getDomain());
}

@Override
public CrawlerResult<?> extract(Document doc) throws IOException {
/*
블로그 본문은 <iframe id="mainFrame"> 안에 로드되므로
먼저 메인 페이지를 가져온 뒤 iframe의 src를 추출하여
해당 URL로 다시 connect 해야 실제 본문 내용을 크롤링할 수 있다.
*/
Element iframe = doc.selectFirst("iframe#mainFrame");
String iframeUrl = iframe.absUrl("src");

Document iframeDoc = Jsoup.connect(iframeUrl)
.userAgent("Mozilla/5.0") // 크롤링 차단 방지를 위해 user-agent 설정 권장
.timeout(10 * 1000) // 타임아웃 (10초)
.get();

// 제목
Element titleSpans = iframeDoc.selectFirst("div.se-module.se-module-text.se-title-text");
String title = titleSpans.text();

// 작성일자
String publishedAt = iframeDoc.selectFirst("span.se_publishDate.pcol2").text();
LocalDateTime rawDate = LocalDateTime.parse(publishedAt, DateTimeFormatter.ofPattern("yyyy. M. d. HH:mm"));
LocalDate dataCreatedDate = rawDate.toLocalDate();

// 내용
Elements spans = iframeDoc.select(".se-main-container span");
StringBuilder sb = new StringBuilder();
for (Element span : spans) {
sb.append(span.text()); // 태그 안 텍스트만
}
String content = sb.toString();

// 썸네일 이미지 URL
Element img = iframeDoc.select("div.se-main-container img").first();

String imageUrl = "";
if (img != null) {
if (!img.attr("data-lazy-src").isEmpty()) {
imageUrl = img.attr("data-lazy-src");
}
}

// 출처
String source = "네이버 블로그";

return new CrawlerResult<>(
CrawlerResult.CrawlerType.SPECIFIC,
new SpecificSiteDto(title, dataCreatedDate, content, imageUrl, source)
);
}

@Override
public LocalDate transLocalDate(String rawDate) {
LocalDateTime dateTime = LocalDateTime.parse(rawDate, NAVERBLOG_FORMATTER);
return dateTime.toLocalDate(); // 시간 버리고 날짜만
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package org.tuna.zoopzoop.backend.domain.datasource.crawler.service;

public enum SupportedDomain {
NAVERNEWS("n.news.naver.com");
NAVERNEWS("n.news.naver.com"),
NAVERBLOG("blog.naver.com");

private final String domain;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import org.tuna.zoopzoop.backend.domain.datasource.crawler.dto.UnspecificSiteDto;
import org.tuna.zoopzoop.backend.domain.datasource.crawler.service.CrawlerManagerService;
import org.tuna.zoopzoop.backend.domain.datasource.crawler.service.GenericCrawler;
import org.tuna.zoopzoop.backend.domain.datasource.crawler.service.NaverBlogCrawler;
import org.tuna.zoopzoop.backend.domain.datasource.crawler.service.NaverNewsCrawler;

import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

Expand All @@ -40,11 +42,14 @@ public class CrawlerManagerServiceTest {
@Mock
private GenericCrawler genericCrawler;

@Mock
private NaverBlogCrawler naverBlogCrawler;

@BeforeEach
void setUp() {
// 직접 리스트를 생성자에 주입
crawlerManagerService = new CrawlerManagerService(
List.of(naverNewsCrawler, genericCrawler)
List.of(naverNewsCrawler, naverBlogCrawler, genericCrawler)
);
}

Expand Down Expand Up @@ -82,7 +87,7 @@ void FetchHtmlUrlTest() throws IOException {
}

@Test
void NaverCrawlerTest() throws IOException {
void NaverNewsCrawlerTest() throws IOException {
// given
String url = "https://n.news.naver.com/mnews/article/008/0005254080"; // 원하는 URL 넣기

Expand Down Expand Up @@ -128,17 +133,88 @@ void NaverCrawlerTest() throws IOException {
assertThat(naverDoc.source()).isEqualTo(sources);
}

@Test
void NaverBlogCrawlerTest() throws IOException {
// given
String url = "https://blog.naver.com/rainbow-brain/223387331292"; // 원하는 URL 넣기
// String url = "https://blog.naver.com/smhrd_official/223078242394";

Document doc = Jsoup.connect(url)
.userAgent("Mozilla/5.0") // 크롤링 차단 방지를 위해 user-agent 설정 권장
.timeout(10 * 1000) // 타임아웃 (10초)
.get();

Element iframe = doc.selectFirst("iframe#mainFrame");
String iframeUrl = iframe.absUrl("src");

Document iframeDoc = Jsoup.connect(iframeUrl)
.userAgent("Mozilla/5.0") // 크롤링 차단 방지를 위해 user-agent 설정 권장
.timeout(10 * 1000) // 타임아웃 (10초)
.get();

// 제목
Element titleSpans = iframeDoc.selectFirst("div.se-module.se-module-text.se-title-text");
String title = titleSpans.text();
System.out.println(title);

// 작성일자
String publishedAt = iframeDoc.selectFirst("span.se_publishDate.pcol2").text();
LocalDateTime rawDate = LocalDateTime.parse(publishedAt, DateTimeFormatter.ofPattern("yyyy. M. d. HH:mm"));
LocalDate dataCreatedDate = rawDate.toLocalDate();
System.out.println(dataCreatedDate);

// 내용
Elements spans = iframeDoc.select(".se-main-container span");
StringBuilder sb = new StringBuilder();
for (Element span : spans) {
sb.append(span.text()); // 태그 안 텍스트만
}
String content = sb.toString();
System.out.println(content);

// 썸네일 이미지 URL
Element img = iframeDoc.select("div.se-main-container img").first();

String imageUrl = "";
if (img != null) {
if (!img.attr("data-lazy-src").isEmpty()) {
imageUrl = img.attr("data-lazy-src");
}
}
System.out.println(imageUrl);

// 출처
String source = "네이버 블로그";


when(naverBlogCrawler.supports(url)).thenReturn(true);
given(naverBlogCrawler.extract(any(Document.class))).willCallRealMethod(); // 실제 메소드 실행
// given(naverBlogCrawler.transLocalDate(any(String.class))).willCallRealMethod();

// when
CrawlerResult<?> result = crawlerManagerService.extractContent(url);
SpecificSiteDto naverDoc = (SpecificSiteDto) result.data();

// then
assertThat(result.type()).isEqualTo(CrawlerResult.CrawlerType.SPECIFIC);
assertThat(naverDoc.title()).isEqualTo(title);
assertThat(naverDoc.content()).isEqualTo(content);
assertThat(naverDoc.dataCreatedDate()).isEqualTo(dataCreatedDate);
assertThat(naverDoc.imageUrl()).isEqualTo(imageUrl);
assertThat(naverDoc.source()).isEqualTo(source);
}

@Test
void GenericCrawlerTest() throws IOException {
// given
String url = "https://bcuts.tistory.com/421"; // 원하는 URL 넣기
String url = "https://blog.naver.com/rainbow-brain/223387331292"; // 원하는 URL 넣기

Document doc = Jsoup.connect(url)
.userAgent("Mozilla/5.0") // 크롤링 차단 방지를 위해 user-agent 설정 권장
.timeout(10 * 1000) // 타임아웃 (10초)
.get();

doc.select("script, style, noscript, iframe, nav, header, footer, form, aside, meta, link").remove();
doc.select("script, style, noscript, meta, link").remove();

String cleanHtml = doc.body().html();

Expand All @@ -149,10 +225,9 @@ void GenericCrawlerTest() throws IOException {
CrawlerResult<?> result = crawlerManagerService.extractContent(url);
UnspecificSiteDto genericDoc = (UnspecificSiteDto) result.data();

System.out.println(genericDoc.rawHtml());

// then
assertThat(result.type()).isEqualTo(CrawlerResult.CrawlerType.UNSPECIFIC);
assertThat(genericDoc.rawHtml()).contains("<!-- warp / 테마 변경시 thema_xxx 변경 -->");
// assertThat(genericDoc.rawHtml()).contains("<html");
assertThat(genericDoc.rawHtml()).isEqualTo(cleanHtml);
}
}