홈에는 어떤 것들이 있어야 할까라는 질문에 공지사항이라 답한 마약사범 형님의 의견에 따라, 아주광장의 주요 공지사항 일부를 스크래핑해 배치하기로 했다.
스크래핑
특정 웹 페이지에서 필요한 데이터(텍스트, 이미지, 메타데이터 등)를 추출하는 작업
크롤링
자동화된 프로그램(일명 웹 크롤러, 웹 스파이더, 봇 등)을 사용하여 웹 사이트의 여러 페이지를 탐색하면서 데이터를 수집하는 과정
두 정의는 약간 다르다. 나도 내가 한게 크롤링인 줄 알았다.
SpringBoot에서는 Jsoup 라이브러리를 통해 스크래핑과 간단한 크롤링 작업을 지원한다. 학교 홈페이지에서 공지사항 페이지의 HTML 구조를 분석하고 Jsoup 라이브러리를 이용해 일반공지와 장학공지, 고정된 공지사항들을 스크래핑하려 한다.
이 포스트에서는 일반공지만 스크래핑해보겠다.
1. 백엔드(Backend)
// Jsoup library
implementation 'org.jsoup:jsoup:1.15.3' // Jsoup 라이브러리
build.gradle에 Jsoup 종속성 추가.
Notice.java
package zerogod.ecetaskhelper.model;
public record Notice(String category, String title, String department, String date, String link) {
}
스크래핑할 정보를 저장하는 레코드 클래스를 정의한다. 고정된 공지사항을 스크래핑하고, 해당 페이지로 이동할 수 있는 링크 역할을 할 것이기 때문에 분류, 제목, 공지부서, 작성일을 스크래핑하고 저장할 것이다.
record 클래스
record는 불변 객체를 저장하는 클래스로 생성자, getter, hashCode(), equals() ,toString() 메소드를 기본으로 제공한다. 모든 필드는 final로 간주되어 변경할 수 없고 상속이 불가능하다.
그리고 공지사항을 스크래핑하고 REST API로 제공하는 NoticeController를 정의한다.
NoticeController.java
package zerogod.ecetaskhelper.controller;
import zerogod.ecetaskhelper.model.Notice;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@RestController
public class NoticeController {
@GetMapping("/api/notices")
public List<Notice> getNotices() throws IOException {
// 스크래핑할 URL
String url = "https://www.ajou.ac.kr/kr/ajou/notice.do";
// Jsoup으로 URL에서 HTML 문서를 가져옴
Document doc = Jsoup.connect(url).get();
List<Notice> notices = new ArrayList<>();
// 고정된 공지사항을 선택 (class="b-top-box"가 있는 tr 요소만)
Elements rows = doc.select("tr.b-top-box");
for (Element row : rows) {
String category = row.select("td").get(1).text(); // 분류 텍스트 추출
Element titleElement = row.selectFirst("td a"); // 제목과 링크가 포함된 요소 선택
String department = row.select("td").get(4).text(); // 공지부서 텍스트 추출
String date = row.select("td").get(5).text(); // 작성일자 텍스트 추출
if (titleElement != null) {
String title = titleElement.text(); // 제목 텍스트 추출
String articleNo = titleElement.attr("href").split("articleNo=")[1].split("&")[0];
String link = "https://www.ajou.ac.kr/kr/ajou/notice.do?mode=view&articleNo=" + articleNo;
// Notice 객체 생성 후 리스트에 추가
notices.add(new Notice(category, title, department, date, link));
}
}
// 작성일 기준으로 최신 5개의 공지사항만 반환
return notices.stream()
.sorted(Comparator.comparing(Notice::date).reversed()) // 최신 작성일 기준으로 정렬
.limit(5) // 상위 5개만 선택
.toList();
}
}
더 이상의 설명은 필요하지 않을 것 같다. 이 코드는 필요한 정보를 파싱하고, 최신 공지사항 5개를 반환하도록 설정했다. 모든 고정된 공지사항을 스크래핑하는 방법도 고려했지만, 아직 홈페이지의 전체적인 구성이 확정되지 않았기 때문에 우선적으로 5개만 반환하도록 했다. 만약 5개 이하의 공지사항만 있을 경우, 이를 처리하는 방법으로 모든 공지사항을 반환하거나, 예외처리를 추가하여 코드의 유연성을 높일 필요가 있다.
기록할 만한 점은 공지사항 링크 파싱 방법이다. 실제 공지사항 URL 형식은 https://www.ajou.ac.kr/kr/ajou/notice.do?mode=view&articleNo={식별번호}&article.offset=0&articleLimit=10과 같다. 여기서 {식별번호}는 각 공지사항의 고유 식별 번호를 나타낸다. 하지만, 이 URL에서 &article.offset=0&articleLimit=10 부분은 목록 페이지에서 10개의 공지사항을 나타내도록 하는 쿼리 파라미터로, 실제로 공지사항 세부 정보를 볼 때는 필수적이지 않다. 따라서 이 부분을 생략해도 공지사항 링크는 정상적으로 작동한다.
다음으로 HomePage.js에 다음 코드들을 추가한다.
2. 프론트엔드(Frontend)
HomePage.js
// 공지사항 데이터를 저장할 상태 변수 정의
const [notices, setNotices] = useState([]);
// 컴포넌트가 마운트될 때 공지사항 데이터를 서버로부터 가져오는 함수 호출
useEffect(() => {
fetchNotices().then(r => {}); // 공지사항 데이터를 가져오는 함수 호출
});
// 공지사항 데이터를 서버에서 가져오는 함수
const fetchNotices = async () => {
try {
// 서버의 API 엔드포인트에 GET 요청을 보내 공지사항 데이터를 가져옴
const response = await axios.get(`${serverUrl}/api/notices`);
setNotices(response.data); // 가져온 데이터를 상태에 저장
} catch (error) {
// 데이터 가져오는 중 오류 발생 시 처리
console.error("공지사항 조회 중 오류 발생:", error);
}
};
백엔드에서 스크래핑한 데이터를 가져온다. 그리고 다음과 같이 return문을 작성하면 공지사항이 테이블 형식으로 배치된다.
return (
<div className="home-page">
<table className="notice-table">
<thead>
<tr>
<th>분류</th>
<th>제목</th>
<th>공지부서</th>
<th>작성일</th>
</tr>
</thead>
<tbody>
{/* notices 배열을 순회하여 각 공지사항을 테이블 행으로 렌더링 */}
{notices.map((notice, index) => (
<tr key={index}>
<td>{notice.category}</td>
<td>
<a href={notice.link} target="_blank" rel="noopener noreferrer">
{notice.title}
</a>
</td>
<td>{notice.department}</td>
<td>{notice.date}</td>
</tr>
))}
</tbody>
</table>
</div>
);
주목할만한 점은 <a href={notice.link} target="_blank" rel="noopener noreferrer"> 태그이다.
<a> 요소는 공지사항의 제목을 링크로 만들어 사용자가 클릭하면 해당 공지사항의 세부 페이지로 이동할 수 있게 한다.
href={notice.link}는 링크의 URL을 지정하며, 여기서 notice.link는 스크래핑한 공지사항의 링크이다.
target="_blank"는 링크를 새 탭에서 열도록 설정한다.
rel="noopener noreferrer"는 보안 및 성능 향상을 위한 설정으로, 링크를 새 탭에서 열 때 부모 페이지와의 관계를 끊어준다.
그리고 적절하게 css를 설정해주면 다음과 같이 스크래핑이 완료된다.
3. 실행 결과
참고자료 및 출처
'Side Project > 중단' 카테고리의 다른 글
ECEtaskHelper: 콘솔로그 제거, 캐시(Cache) 설정을 통한 성능 개선 및 보안 강화 (0) | 2024.08.14 |
---|---|
ECEtaskHelper: CloudType을 이용해 배포하기 (0) | 2024.08.14 |
ECEtaskHelper: 시간표 페이지(SchedulePage) 구성하기(3) - 시간표 렌더링 (0) | 2024.08.10 |
ECEtaskHelper: PrivateRoute, 반응형 사이드바(NavBar) (1) | 2024.08.06 |
ECEtaskHelper: 시간표 페이지 (SchedulePage) 구성하기(2) - 과목 검색/담기/추가/삭제 (0) | 2024.08.05 |