티스토리 뷰

현재 KOSTA 에서 진행중인 Java 개발자 과정을 듣고 있으며, 약 3주후에 Final Project가 시작된다.

그 전에 몰랐던 내용, 알아두면 좋을 내용을 공부중인데 이전 프로젝트에서 웹 크롤링하여 데이터를 가져오는 부분에 대해서

어려움을 겪었던지라 Java 로 웹 크롤링 방법을 찾아보던 중 Jsoup 라이브러리를 알게되어 사용법을 포스팅하려 한다.

 

사실 몇 시간동안 정성들여 작성하던 중에 거의 90% 작성 되어가던 중 작성 내용이 날라가서 멘탈이 흔들리는 중이다...

그래서 정성을 좀만 덜어내고 작성하려고 한다. ㅎㅎ..

 

크롤링 타겟 주소 : http://www.cgv.co.kr/movies/?lt=1&ft=0

 

무비차트 < 무비차트 | 깊이 빠져 보다, CGV

베테랑2 예매율20.6% 87% 2024.09.13 개봉 예매

www.cgv.co.kr

 

 

 

1. Jsoup 라이브러리 설치

 

Maven, Gradle, .Jar(순수 Java) 환경에 따라 설치 방법이 달라진다. 

 

1-1. Maven

<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.18.1</version>
</dependency>

 

 

1-2. Gradle

dependencies {
    implementation 'org.jsoup:jsoup:1.18.1'
}

 

 

1-3. Jar 파일

 

라이브러리 설치 경로 : https://jsoup.org/download

 

Download and install jsoup

Download and install jsoup jsoup is available as a downloadable .jar java library. The current release version is 1.18.1. What's new See the 1.18.1 release announcement for the latest changes, or the changelog for the full history. Previous releases of jso

jsoup.org

 

 

아래와 같이 .jar 파일을 다운받아서 Import 해주면 되겠다.

 

 

나는 이클립스를 사용하고 있어서 아래와 같은 경로에 파일을 넣어두고 Import 하였다.

 

 

 

2. Jsoup 라이브러리 사용 (웹 크롤링)

 

우선, 크롤링할 타겟 주소로 이동하자.

 

여기서 내가 뽑아내려고 하는 데이터는 각 영화의 이름, 포스터 이미지 url, 예매율, 개봉일자, 영화 장르, 영화 상세 정보이다.

 

데이터를 뽑아내기 위한 규칙을 찾기 위해 개발자 도구를 사용한다. 

난 맥북이므로 option + command + I 버튼을 눌러 사용한다. (윈도우는 F12)

개발자 도구 탭 확대

 

이곳에서 우리가 얻을 수 있는 값은 box-contents 라는 div 태그 안에 title 이라는 class에 영화제목이 담겨있다는 것.

box-image 라는 div 태그 안에, thumb-image 라는 클래스 안에 src 속성으로 포스터의 이미지 url 이 있는것을 볼 수 있다.

이 외에도 개봉일자와 예매율을 알 수 있다.

 

 

우선 크롤링 코드를 보기전에 간단하게 Jsoup 라이브러리의 핵심 함수들을 알아보자.

 

 

Document - Connect 이후 받은 HTML 전체 문서

우리가 크롤링 하려는 URL 주소의 페이지에 HTML 전체 문서를 가져온다.

 

작성 코드

package src.main.java.code;

import java.net.URL;
import java.util.Iterator;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import src.main.java.vo.Movie;

public class test2 {
    public static void main(String[] args) {
        String URL = "http://www.cgv.co.kr/movies/?lt=1&ft=0";
        Document doc = null; //Document 객체 생성

        try {
            doc = Jsoup.connect(URL).get(); //Jsoup 클래스로 url 연결하여 정보를 doc에 담음
        } catch(IOException e) {
            e.printStackTrace();
        }

        System.out.println(doc);
    }
}

 

 

결과

 

 

Elements - Element 가 모인 자료형

 

아래서 설명할 Element 를 모아놓은 자료형이다.

 

 

Element - Document의 HTML 요소

Document 의 HTML 요소를 담은 값이다.

 

예를 들어, 아래 코드에서 String title = "strong.title"; 이라는 값은 Document 에서 strong 태그 중 title 클래스를 가진 태그의 값을 뽑아내기 위한 변수를 선언한 것이다.

 

뽑아내려는 조건의 작성 양식?방법은 CSS의 선택자 작성법과 동일한 것 같다.

 

doc.select(title); 메서드를 통해서 조건에 해당하는 Elements 를 받아온다.

 

영화 하나당 title이 하나 존재한다. 현재 페이지에서는 19개의 영화를 보여주므로 19개의 데이터(Element)가 담기게 된다.

 

작성 코드

package src.main.java.code;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Iterator;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import src.main.java.vo.Movie;

public class test2 {
    public static void main(String[] args) {
        String URL = "http://www.cgv.co.kr/movies/?lt=1&ft=0";
        Document doc = null; //Document 객체 생성

        try {
            doc = Jsoup.connect(URL).get(); //Jsoup 클래스로 url 연결하여 정보를 doc에 담음
        } catch(IOException e) {
            e.printStackTrace();
        }

        String title = "strong.title"; //영화제목 (DevTools에서 확인한 경로를 적는다.)	   
        Elements e1 = doc.select(title); //해당 url에서 영화제목 정보만 e1에 담는다. 

        Iterator<Element> itr1 = e1.iterator(); //영화제목 정보를 요소 별로 분리

        while(itr1.hasNext()) {
            Element e = itr1.next();
            System.out.println(e);
        }
    }
}

 

 

결과

 

 

아무튼 위를 기반으로 1차적으로 크롤링을 위한 코드는 아래와 같다.

package src.main.java.code;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Iterator;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import src.main.java.vo.Movie;

public class test {
    public static void main(String[] args) {
        String URL = "http://www.cgv.co.kr/movies/?lt=1&ft=0";
        Document doc = null; //Document 객체 생성
        URL url = null; //이미지를 담아올 URL 객체 생성
        InputStream in = null; //InputStream 객체 생
        OutputStream out = null; //OutputStream 객체 생성

        try {
            doc = Jsoup.connect(URL).get(); //Jsoup 클래스로 url 연결하여 정보를 doc에 담음
        } catch(IOException e) {
            e.printStackTrace();
        }

        String title = "strong.title"; //영화제목 (DevTools에서 확인한 경로를 적는다.)
        Elements e1 = doc.select(title); //해당 url에서 영화제목 정보만 e1에 담는다. 

        String img = ".thumb-image > img"; //이미지 (DevTools에서 확인한 경로를 적어준다.)
        Elements e2 = doc.select(img); //해당 url에서 영화 이미지 정보만 e2에 담는다.

        Iterator<Element> itr1 = e1.iterator(); //영화제목 정보를 요소 별로 분리
        Iterator<Element> itr2 = e2.iterator(); //이미지 정보를 요소 별로 분리

        int N = 1; //파일 이름 중복을 피하기 위해 변수를 사용?
        int num = 0;

        while(itr2.hasNext()) {
            try {				
                Movie vo = new Movie();

                String img2 = itr2.next().attr("src"); //영화 이미지 정보의 속성값(attribute)인 src(이미지 주소를) 담는다.
                url = new URL(img2); //url 객체에 이미지 주소를 담는다.
                in = url.openStream(); // in객체에 url 정보를 담는다.(받고싶은 데이터 연결)
                out = new FileOutputStream("/Users/baegdohyeon/eclipse-workspace/BAEK/cTest/"+N+".png"); //out 객체에 저장경로(저장위치) 지정
                N++;

                while(true) {
                    int data = in.read(); // in 객체로 해당 이미지를 읽어들임
                    if (data == -1) break; // 더이상 읽을것이 없다면 멈춘다.
                    out.write(data); // 읽어들인 데이터를 경로에 작성
                }
                in.close();
                out.close();

                String title2 = itr1.next().text(); // text()를 사용하여 태그를 제외한 영화 제목 정보를 담음

                vo.setTitle(title2);
                vo.setImage(img2);
            } catch(Exception e) { 
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                    out.close();
                } catch(IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

이때 사용된 URL 클래스와 InputStream, OutputStream, FileOutputStream 등은 이미지 경로로부터 파일을 int 타입의 data에 in.read 하여 파일의 끝인 EOF(-1)이 되면 out.write(data)로 data에 그 정보들을 작성하게 되고 다 사용하게 되면 in.close로 읽기를 닫고, out.close로 쓰기를 종료해주면 된다.

 

그리고 FileOutputStream("경로" + 파일명 + "확장자") 로 저장하면 정해놓은 경로에 해당 확장자로 이미지가 저장된다.

 

 

이후에 문제가 생겼다. 영화 장르와 영화의 상세 정보를 해당 페이지에서 볼 수 없고 영화 상세 페이지에 들어가야 알아낼 수 있었다.

 

이러한 상세페이지에 대한 링크주소를 보다 한가지 반복되는 특징이 발견된다.

바로 주소가 http://www.cgv.co.kr/movies/detail-view/?midx=[영화PK값] 라는 일정한 구조를 가지고 있다는 것이다.

 

해당 주소를 크롤링을 통해 어떻게 알아낼 수 있을까?

개발자 도구를 사용하여 찾아내었다.

 

아래와 같이 크롤링 타겟 주소에서 개발자도구를 사용하면 아래와 같이 box-image 클래스를 가지는 div 태그 안에 있는 href 링크가 무비차트 페이지안에 내포되어 있는것을 볼 수 있다.

 

이 링크를 누르면 각각의 영화의 상세정보보기로 이동할 수 있는 것이다.

그래서 그 내용물을 가져오기 위한 상세정보보기 주소의 경로를 "div.box-image>a" 로 결정하였다.

 

 

두 번째 문제는 장르를 빼오려고 할 때, 많은 <dt> 태그들 때문에 내가 원하는 장르만 빼오기가 곤란해졌다.

그냥 <dt> 태그를 이터레이터로 가져오게 되면 장르 뿐만 아니라 감독 / 프로듀서 / 배우 / 장르 등등이 같이 출력 된다.

그래서 크롤링 중에 "정제" 단계를 거치게 될 수 밖에 없다.

 

해결 방법으로는 

우선, 각 영화의 <dt>의 정보 (감독 / 배우 / 장르 등등...)을 빼온다.

그리고 indexOf() 메서드를 사용하여 "장르 : " 라는 내용이 포함된 정보만 색출해낸다.

이때, indexOf() 메서드는 "장르: " 라는 내용이 없으면 -1을 반환한다는 것을 알게 되었다.

 

결론적으로, indexOf("장르: ") 가 -1이 아니라면 "장르: " 뒤에 있는 값들을 얻기 위해 substring() 메서드로 "장르: "를 잘라내고 출력하게 되면 되는 것이다.

아래 코드는 최종적으로 완성한 코드이다.

package src.main.java.code;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Iterator;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import src.main.java.vo.Movie;

public class test {
    public static void main(String[] args) {
        String URL = "http://www.cgv.co.kr/movies/?lt=1&ft=0";
        Document doc = null; //Document 객체 생성
        URL url = null; //이미지를 담아올 URL 객체 생성
        InputStream in = null; //InputStream 객체 생
        OutputStream out = null; //OutputStream 객체 생성

        try {
            doc = Jsoup.connect(URL).get(); //Jsoup 클래스로 url 연결하여 정보를 doc에 담음
        } catch(IOException e) {
            e.printStackTrace();
        }

        String title = "strong.title"; //영화제목 (DevTools에서 확인한 경로를 적는다.)
        Elements e1 = doc.select(title); //해당 url에서 영화제목 정보만 e1에 담는다. 

        String img = ".thumb-image > img"; //이미지 (DevTools에서 확인한 경로를 적어준다.)
        Elements e2 = doc.select(img); //해당 url에서 영화 이미지 정보만 e2에 담는다.

        String category = "div.box-image>a"; //장르
        Elements e3 = doc.select(category); //해당 url 에서 영화 장르를 추출하기 위한 정보를 e3에 담는다. 
                                            //장르 정보가 완전히 정제되어 있지 않다.
                                            //영화 상세정보 페이지의 주소가 있는 a 태그이다.

        String percent = "strong.percent>span";
        Elements e5 = doc.select(percent);

        String textInfo = ".txt-info>strong";
        Elements e6 = doc.select(textInfo);

        Iterator<Element> itr1 = e1.iterator(); //영화제목 정보를 요소 별로 분리
        Iterator<Element> itr2 = e2.iterator(); //이미지 정보를 요소 별로 분리
        Iterator<Element> itr3 = e3.iterator(); //장르 정보를 요소 별로 분리
        Iterator<Element> itr5 = e5.iterator(); //
        Iterator<Element> itr6 = e6.iterator(); //개봉일

        int N = 1; //파일 이름 중복을 피하기 위해 변수를 사용?
        int num = 0;

        while(itr3.hasNext()) {
            try {				
                Movie vo = new Movie();

                String img2 = itr2.next().attr("src"); //영화 이미지 정보의 속성값(attribute)인 src(이미지 주소를) 담는다.
                url = new URL(img2); //url 객체에 이미지 주소를 담는다.
                in = url.openStream(); // in객체에 url 정보를 담는다.(받고싶은 데이터 연결)
                out = new FileOutputStream("/Users/baegdohyeon/eclipse-workspace/BAEK/cTest/"+N+".png"); //out 객체에 저장경로(저장위치) 지정
                N++;

                while(true) {
                    int data = in.read(); // in 객체로 해당 이미지를 읽어들임
                    if (data == -1) break; // 더이상 읽을것이 없다면 멈춘다.
                    out.write(data); // 읽어들인 데이터를 경로에 작성
                }
                
                in.close();
                out.close();

                String title2 = itr1.next().text(); // text()를 사용하여 태그를 제외한 영화 제목 정보를 담음
                String category2 = itr3.next().attr("href"); //장르르 정제하기 위해 속성값 href(상세정보 페이지) 추출              

                String percent2 = itr5.next().text();

                String textInfo2 = itr6.next().text();

                //추출한 주소에서 다시 한번 원하는 영화의 midindex 추출 (개별 영화의 상세정보 페이지)            
                String str = "http://www.cgv.co.kr" + category2;

                String url2 = str; //개별 영화의 상세정보 페이지
                Document doc2 = null;

                try {
                    doc2 = Jsoup.connect(url2).get(); // 상세정보 페이지에 연결하여 정보를 담는다.
                } catch(IOException e) {
                    e.printStackTrace();
                }

                String category3 = "div>dl>dt"; //감독, 배우, 장르 등등
                Elements e4 = doc2.select(category3); //영화소개정보(장르포함)만 e4에 담음.
                Iterator<Element> itr4 = e4.iterator();

                //하나의 루틴
                String movieInfo = "div.sect-story-movie";
                Elements e7 = doc2.select(movieInfo);
                Iterator<Element> itr7 = e7.iterator();
                String movieInfo2 = itr7.next().text();

                while(itr4.hasNext()) {
                    String category4 = itr4.next().text(); //태그를 제외한 영화정보

                    String a = "장르 : ";
                    int c = category4.indexOf(a); //장르만 추출해내기 위해 사용(장르가 아니면 -1 반환)
                    if(c != -1) {
                        System.out.println(title2);
                        System.out.println(img2);
                        System.out.println(category4.substring(5, category4.length()));
                        System.out.println(percent2);
                        System.out.println(textInfo2.substring(0, 13));
                        System.out.println(movieInfo2);
                        System.out.println();
                        vo.setGenre(category4.substring(5, category4.length()));
                    }
                }

                if(vo.getGenre() == null) continue; //장르가 없다면 영화를 넣지 않는다.

                vo.setTitle(title2);
                vo.setImage(img2);
                vo.setBookCnt(num);
            } catch(Exception e) { 
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                    out.close();
                } catch(IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

 

 

참고(출처)

https://note-summer.tistory.com/186

https://devsmaru.tistory.com/26#1.%20Jsoup%20%EC%84%A4%EC%B9%98%20%EB%B0%A9%EB%B2%95-1