수업 과제 & 실습/운영체제

멀티스레드 - Web Crawler 구현

ddongyeonn 2020. 5. 23. 22:20

멀티스레드를 두 번 이용하여 40개의 웹페이지를 크롤링하는 프로그램을 만들어보자.

웹 크롤링이란 웹페이지네 데이터를 추출하여 가공하는 행위를 말한다.

JAVA JSOUP Library를 이용하여 1980년 ~ 2019년까지 genie차트에서 원하는 가수의 랭킹을 출력하는 프로그램을 만들어 보았다.

 

1. 멀티스레드를 이용하는 지점

  • 웹 페이지를 크롤링할때
    • 각각의 스레드는 1개의 URL 주소를 할당받고 크롤링하는 작업을 진행한다.
  • 웹 페이지에서 크롤링한 데이터를 이용하여 원하는 검색어를 탐색할 때
    •  각각의 스레드는 1개의 URL 주소를 크롤링한 결과에서 원하는 검색어를 탐색하는 작업을 진행한다.

2. 프로그램 구성 클래스

  • WebCrawling Class
    • WebCrawling Class는 프로그램의 전반적인 진행을 담당한다.
    • 크롤링 결과를 저장해둘 2차원 벡터 result를 static으로 선언하여 가지고 있고 각각의 페이지에서 크롤링하여 가공한 데이터를 SongInfrom형 객체를 원소로 가지는 Vector로 만들어 저장 및 유지한다.
    • 크롤링을 진행하는 CrawlingThread와 탐색을 진행하는 SearchThread를 각각 40개씩 생성하고 진행시키며 동기화한다.
package OSReport1;

import java.io.IOException;
import java.util.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

// 웹 크롤링 실행 관리 
public class WebCrawling {

	public static int searchnum = 0; // 찾은 단어 개수

	public static void main(String[] args) {
		String singer = null;
		String title = null;
		int choice;

		// 스레드 주소를 저장할 배열 선언
		Thread thread[] = new Thread[40];

		while (true) {
			searchnum = 0;
			System.out.println("\n==========  1980년 부터 2019년 까지 지니차트에서 50위 이내에 든 노래 찾기  ==========");
			System.out.println("1. 노래 제목으로 찾기   2. 가수 이름으로 찾기");
			System.out.println("번호 입력: ");

			// 크롤링-----------------------------------------------------------------------------
			// 크롤링 결과를 저장해둘 리스트
			Vector<Vector<SongInform>> result = new Vector<Vector<SongInform>>();

			// 40개의 페이지 크롤링
			for (int i = 0; i < 40; i++) {

				// 각각의 스레드 생성
				Runnable task = new CrawlingThread(i);
				thread[i] = new Thread(task);
				thread[i].start();

				// 각 년도별 크롤링 결과물을 2차원 배열에 저장
				result.add(((CrawlingThread) task).getInform());
			}

			// 다른 스레드 기다리도록 동기화
			for (int i = 0; i < 40; i++) {
				try {
					thread[i].join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

			// 사용자로부터
			// 입력받기--------------------------------------------------------------------
			Scanner scanner = new Scanner(System.in);
			while (true) {
				choice = scanner.nextInt();
				if (choice == 1) {
					System.out.println("노래 제목 입력(2글자 이상)");
					title = scanner.next();
					System.out.println("==============" + title + " 탐색 결과==============");
					break;
				} else if (choice == 2) {
					System.out.println("가수 이름 입력 ");
					singer = scanner.next();
					System.out.println("==============" + singer + " 탐색 결과==============");
					break;
				} else
					System.out.println("입력 오류! 다시 입력하세요.");
				continue;
			}

			// 1980년 ~ 2019년 음악차트
			// 탐색하기---------------------------------------------------------
			for (int i = 0; i < 40; i++) {
				Runnable task;
				// 각각의 스레드 생성
				if (choice == 1)
					task = new SearchThread(result.get(i), title, choice);
				else
					task = new SearchThread(result.get(i), singer, choice);

				thread[i] = new Thread(task);
				thread[i].start();
			}

			// 다른 스레드 기다리도록 동기화
			for (int i = 0; i < 40; i++) {
				try {
					thread[i].join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

			// 결과 출력
			if (searchnum == 0) {
				System.out.println("입력이 잘못되었거나 검색 결과가 차트에 없습니다.");
				System.out.println("=================================");
			} else {
				if (choice == 1)
					System.out.println(title + "는 총 " + searchnum + "회 검색되었습니다.");
				else
					System.out.println(singer + "는 총 " + searchnum + "회 검색되었습니다.");
				System.out.println("==========================================");
			}

			// 프로그램을 계속 진행할지 확인
			System.out.println("\n계속 진행하시겠습니까? [Y/N]");
			String step = scanner.next();
			if (step.equals("Y") || step.equals("y"))
				continue;
			else {
				System.out.println("프로그램 종료");
				break;
			}
		}
	}
}

 

  • CrawlingThread Class
    • Crawling을 진행하는 Thread 자체를 나타내는 Class이다.
    • CrawlingThread의 Run() 메소드 내부에 1개의 페이지를 Crawlilng 하는 소스코드가 포함된다.
    • 총 40개의 CrawlingThread가 생성되어 40개의 웹페이지가 크롤링된다.
package OSReport1;

import java.io.IOException;
import java.util.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

//Document 클래스 : 연결해서 얻어온 HTML 전체 문서
//Element 클래스 : Document의 HTML 요소
//Elements 클래스 : Element가 모인 자료형
//select(자료형.클래스이름)

public class CrawlingThread implements Runnable {

	private String url = "https://www.genie.co.kr/chart/musicHistory?year=2019&category=0"; // 크롤링할 URL
	private int threadnum = 0; // 스레드 번호
	private int year = 0; // 스레드가 크롤링하는 년도
	private Vector<SongInform> inform = new Vector<SongInform>(); // 0위 ~ 50위 곡정보 저장할 벡터

	// 생성자
	public CrawlingThread(int num) {
		threadnum = num; // 스레드 번호 지정
		// 연도별 url 설정
		year = 2019 - threadnum;
		String tmp = Integer.toString(year);
		url = url.replace("2019", tmp); // url 중간 연도부분을 해당 연도로 바꿈
	}

	public void run() {

		Document doc = null; // Document에는 페이지의 전체 소스가 저장된다

		try {
			// connect를 통해 url 접속 -> get을 통해 페이지 소스를 얻어옴
			doc = Jsoup.connect(url).get();
		} catch (IOException e) {
			System.out.println(e.getMessage()); // 예외가 발생한 위치와 호출된 메소드 정보 출력
		}

		// select를 이용하여 원하는 태그를 선택한다.
		Elements element = doc.select("table.list-wrap");
		// element로부터 원하는 데이터를 저장한다.
		Iterator<Element> title = element.select("a.title").iterator();
		Iterator<Element> singer = element.select("a.artist").iterator();
		Iterator<Element> rank = element.select("td.number").iterator();

		// 추출한 정보를 모아 객체를 생성한다
		while (rank.hasNext()) {
			SongInform tmp = new SongInform(year, rank.next().text(), title.next().text(), singer.next().text(), url);
			// 가수이름 문자열 공백제거
			tmp.singer = tmp.singer.trim();
			tmp.singer = tmp.singer.replace(" ", "");
			inform.add(tmp);
		}
	}

	public Vector<SongInform> getInform() {
		return inform;
	}
}

 

  • SearchThread Class
    • Crawling결과를 저장해둔 SongInfrom형 벡터를 탐색하는 Class이다.
    • SearchThread의 Run() 메소드 내부에 1개의 페이지에 대한 Crawling 결과를 탐색하는 소스코드가 포함된다.
    • 총 40개의 SearchThread가 생성되어 40개의 SongInfrom형 벡터를 탐색하고 검색어와 일치하는 정보가 있으면 곡의 정보를 출력한다.
package OSReport1;

import java.util.*;

public class SearchThread implements Runnable {

	int choice; // 1 - title로 탐색 2 - 가수명으로 탐색
	String inform;
	Vector<SongInform> songlist; // 해당 년도 곡들을 저장한 리스트

	SearchThread(Vector<SongInform> songlist, String inform, int choice) {
		this.songlist = songlist;
		this.inform = inform;
		this.choice = choice;
	}

	public void run() {
		// 탐색하기
		for (int i = 0; i < songlist.size(); i++) {
			// 곡명으로 검색
			if (choice == 1 && songlist.get(i).title.contains(inform)) {
				int year = songlist.get(i).year;
				String singer = songlist.get(i).singer;
				String rank = songlist.get(i).rank;
				String title = songlist.get(i).title;
				WebCrawling.searchnum++;
				// 출력
				System.out.println(title + "(은)는   " + year + "년에  " + rank + "위를  한 " + singer + "의 노래입니다.\n"
						+ "검색어를 찾은 url: " + songlist.get(i).url + "\n");
			}
			// 가수명으로 검색
			else if (choice == 2 && songlist.get(i).singer.equalsIgnoreCase(inform)) {
				int year = songlist.get(i).year;
				String rank = songlist.get(i).rank;
				String title = songlist.get(i).title;
				WebCrawling.searchnum++;
				// 출력
				System.out.println(inform + "(은)는   " + year + "년에  " + title + "(으)로  " + rank + "위를  했습니다.\n"
						+ "검색어를 찾은 url: " + songlist.get(i).url + "\n");
			}
		}
	}
}

 

  • SongInfrom Class
    • 크롤링한 결과를 가공하여 저장해두기 위해 생성한 Class이다.
    • SongInfrom Class에는 인스턴스로 URL, 음원 발매 연도, 가수 이름, 노래 제목, 랭킹을 포함하고 있다.
package OSReport1;

// 노래 정보를 담은 클래스
public class SongInform {
	int year; // 노래가 출시된 년도
	String rank, title, singer, url; // 노래의 순위, 제목, 가수, 찾은 url 정보

	// 생성자
	SongInform(int year, String rank, String title, String singer, String url) {
		this.year = year;
		this.rank = rank;
		this.title = title;
		this.singer = singer;
		this.url = url;
	}
}

 

 

3. 결과화면

 

4. 첨부파일 (소스코드, 실행파일, 보고서)

WebCrawling.zip
0.78MB