본문 바로가기

개발자노트/웹

웹 크롤링 - Jsoup을 이용한 ( CGV ) 크롤링

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

 

영화 그 이상의 감동. CGV

큐어 예매율0.2% 92% 2022.07.06 개봉 예매

www.cgv.co.kr

CGV 무비차트 탭에 있는 영화정보에 대해 크롤링 해보려 한다.

 

 

Chrome 브라우저로 CGV 무비차트 탭에 접속한 뒤 키보드의 f12를 누르게 되면 DevTools 탭이 활성화 된다.

내가 크롤링 하려고 하는 정보는 영화들의 이름/ 이미지 / 장르이다.

 

선택툴

DevTools 좌측상단에 있는 화살표를 누르게 되면 웹 페이지 안에 요소들에 대한 코드로 이동이 가능하다.

저렇게 활성화가 되고 클릭을 하게 되면 다음과 같은  DevTools가 반응하여 코드에 해당위치로 이동하게 된다.

 

 

 

이곳에서 우리가 얻을 수 있는 값은

box-contents 라는 div 안에  title이라는 class에 영화제목이 담겨져 있다는 것과

box-image 라는 div 안에, thumb-image라는 클래스 안에, src속성으로 친절하게 포스터 라고 까지 적혀져 있는 것을

볼 수있다.

 

이것을 기반으로 1차적으로

크롤링을 위한 코드는 다음과 같은데 

package model;

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;

public class Sample {
      public static void sample() {
         
      String cgvUrl = "http://www.cgv.co.kr/movies/?lt=1&ft=0"; // 크롤링하려는 페이지의 url
      Document doc = null; // Document 객체 생성
      URL url = null;   // 이미지를 담아올 URL 객체생성
      InputStream in = null; // InputStream 객체 생성
      OutputStream out = null; // OutputStream 객체 생성
      
      try {
         doc = Jsoup.connect(cgvUrl).get(); // Jsoup 클래스로 url 연결하여 정보를 doc에 담음

      } catch (IOException e) {
         e.printStackTrace();
      }
      String title = "strong.title"; // 영화제목 ( DevTools에서 확인한 경로를 적어준다.)
      Elements eles1 = doc.select(title); // 해당 url에서 영화 제목 정보만 eles1에 담음

      String img = ".thumb-image > img"; // 이미지 ( DevTools에서 확인한 경로를 적어준다.)
      Elements eles2 = doc.select(img); // 해당 url에서 영화 이미지 정보만 eles2에 담음
      
      Iterator<Element> itr1 = eles1.iterator(); // 영화제목 정보를 요소 별로 분리 
      Iterator<Element> itr2 = eles2.iterator(); // 이미지 정보를 요소 별로 분리
      
      int N = 1; // 파일 이름 중복을 피하기 위해 변수를 사용
      int num = 0;
      while(itr3.hasNext()) { // itr3의 정보를 모두 나타낼때까지 반복 (hasNext()==정보가 있니?)
         try {
            CgvVO vo = new CgvVO();
            CgvDAO cDAO = new CgvDAO();            

            String img2 = itr2.next().attr("src"); // 영화 이미지 정보의 속성값(attribute)인 src(이미지 주소)를 담음
            url = new URL(img2); // url 객체에 이미지 주소를 담음, 열림 1
            in = url.openStream(); // in 객체에 url 정보 담음(받고싶은 데이터 연결)
            out = new FileOutputStream("D:\\development\\poster\\"+N+".png"); // out 객체에 저장경로(저장을 원하는 위치) 입력
            N++;
            while(true) {
               int data = in.read(); // in 객체로 해당 이미지를 읽어들임

               if(data==-1) { // 더이상 읽을것이 없으면 멈춤 (앞서 배운 EOF 이다.)
                  break;
               } 
               out.write(data); // 읽어들인 데이터를 경로에 작성
            }
            in.close(); // 저장이 끝난후 사용한 객체는 close, 닫음 1
            out.close();
            
            String title2 = itr1.next().text(); // .text()를 사용하여 태그를 제외한 영화 제목 정보를 담음
            
             vo.setTitle(title2) // 읽어들인 title과
             vo.setImage(img2); // 이미지를 객체에 저장한다.
             } catch (Exception e) {
          	// TODO Auto-generated catch block
            e.printStackTrace();
         } finally {
            try {
               in.close();
               out.close();
            } catch (IOException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
            }            
         }

이 때 사용된

URL 클래스와

InputStream

OutputStream

FileOutputStream

등은 이미지 경로로부터 파일을 int 타입의 data에 in.read하여 파일의 끝인 EOF(-1)이 되면 out.write(data)로 data에

그 정보들을 작성하게 되고 다 사용하게 되면 in.close로 읽기를 닫고, out.close로 쓰기를 종료해주면 된다.

그리고 FileOutputStream(경로\\ 파일명+.확장자)로 저장하면 정해논 경로에 그 확장자로 이미지가 저장되어 있는 것을 볼 수 있다. 

 

실제로 지정해둔 경로에 저장된 이미지파일들

 

 

여기까지 진행한 뒤 마지막으로 장르에 대해서 크롤링을 하려 했는데 첫번 째 난관에 부딪혔다.

CGV 사이트에서 장르를 찾을 수 있는 것은 단 한 곳 밖에 없던 것이다.

그건 바로 무비차트에서 각 영화들을 클릭 했을 때 들어가지는 영화 상세보기에만 장르가 기재되어있었다.

 

영화상세페이지

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

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

 

그렇다면 이러한 주소값들을 우리가 크롤링할 웹 사이트에서 찾을 수 있을까?

하고 다시 무비차트페이지에서 DevTools를 사용해보았다.

찾아보니 각각의 box-image 클래스 안에 있는 href 링크가 무비차트 페이지 안에 내포되어 있었다.

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

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

 

그런데 두번째 난관에 부딪혔다. 그것은 바로

많은 dt..

많은 <dt>태그들 때문에 내가 원하는 장르: 만 빼오기가 곤란해졌다.

그냥 <dt> 를 이터레이터로 가져오게 되면 장르 뿐만 아니라 감독 / 배우/ 장르 / 개봉 등등이 같이 출력

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

 

처음으로 고안한 방법은 substring으로 무비차트 페이지 안에 있는 href midx=? 뒤의 값들을 가져온 뒤, 

indexOf와 lastindexOf 를 사용하여 부분적으로 색출해낸 뒤, if 문으로 장르들을 지정해버리는 것이였다.

   titles.add(itrT.next().text());
         img.add(itrU.next().attr("src"));
         
         String mid = itr.next().attr("href");
         String midx = mid.substring(mid.length()-5).trim(); // midx 값 
         // System.out.println(midx);
         String Cgvurl2="http://www.cgv.co.kr/movies/detail-view/?midx="+midx; // 장르를 담기 위한 새로운 페이지 추출
         datas.add(Cgvurl2);
      }
      try {
         for(int i=0; i<datas.size(); i++) {
            doc1 = Jsoup.connect(datas.get(i)).get();
            String genre1=".spec";
            Elements ele3 = doc1.select(genre1);
            Iterator<Element> itrG = ele3.iterator();
            while(itrG.hasNext()) {
               String genre2 = itrG.next().text();
               int a = genre2.indexOf("장르");
               int b = genre2.lastIndexOf("장르");
               //System.out.println(a);
               //System.out.println(b);

               String genre3 = genre2.substring(a+4,b+7).trim();

               if(genre3.equals("드라")) {
                  genre3="드라마";
               }
               if(genre3.equals("로맨")) {
                  genre3="로맨스";
               }
               if(genre3.equals("애니")) {
                  genre3="애니메이션";
               }
               if(genre3.equals("/")) {
                  genre3="기본";
               }
               System.out.println(genre3);

               genre.add(genre3);
            }
         }

      } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }

 

이렇게 하게 되면, 내가 지정해놓은 장르외에는 크롤링이 안되기 때문에 더 깔끔한 방법이 없을까 고민을 하다가

정제된 데이터를 또 한번 더 정제하는 것을 고안하게 되었는데,

 

순서는 다음과 같다.

1. 무비차트(대분류) 안에 선택한영화정보(소분류)가 있다는 것을 알아냈다.

 

2. 서브스트링을 사용하여 대분류 안에 있는 href에 있는 midx값들을 색출한다.

 

3. 여러 midx가 존재할 텐데 각각의 영화정보들에 대한 url로 다시 지정해준다.

 

4. 그 변하는 url 중에서 dt의 정보 ( 감독 / 배우 / 장르 / 등등 ..) 을 빼온다.

 

5. indexOf를 사용하여 "장르 : " 라는 내용이 포함된 정보만 색출해낸다.

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

 ( indexOf에 대해 찾아보니 배열과 비슷한 성질이라 -1이라는 것은 생성 조차  안되었단 소리라고 이해하면 될 것 같다.)

 

6. 그래서 결국 indexOf("장르 : ") 가 -1이 아니라면 "장르 : " 뒤에 있는 값들을 얻기 위해 서브스트링으로 "장르 : " 를 

잘라내고 출력하게 되면 되는 것이다. 최종적으로 완성한 코드를 올리겠다.

 

package model;

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;

public class Crawlling {
      public static void sample() {
         
      String cgvUrl = "http://www.cgv.co.kr/movies/?lt=1&ft=0"; // 크롤링하려는 페이지의 url
      Document doc = null; // Document 객체 생성
      URL url = null;   // URL 객체생성
      InputStream in = null; // InputStream 객체 생성
      OutputStream out = null; // OutputStream 객체 생성
      
      try {
         doc = Jsoup.connect(cgvUrl).get(); // Jsoup 클래스로 url 연결하여 정보를 doc에 담음

      } catch (IOException e) {
         e.printStackTrace();
      }

      String title = "strong.title"; // 영화제목
      Elements eles1 = doc.select(title); // 해당 url에서 영화 제목 정보만 eles1에 담음

      String img = ".thumb-image > img"; // 이미지
      Elements eles2 = doc.select(img); // 해당 url에서 영화 이미지 정보만 eles2에 담음

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

      Iterator<Element> itr1 = eles1.iterator(); // 영화제목 정보를 요소 별로 분리 
      Iterator<Element> itr2 = eles2.iterator(); // 이미지 정보를 요소 별로 분리
      Iterator<Element> itr3 = eles3.iterator(); // 장르 정보를 요소 별로 분리



      int N = 1; // png 파일에 붙을 숫자
      int num = 0;
      while(itr3.hasNext()) { // itr3의 정보를 모두 나타낼때까지 반복
         try {
            CgvVO vo = new CgvVO();
            CgvDAO cDAO = new CgvDAO();            

            String img2 = itr2.next().attr("src"); // 영화 이미지 정보의 속성값인 src(이미지 주소)를 담음
            url = new URL(img2); // url 객체에 이미지 주소를 담음, 열림 1
            in = url.openStream(); // in 객체에 url 정보 담음(받고싶은 데이터 연결)
            out = new FileOutputStream("D:\\development\\poster\\"+N+".png"); // out 객체에 저장경로(저장을 원하는 위치) 입력
            N++;
            while(true) {
               int data = in.read(); // in 객체로 해당 이미지를 읽어들임

               if(data==-1) { // 더이상 읽을것이 없으면 멈춤
                  break;
               } 
               out.write(data); // 읽어들인 데이터를 경로에 작성
            }
            in.close(); // 저장이 끝난후 사용한 객체는 close, 닫음 1
            out.close();

            String title2 = itr1.next().text(); // 태그를 제외한 영화 제목 정보를 담음
            String category2 = itr3.next().attr("href"); // 장르를 정제하기 위해 속성값 href(상세정보 페이지) 추출
            System.out.println(category2);
            String str = "https://www.cgv.co.kr/movies/detail-view/?"+category2.substring(21,category2.length());
            // 추출한 주소에서 다시 한번 원하는 영화의 midindex 추출 (개별 영화의 상세정보 페이지)

            String cgvUrl2 = str; // 개별 영화의 상세정보 페이지
            Document doc2 = null;
            try {
               doc2 = Jsoup.connect(cgvUrl2).get(); // 상세정보 페이지에 연결하여 정보를 담음

            } catch (IOException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
            }
            String category3 = "div>dl>dt"; // 감독, 배우, 장르 등등
            Elements eles4 = doc2.select(category3); // 영화소개정보(장르포함)만 eles4에 담음 
            Iterator<Element> itr4 = eles4.iterator(); // 영화정보를 요소별로 분리
            while(itr4.hasNext()) { // itr4의 요소를 모두 나타낼때 까지 반복
               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();
                  vo.setGenre(category4.substring(5, category4.length()));;
               }
            }
            if(vo.getGenre()==null) { // 장르가 없으면 영화를 넣지 않음
               continue;
            }
            vo.setTitle(title2); // 객체에 영화제목 입력
            vo.setImage(img2); // 이미지 입력
            vo.setBookcnt(num); // 예매횟수 기본값 설정
            cDAO.insert(vo); // DAO의 insert 함수로 DB에 영화정보 추가
         } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
         } finally {
            try {
               in.close();
               out.close();
            } catch (IOException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
            }            
         }
      }
   }
}

 

 

이것으로 기본적인 크롤링과 정제를 공부하였고,

느낀점을 간략하게 말하자면 CGV 크롤링하기 까다롭다..

첫째로 영화상세보기에만 장르가 존재하는 것도 힘들었고

두번째로 장르나 감독이 없는 영화가 존재하는 것도 어이 없었다..

힘들었던만큼 찾아보며 배운 것이 많아서 도움은 됐지만, 정말 오랜시간 힘들게 완성해서 뿌듯하다.

원래 할 생각 없었던 정제를 하게 되어서 보람차긴 했다..