본문 바로가기

수업 정리

35일차 수업정리(HTML Parsing, selenium)

**HTML Parsing

    => 웹사이트에 데이터가 출력은 되지만 OpenAPI 형태로 데이터는 제공하지 않는 경우 이 데이터를 사용하고자 하면 HTML을 가져와서 필요한 데이터만 추출 해야 하는데 이 것을 HTML Parsing이라고 함

        - 최근에는 Web Crawling 이라고 하는 경우가 많음

    => 사용 라이브러리 : Jsoup(Python의 BeautifulSoup 라이브러리와 유사)

 

1. Jsoup를 이용한 파싱

    => Jsoup.parse(html 문자열) : 문자열을 트리형태로 메모리에 펼치고, 메소드를 이용하여 원하는 Dom을 찾도록해줌

  1) Dom을 찾기 위한 속성

    => tag : HTML 문서의 구조를 나타내기 위한 명령어로 중복될수 있음

    => id : 하나의 Dom을 구분하기 위해 태그에 붙인 식별자(중복될수 없음)

    => class : 여러 Dom에 동일한 디자인을 적용하기 위한 이름으로 중복될 수 있음

    => name : 서버에게 데이터를 전달 할 때 서버에서 인식할 이름으로 중복될수 있음

 

    => selector : Dom을 다양한 방법으로 선택하기 위한 문법으로 중복될수 있음

        - jQuery와 같은 javascript 라이브러리를 학습할 때 중요

 

    => xpath : XML에서 각각의 Dom을 찾아가기 위한 언어로 중복될수 없음

        - Macro와 같은 웹페이지 자동화를 위한 응용 프로그램 작성에서 중요

 

     2) Dom을 찾기 위한 메소드

    => getElementById(String id)

    => getElementByTagName(String tag)

    => getElementByClass(String class)

    => select(String selector)

 

위의 메소드를 호출 시 Element나 Element의 List인 Elements를 리턴

  3) Element의 메소드

    => text() : 태그 안의 문자열 리턴

    => attr(String attribute) : 태그 안의 attribute의 값을 문자열로 리턴

 

2. http://www.hani.co.kr에서 서 메인기사의 내용 가져오기 

  1) HTML Parsing을 위한 라이브러리의 의존성을 설정

    => pom.xml 파일의 dependencies 태그 안에 추가

 

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.11.3</version>
</dependency>

 

  2) 데이터를 다운로드 받는 코드를 main 메소드에 추가

String html = null;
		
try {
	//1. 주소 만들기
	//파라미터에 한글이 있으면 파라미터를 인코딩
	//파라미터 : ?다음에 나오는 문자열
	String addr = "http://www.hani.co.kr/";
	URL url = new URL(addr);
			
	//2. 연결 객체 만들기
	//header에 추가하는 옵션이 있는지 확인
	//header가 있는 경우는 api key나 id나 비밀번호를 설정해야 하는 경우
	HttpURLConnection con = (HttpURLConnection)url.openConnection();
	con.setConnectTimeout(30000);
	//캐시에 저장된 값을 가져옴
	con.setUseCaches(true);
			
	//헤더 설정
		
	//3. 스트림을 만들어서 문자열 읽어오기
	//읽었는데 한글이 깨지면 InputStreamReader 생성자에 euc-kr 추가
	BufferedReader br = new BufferedReader(
			new InputStreamReader(con.getInputStream()));
		
	//문자열을 임시로 저장할 인스턴스
	StringBuilder sb = new StringBuilder();
			
	//줄단위로 읽어서 sb에 저장
	while(true) {
		String line = br.readLine();
		if(line == null)
			break;
		sb.append(line + "\n");
	}
	html = sb.toString();
			
	//4. 정리하기
	br.close();
	con.disconnect();
			
}catch (Exception e) {
	System.err.println("다운로드 실패");
	System.out.println(e.getMessage());
	e.printStackTrace();
}
//데이터 확인
//System.out.println(html);
		
if(html != null && html.trim().length() > 0) {
	//문서 구조 가져오기
	Document document = Jsoup.parse(html);
			
	//선택자 이용하여 가져오기
	Elements elements = document.select(
			"#main-top03-scroll-in > div.photo_area");
	for(int i=0; i<elements.size(); i+=1) {
		Element element = elements.get(i);
		System.out.println(element.text());
	}
	System.out.println("??");
}else {
	System.out.println("읽어온 데이터가 없음");
}

 

3. 메인 기사의 링크 가져오기

    => HTML에서 링크의 구조

        - <a href="링크">텍스트나 이미지</a>

    => 게시판(신문)이나 SNS등에서 검색을 하게되면 제목과 링크가 나옴

        - 이 경우 링크를 따라가서 실제 데이터를 가져와야 함

        - 

    => 속성의 값을 가져올 때는 text() 대신에 get(String attribute) 사용

        - System.out.println(element.attr("href"));

 

4. SNS나 게시판에서 기사 가져오기

    => URL의 구조를 잘 파악해야 함

    => 링크를 따라가서 기사나 본문의 내용을 가져와야 함

    => 첫번째 검색의 결과에서 데이터의 갯수를  잘 추출해야 함

        - 언제까지 수행해서 데이터를 가져올 것이지 결정

 

5. www.donga.com에서 최성권으로 검색한 기사들의 내용을 파일에 저장

    => URL 패턴을 확인

    => 처음에는 데이터 개수를 찾아야 함

    => 데이터 개수를 알면 반복문을 이용하여 검색된 모든 결과에서 데이터를 찾아와야 함

  1) 패턴을 확인

    => 검색어는 query에 대입

    => 페이지가 변경되면 p 파라미터가 15씩 변경(page1 -> p=1, page2 -> p=16, page3 -> p=31)

    => 전체 데이터 개수를 이용해서 페이지 개수를 찾아야 함

        - 전체 데이터 개수가 15이면 1page, 16이면 2page, 30이면 2page

        - 페이지 개수는 전체 데이터 / 페이지당 데이터 개수를 구한 후 %를 해서 나머지가 있으면 +1

        //계산할 수 있도록 숙지

        - int pagesu = (int)((전체데이터개수 / 15) + ((double)(15-1)/15))  => page 구분 기준 = 15

        - int pagesu = 전체데이터개수 / 15

          if(전체데이터 개수 % 15!=0){ pagesu += 1; }

    => 페이지 개수를 찾으면 페이지 번호를 어떻게 대입해야 하는가 계산

    

    => MySQL 같은 곳에서 원하는 범위의 데이터를 가져올 때 활용

        ex) 페이지 번호 : 1, 2, 3... , p : 1, 16, 31...   : 15*페이지번호 -14

 

  2) 첫번째 페이지의 데이터를 읽어서 검색 건수 찾아오기

//데이터 건수를 저장 할 변수
int cnt = -1;
try {
	//텍스트를 메모리에 펼치기
	Document document = Jsoup.parse(html);
	Elements elements = document.select(
			"#content > div.searchContWrap > div.searchCont > h2 > span:nth-child(1)");
	for(int i=0; i<elements.size(); i+=1) {
		Element element = elements.get(i);
		String content = element.text();
		//System.out.println(content);
				
		//기사 건수만 찾아오기 
		//공백을 기준으로 분할
		String[] ar = content.split(" ");
		cnt = Integer.parseInt(ar[1]);
		//System.out.println(cnt);
	}
			
}catch (Exception e) {
	System.err.println("데이터 건수 저장 실패");
	System.out.println(e.getMessage());
	e.printStackTrace();
}

//페이지당 데이터 개수
int perPageCnt = 15;
		
//페이지당 개수 계산
//전체 데이터 개수를 페이지당 데이터 개수로 나누고 나머지가 있으면 페이지 개수를 1개 추가
int pageCnt = cnt / perPageCnt;
if(cnt % perPageCnt !=0) {
	pageCnt += 1;
}
//System.out.println(pageCnt);

 

  3) 기사의 링크(a 태그의 )만 전부 수집

//기사의 링크를 저장할 변수
List<String> list = new ArrayList<>();
	
try {
	//반복문 안에서 예외가 발생했을 때 다음 반복으로 넘어가고자 하면 
	//반복문 내부에서 예외처리
	for(int i=0; i<pageCnt; i+=1) {
		try {
			String query = "최성권";
			//파라미터 인코딩
			query = URLEncoder.encode(query, "utf-8");
			String addr = "https://www.donga.com/news/search?p="
					+ ((i*perPageCnt)+1) + "&query=" + query
					+ "&check_news=1&more=1&sorting=1&search_date=1&v1=&v2=&range=1";
			URL url = new URL(addr);
					
			HttpURLConnection con = (HttpURLConnection)url.openConnection();
			con.setUseCaches(false);
			con.setConnectTimeout(10000);
					
			BufferedReader br = new BufferedReader(
					new InputStreamReader(con.getInputStream()));
			StringBuilder sb = new StringBuilder();
					
			while (true) {
				String line = br.readLine();
				if (line == null)
					break;
				sb.append(line + "\n");
			}
			html = sb.toString();

			br.close();
			con.disconnect();
			
			//다운로드 되었는지 한글은 깨지지 않는지 확인
			//System.out.println(html);
					
			//링크 수집을 위해서 html 파싱
			Document doc = Jsoup.parse(html);
			Elements elements = doc.select(
					"div.t > p.txt > a");
					
			for(int j=0; j<elements.size(); j +=1) {
				Element element = elements.get(j);
				//a태그의 href 속성을 list에 저장
				list.add(element.attr("href"));
			}
			System.out.println(list);
		}catch (Exception e) {
			System.err.println("현재 페이지 읽어오기 실패");
			System.out.println(e.getMessage());
			e.printStackTrace();
		}
	}
}catch (Exception e) {
	System.err.println("기사 링크 저장 실패");
	System.out.println(e.getMessage());
	e.printStackTrace();
}

 

  4) 기사 링크를 전부 순회하면서 기사 내용을 파일에 저장

//현재 디렉토리에 최성권.txt 파일을 만들고, 기사 내용 저장
//try() 안에 만든 객체는 close를 호출할 필요X
try(PrintWriter pw = new PrintWriter("./최성권.txt")) {
	for(String link : list) {
		try {
			URL url = new URL(link);
					
			HttpURLConnection con = (HttpURLConnection) url.openConnection();
			con.setConnectTimeout(30000);
			con.setUseCaches(true);

			BufferedReader br = new BufferedReader(
					new InputStreamReader(con.getInputStream()));

			StringBuilder sb = new StringBuilder();

			// 줄단위로 읽어서 sb에 저장
			while (true) {
				String line = br.readLine();
				if (line == null)
					break;
				sb.append(line + "\n");
			}
			html = sb.toString();

			br.close();
			con.disconnect();
			//System.out.println(html);
					
			Document document = Jsoup.parse(html);
			Elements elements = document.select("#content > div > div.article_txt");
			for(int k = 0; k<elements.size(); k+=1) {
				Element element = elements.get(k);
				pw.println(element.text());
				pw.flush();
			}
					
			//pw.println(html);
		}catch (Exception e) {
			System.err.println("기사 가져오기 실패");
			System.out.println(e.getMessage());
			e.printStackTrace();
		}
	}
}catch (Exception e) {
	System.err.println("기사 저장 실패");
	System.out.println(e.getMessage());
	e.printStackTrace();
}

 

**Selenum

    => Selenium은 웹 앱을 테스트하는데 이용하는 프레임워크

    => Webdriver라는 API를 통해 운영체제에 설치된 브라우저를 제어

1. Web 문서를 스크래핑 할 때 스크래핑이 안되는 데이터

    => ajax(Asynchronous JAvascript Xml-비동기적으로 자바스크립트를 이용해서 가져오는 XML)을 통해 가져온 데이터

    => 로그인 해야 접속이 가능한 페이지

    => 자바스크립트의 이벤트를 이용해서 가져오는 데이터

 

 2. 1번과 같은 데이터들은 브라우저를 직접 구동 시켜서 동작을 수행하도록 해서 가져올 수 있음

    => 이러한 작업을 수행할 수 있도록 만들어진 라이브러리가 selenium

 

3. 준비

    => 제어하고자하는 웹 브라우저의 드라이버 : 자신의 브라우저 버전과 드라이버 버전이 맞아야 함

    => selenium을 자바에서 구동하기 위한 라이브러리

  1) 브라우저 드라이버를 다운로드

    => 구글 드라이버 검색(자신의 버전확인 후 동일한 버전 드라이버 설치) 

  2) selenium 드라이버 다운로드

    => stable : 안정화 버전  /  debug : 테스트하면서 실행(테스트판)  /  release : 실행(배포판)

<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.0.0-alpha-5</version>
</dependency>

 

4. 브라우저를 실행시켜서 사이트 접속과 html 가져오기

    => WebDriver 드라이버 = new 드라이버Driver();

         드라이버.get(String url);  //url 접속

         드라이버.getPageSource()  //html 리턴

    => 위의 코드를 실행하기 전에 브라우저 설정

        - System.setProperty(String 브라우저 드라이버명, string 드라이버 경로);

  1) pom.xml 파일에 selenium 라이브러리를 사용하기 위한 의존성을 설정

 

  2) main메소드에 작성

try {
	//크롬을 사용하기 위한 환경 설정
	System.setProperty("webdriver.chrome.driver", "./chromedriver.exe");
			
	//크롬 실행 객체 만들기
	WebDriver driver = new ChromeDriver();
			
	//페이지 접속
	driver.get("https://www.naver.com");
}catch (Exception e) {
	System.err.println("크롬 실행 실패");
	System.out.println(e.getMessage());
	e.printStackTrace();
}

  3) 크롬의 경우 실행시 최신버전으로 자동 업데이트하므로 드라이버를 최신버전으로 변경해야 두번째부터 실행 가능

 

5. WebDriver 클래스의 메소드

    => get(String url) : url에 접속

    => String getPageSource() : html을 리턴

    => close() : 브라우저 종료

 

    => element를 찾아오는 메소드

        - WebElement findElement(By.Id(String id) 또는 By.xpath(String xpath))

        - WebElement findElements(By.tagName(String tag) 또는 By.class(String class))

    => 크롬을 실행시키지 않고 작업을 수행

        - ChromeOptions 옵션 = new ChromeOptions();

          옵션.addArguments("headless");

          WebDriver 드라이버 = new ChromeDriver(옵션);

 

    => WebElement 에 데이터를 입력하거나 클릭한 효과

        - send_keys(String data) : 입력도구인 경우 data가 입력

        - click() : 버튼이나 a 태그인 경우 클릭한 효과가 나타남

 

6. 다음의 자동로그인

    => 다음의 로그인 페이지로 이동

    => 페이지가 frame으로 구성된 경우 frame의 데이터에는 직접 접근이 안됨

        - frame : 하나의 문서를 여러개의 문서로 만들기 위한 html 태그

        - 프레임 안에 있는 요소는 프레임으로 이동한 후 찾아야 함

try {
	//브라우저 실행
	System.setProperty("webdriver.chrome.driver", "./chromedriver.exe");
	WebDriver driver = new ChromeDriver();
			
	//다음 로그인 페이지 접속
	driver.get("https://logins.daum.net/accounts/signinform.do?url=https%"
			+ "3A%2F%2Fwww.daum.net%2F");
			
	//아이디 입력란을 찾기
	WebElement id = driver.findElement(By.xpath("//*[@id=\"id\"]"));
	id.sendKeys("gs90826");
			
	WebElement pw = driver.findElement(By.xpath("//*[@id=\"inputPwd\"]"));
	pw.sendKeys("qkftk.001!");
			
	//로그인 버튼 클릭
	WebElement login = driver.findElement(By.xpath("//*[@id=\"loginBtn\"]"));
	login.click();
			
	//페이지 이동이 많은 경우 과부하 방지를 위해 중간에 sleep 추가
	Thread.sleep(3000);
			
	//카페로 이동
	driver.get("http://cafe.daum.net/samhak7");
			
	//프레임으로 이동
	driver.switchTo().frame("down");
			
	//글을 입력 
	WebElement memo = driver.findElement(By.xpath(
			"//*[@id=\"memoForm__memo\"]/div/table/tbody/tr[1]/td[1]/div/textarea"));
	memo.sendKeys("댓");
			
	//등록버튼 클릭 
	WebElement write = driver.findElement(By.xpath(
            "//*[@id=\"memoForm__memo\"]/div/table/tbody/tr[1]/td[2]/a[1]/span[1]"));
	write.click();
			
}catch (Exception e) {
	System.err.println("다음 카페 접속 실패");
	System.out.println(e.getMessage());
	e.printStackTrace();
}

 

Tip!

1. 자신이 만들 프로그램과 유사한 프로그램을 살펴보고 왜 이렇게 했을지에 대해 생각해보기

    - 비지니스 인사이트를 염두에 두고 프로그램 제작

    - 구글 애널리틱스 자세히 살펴보기