Skip to content

GamJaDo/MOONPD_Crawling_Backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MOONPD Crawling Backend

지자체 및 문화재단 웹사이트에서 PDF 문서(문화행사 정보 등)를 자동으로 수집하는 크롤링 서버

개요

전국 지자체/문화재단 웹사이트는 구조가 제각각입니다. 어떤 사이트는 한 페이지에 PDF가 다 있고, 어떤 사이트는 글 목록을 클릭해야 하고, 어떤 사이트는 연도별 필터가 있습니다. 링크 방식도 <a href="">, onclick, javascript:, 커스텀 속성 등 다양합니다.

이 서버는 크롤링 대상 정보(Target)를 DB에 저장해두고, 저장된 설정을 기반으로 다양한 구조의 웹사이트를 자동으로 크롤링합니다.

기술 스택

  • Java 21
  • Spring Boot 3.x
  • Spring Data JPA
  • Selenium WebDriver (동적 페이지 처리)
  • ChromeDriver

핵심 개념

1. Target (크롤링 대상)

크롤링할 웹사이트의 정보를 담고 있는 엔티티입니다. 어떤 사이트를, 어떤 방식으로, 어떤 요소를 크롤링할지 모든 정보가 들어있습니다.

필드 설명
organizationType 기관 유형 (지자체/문화재단)
region 지역 (예: 경상남도)
group 기관명 (예: 의령군)
structureType 페이지 구조 유형
pageUrl 크롤링 시작 URL
totalPage 총 페이지 수
lst* 목록(글 리스트) 요소 정보
pdf* PDF 링크 요소 정보
title* 제목 요소 정보
year* 연도 필터 요소 정보
nextPage* 페이지네이션 요소 정보

2. CrawlingData (수집된 데이터)

크롤링 결과로 수집된 PDF 정보입니다.

필드 설명
target 어떤 Target에서 수집했는지
pdfUrl PDF 다운로드 URL
title PDF 제목
crawlingTime 수집 시간

페이지 구조 유형 (StructureType)

SINGLE_PAGE

한 페이지에 PDF 링크와 제목이 모두 있는 경우

┌─────────────────────────────┐
│  [PDF] 2024년 문화행사 안내  │
│  [PDF] 2024년 축제 일정      │
│  [PDF] 공연 안내             │
│         1  2  3  4  →       │
└─────────────────────────────┘

처리 흐름:

  1. 페이지 접속
  2. PDF 링크들과 제목들 추출
  3. DB 저장
  4. 다음 페이지로 이동 (반복)

LISTED_CONTENT

글 목록 형식으로, 각 글을 클릭해야 PDF가 있는 경우

┌─────────────────────────────┐
│  📄 2024년 문화행사 안내     │  ← 클릭하면
│  📄 2024년 축제 일정         │
│  📄 공연 안내                │
└─────────────────────────────┘
              ↓
┌─────────────────────────────┐
│  2024년 문화행사 안내        │
│  ───────────────────────    │
│  첨부파일: [다운로드]        │  ← 여기서 PDF 추출
└─────────────────────────────┘

처리 흐름:

  1. 목록 페이지 접속
  2. 각 글 링크 추출
  3. 글 하나씩 접속 → PDF 링크 추출 → DB 저장
  4. 다음 페이지로 이동 (반복)

YEAR_FILTERED

연도별 필터를 선택해야 콘텐츠가 나오는 경우

┌─────────────────────────────┐
│  [2024] [2023] [2022]       │  ← 연도 선택
│  ───────────────────────    │
│  [PDF] 문화행사 안내         │
│  [PDF] 축제 일정             │
└─────────────────────────────┘

처리 흐름:

  1. 페이지 접속
  2. 연도 버튼들 추출
  3. 각 연도 클릭 → SINGLE_PAGE 또는 LISTED_CONTENT 방식으로 처리
  4. 다음 연도로 이동 (반복)

링크 타입 (LinkType)

웹사이트마다 링크를 구현하는 방식이 다릅니다.

HREF_LINK

일반적인 <a href="..."> 링크

<a href="/download/file.pdf">다운로드</a>

ONCLICK_LINK

onclick 이벤트로 동작하는 링크

<a onclick="fn_egov_downFile('FILE_001', '1')">다운로드</a>
<a onclick="fn_view('123', 'BBS001')">상세보기</a>

지원하는 onclick 함수 (PDF):

  • fn_egov_downFile(atchFileId, fileSn)/cmm/fms/FileDown.do
  • yhLib.file.download(atchFileId, fileSn)/common/file/download.do
  • pdf_download(bcd, bn, num)/fnc_bbs/user_bbs_download
  • openDownloadFiles(uid)/programs/board/download.do
  • gfnFileDownload(fileNo, fileNm)/cmmn/file/download
  • cf_downloadPDF(fmKeyNo)/async/file/PDFdownload.do
  • location.href='...'

지원하는 onclick 함수 (목록):

  • fn_view(nttId, bbsId)/board/viewArticle.do
  • fn_articleLink(boardIdx)/kor/boardView.do
  • goTo.view(bIdx, ptIdx, mId)/portal/bbs/view.do
  • goDetail(idx)/portal/ebook/siboDetail.do
  • fn_search_detail(nttId)/prog/bbsArticle/.../view.do

JAVASCRIPT_LINK

href="javascript:..." 형태

<a href="javascript:goPage(2)">2</a>

PSEUDO_LINK

실제 URL 없이 커스텀 속성만 있는 경우

<div data-idx="123" data-file-key="abc">다운로드</div>

지원하는 속성:

  • data-idx
  • data-req-get-p-idx
  • data-file-key

OPTION_LINK

<select> 드롭다운으로 필터링하는 경우

<select>
  <option value="2024">2024년</option>
  <option value="2023">2023년</option>
</select>

요소 선택 방식

각 요소(목록, PDF, 제목, 페이지네이션)를 찾기 위해 CSS Selector를 사용합니다.

구성 요소

필드 설명 예시
parent*Identifier 부모 요소 식별자 boardList
parent*TagType 부모 요소 태그 DIV, UL, TABLE
parent*SelectorType 식별 방식 CLASS, ID, STYLE
child*Identifier 자식 요소 식별자 item
child*TagType 자식 요소 태그 A, LI, TR
child*SelectorType 식별 방식 CLASS, ID, STYLE
*OrdinalNumber n번째 자식 선택 1, 2, 0(전체)

CSS Selector 생성 로직

// 예: parentIdentifier="boardList", parentTagType=DIV, parentSelectorType=CLASS
//     childIdentifier="", childTagType=A, ordinalNumber=0
// 결과: div.boardList > a

// 예: parentIdentifier="list", parentTagType=UL, parentSelectorType=ID
//     childIdentifier="item", childTagType=LI, childSelectorType=CLASS, ordinalNumber=1
// 결과: ul#list > li.item:nth-child(1)

Extended 옵션

특정 조상 요소 안에 있는 것만 필터링하고 싶을 때 사용합니다.

extended*Type: ON/OFF
extended*Identifier: 조상 요소 식별자
extended*TagType: 조상 요소 태그
extended*SelectorType: 식별 방식

제목 추출 위치 (TitleType)

OUT

목록 페이지에서 제목을 추출 (LISTED_CONTENT에서 사용)

[목록 페이지]
├── 제목1  ← 여기서 추출
├── 제목2
└── 제목3

IN

상세 페이지에서 제목을 추출

[상세 페이지]
┌─────────────────┐
│ 제목: 문화행사   │  ← 여기서 추출
│ 첨부: [PDF]     │
└─────────────────┘

API

크롤링 실행

POST /api/crawling

DB에 저장된 모든 Target을 순회하며 크롤링을 실행합니다.

Target 관리

메서드 엔드포인트 설명
GET /api/target/view 전체 Target 조회
POST /api/target/register 새 Target 등록
PUT /api/target/{targetId} Target 수정
GET /api/target/{targetId} 크롤링 결과 조회 (수집 건수, 최근 수집 시간)

요청 예시

LISTED_CONTENT + ONCLICK_LINK 예시

{
  "organizationType": "MUNICIPALITY",
  "region": "경상남도",
  "group": "의령군",
  
  "structureType": "LISTED_CONTENT",
  
  "lstType": "ONCLICK_LINK",
  "parentLstIdentifier": "htoListCnt",
  "parentLstTagType": "LI",
  "parentLstSelectorType": "CLASS",
  "childLstIdentifier": "",
  "childLstTagType": "A",
  "childLstSelectorType": "NONE",
  "lstOrdinalNumber": 1,
  
  "extendedLstType": "OFF",
  
  "pageUrl": "https://www.uiryeong.go.kr/board/list",
  "totalPage": 4,
  
  "pdfType": "HREF_LINK",
  "parentPdfIdentifier": "gnb",
  "parentPdfTagType": "DIV",
  "parentPdfSelectorType": "CLASS",
  "childPdfIdentifier": "",
  "childPdfTagType": "A",
  "childPdfSelectorType": "NONE",
  "pdfOrdinalNumber": 1,
  
  "extendedPdfType": "OFF",
  
  "titleType": "OUT",
  "parentTitleIdentifier": "photoInfoBox",
  "parentTitleTagType": "DIV",
  "parentTitleSelectorType": "CLASS",
  "childTitleIdentifier": "photoTitle",
  "childTitleTagType": "P",
  "childTitleSelectorType": "CLASS",
  "titleOrdinalNumber": 1,
  
  "extendedTitleType": "OFF",
  
  "nextPageType": "HREF_LINK",
  "parentNextPageIdentifier": "bdNumWrap",
  "parentNextPageTagType": "DIV",
  "parentNextPageSelectorType": "CLASS",
  "childNextPageIdentifier": "",
  "childNextPageTagType": "A",
  "childNextPageSelectorType": "NONE",
  "nextPageOrdinalNumber": 0,
  
  "yearType": "NONE"
}

YEAR_FILTERED 예시

{
  "organizationType": "CULTURE_FOUNDATION",
  "region": "서울특별시",
  "group": "서울문화재단",
  
  "structureType": "YEAR_FILTERED",
  
  "yearType": "ONCLICK_LINK",
  "parentYearIdentifier": "yearFilter",
  "parentYearTagType": "DIV",
  "parentYearSelectorType": "CLASS",
  "childYearIdentifier": "",
  "childYearTagType": "BUTTON",
  "childYearSelectorType": "NONE",
  "yearOrdinalNumber": 0,
  
  "lstType": "HREF_LINK",
  ...
}

프로젝트 구조

src/main/java/Intern/moonpd_crawling/
│
├── MoonpdCrawlingApplication.java       # 메인 클래스
│
├── config/
│   └── WebConfig.java                   # CORS 설정
│
├── constants/
│   └── KeywordConstants.java            # onclick 함수명, 페이지 파라미터 등 상수
│
├── controller/
│   ├── CrawlingController.java          # POST /api/crawling
│   └── TargetController.java            # Target CRUD API
│
├── dto/
│   ├── request/
│   │   ├── TargetRegisterRequest.java   # Target 등록 요청
│   │   └── TargetUpdateRequest.java     # Target 수정 요청
│   └── response/
│       ├── TargetViewResponse.java      # Target 목록 응답
│       └── CrawlingResultResponse.java  # 크롤링 결과 응답
│
├── entity/
│   ├── Target.java                      # 크롤링 대상 정보
│   └── CrawlingData.java                # 수집된 PDF 정보
│
├── exception/
│   ├── GlobalExceptionHandler.java
│   ├── NotFoundException.java
│   ├── DuplicateException.java
│   ├── CrawlingException.java
│   ├── WebDriverException.java
│   └── JavaScriptException.java
│
├── repository/
│   ├── TargetRepository.java
│   └── CrawlingDataRepository.java
│
├── service/
│   ├── CrawlingService.java             # 메인 크롤링 로직 (Target 순회)
│   ├── CrawlDetailPageService.java      # 상세페이지 크롤링
│   └── TargetService.java               # Target CRUD
│
├── status/type/                         # Enum 정의
│   ├── OrganizationType.java            # MUNICIPALITY, CULTURE_FOUNDATION
│   ├── StructureType.java               # SINGLE_PAGE, YEAR_FILTERED, LISTED_CONTENT
│   ├── LinkType.java                    # HREF_LINK, ONCLICK_LINK, PSEUDO_LINK, ...
│   ├── TagType.java                     # DIV, A, LI, TABLE, ...
│   ├── SelectorType.java                # CLASS, ID, STYLE
│   ├── TitleType.java                   # IN, OUT
│   └── ExtendedType.java                # ON, OFF
│
└── util/
    ├── CrawlConfluenceUtil.java         # 링크 타입별 크롤링 분기 처리
    ├── CrawlPdfConfluenceUtil.java      # PDF 링크 타입별 처리
    ├── ElementFinderUtil.java           # CSS Selector로 요소 찾기
    ├── ElementFilterUtil.java           # 유효한 링크만 필터링
    ├── ElementExtendedUtil.java         # Extended 필터링
    ├── ElementCountUtil.java            # 페이지/연도 수 계산
    ├── ElementLinkExtractorUtil.java    # 링크 추출
    ├── NextPageLinkExtenderUtil.java    # 페이지네이션 링크 확장
    ├── GapAwarePdfExtractorUtil.java    # 제목-PDF 매칭 (빈 슬롯 처리)
    │
    ├── structure/                       # 페이지 구조별 처리
    │   ├── SinglePageStructureUtil.java
    │   ├── ListedContentStructureUtil.java
    │   └── YearFilteredStructureUtil.java
    │
    ├── lstCrawling/                     # 목록 링크 처리
    │   ├── HrefLinkLstUtil.java
    │   ├── OnClickLinkLstUtil.java
    │   ├── JavaScriptLinkLstUtil.java
    │   └── PseudoLinkLstUtil.java
    │
    ├── pdfCrawling/                     # PDF 링크 추출
    │   ├── HrefLinkPdfUtil.java
    │   ├── OnClickLinkPdfUtil.java
    │   ├── JavaScriptLinkPdfUtil.java
    │   └── PseudoLinkPdfUtil.java
    │
    ├── nextPageCrawling/                # 페이지네이션 처리
    │   ├── HrefLinkNextPageUtil.java
    │   └── OnClickLinkNextPageUtil.java
    │
    └── yearCrawling/                    # 연도 필터 처리
        ├── HrefLinkYearUtil.java
        └── OnClickLinkYearUtil.java

크롤링 흐름도

CrawlingController.startCrawling()
         │
         ▼
CrawlingService.startCrawling()
         │
         ├── Target 전체 조회
         │
         ▼
    ┌────────────────────────────────────────┐
    │         StructureType 분기             │
    └────────────────────────────────────────┘
         │              │              │
         ▼              ▼              ▼
   SINGLE_PAGE    LISTED_CONTENT   YEAR_FILTERED
         │              │              │
         ▼              ▼              ▼
  SinglePage       ListedContent    YearFiltered
  StructureUtil    StructureUtil    StructureUtil
         │              │              │
         │              ▼              │
         │     CrawlConfluenceUtil     │
         │     .crawlLst()             │
         │         │                   │
         │         ├── ONCLICK → OnClickLinkLstUtil
         │         ├── HREF → HrefLinkLstUtil
         │         ├── PSEUDO → PseudoLinkLstUtil
         │         └── JAVASCRIPT → JavaScriptLinkLstUtil
         │              │
         │              ▼
         │     CrawlDetailPageService
         │     .crawlSubPage()
         │              │
         ▼              ▼
  CrawlPdfConfluenceUtil.crawlPdf()
         │
         ├── ONCLICK → OnClickLinkPdfUtil
         ├── HREF → HrefLinkPdfUtil
         ├── PSEUDO → PseudoLinkPdfUtil
         └── JAVASCRIPT → JavaScriptLinkPdfUtil
                   │
                   ▼
         CrawlingDataRepository.save()

설정

ChromeDriver 경로

// CrawlingService.java, CrawlDetailPageService.java
System.setProperty("webdriver.chrome.driver", "C:\\tools\\chromedriver-win64\\chromedriver.exe");

CORS 설정

// WebConfig.java
.allowedOrigins("http://localhost:5173")  // 프론트엔드 주소

데이터베이스

application.yml에서 설정 (기본: H2 또는 MySQL)


실행

# ChromeDriver 설치 필요
./gradlew bootRun

중복 방지

이미 수집된 PDF URL은 다시 저장하지 않습니다.

if (crawlingDataRepository.existsByPdfUrl(pdfLink)) {
    continue;  // 스킵
}

라이센스

MIT License

About

⚙️Moonpd 서비스 - 웹 크롤링 기능

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages