이번 포스팅은 word cloud 를 만들기 위한 전체적인 코드와 찾아봤던 정보들을 정리하기 위한 글을 작성하도록 하겠다.
모든 코드는 git 에 올려두었고, 여기에는 크롤링 -> 데이터 파싱 -> Controller -> html 순으로 정리하도록 하겠다.
1. 크롤링 crewler
- 크롤링을 위한 코드는 네이버 블로그 검색 API 와 아래 블로그의 Steele 님의 코드를 약간 수정해서 사용하였다
(주소는 아래 참조에 달아두었습니다)
- 나름대로 로직을 이해하기 위해 주석을 달아서 정리하였으나, 보다 자세한 설명은 Steele 님의 글을 보는게 훨~~씬 도움이 되리라 생각한다.
package HJproject.DataMining;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
public class NaverCrawler implements APIdata{ // 베이스 URL
final String baseUrl = "https://openapi.naver.com/v1/search/blog.json?query=";
public String Navercrawler(String word){
String crawerString = null;
try {
// 매개변수 : 검색단어, 인코딩
String url = URLEncoder.encode(word, "UTF-8");
// crawler 의 search 메소드 사용
// 이때 naverID 와 naverSecret 은 APIdata 안에 있는 내용 사용
String response = search(naverID, naverSecret, url);
// 필드값은 title 가 desc 2개!
// 크롤링을 하게되면 field 가 여러개가 나오는데 이 중에서 title 와 desc 만 가져온다는 의미
String[] fields = {"title","description"};
// 결과를 Map 형태로 저장장
Map<String, Object> result = getResult(response, fields);
// 검색 결과가 1개 이상인 경우 result 값을 출력
if (result.size() > 0) {
System.out.println("total -> " + result.get("total"));
}
// System.out.println("result : "+result);
// 검색 결과를 다시 List 형태로 저장
List<Map<String, Object>> items = (List<Map<String, Object>>) result.get("result");
//
for (Map<String, Object> item : items) {
crawerString += item.get("title");
crawerString += item.get("description");
}
return crawerString;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// 여기는 네이버 검색 API
public String search(String clientId, String secret, String _url) {
HttpURLConnection con = null;
String result = "";
try {
URL url = new URL(baseUrl + _url +"&display=50");
con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("X-Naver-Client-Id", clientId);
con.setRequestProperty("X-Naver-Client-Secret", secret);
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) result = readBody(con.getInputStream());
else result = readBody(con.getErrorStream());
} catch (Exception e) {
System.out.println("연결 오류 : " + e);
} finally {
con.disconnect();
}
return result;
}
public String readBody(InputStream body) {
InputStreamReader streamReader = new InputStreamReader(body);
try (BufferedReader lineReader = new BufferedReader(streamReader)) {
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = lineReader.readLine()) != null) {
responseBody.append(line);
}
return responseBody.toString();
} catch (IOException e) {
throw new RuntimeException("API 응답을 읽는데 실패했습니다.", e);
}
}
public Map<String, Object> getResult(String response, String[] fields) {
Map<String, Object> rtnObj = new HashMap<>();
try {
JSONParser parser = new JSONParser();
JSONObject result = (JSONObject) parser.parse(response);
rtnObj.put("total", (long) result.get("total"));
JSONArray items = (JSONArray) result.get("items");
List<Map<String, Object>> itemList = new ArrayList<>();
for (int i = 0; i < items.size(); i++) {
JSONObject item = (JSONObject) items.get(i);
Map<String, Object> itemMap = new HashMap<>();
for (String field : fields) {
itemMap.put(field, item.get(field));
}
itemList.add(itemMap);
}
rtnObj.put("result", itemList);
} catch (Exception e) {
System.out.println("getResult error -> " + "파싱 실패, " + e.getMessage());
}
return rtnObj;
}
}
2. 파싱 Parsing
- 데이터를 파싱하기 위한 코드로 userDic 를 가져온 뒤 설정해두고, Komoran 객체를 생성한다. 그 후 Navercrawler.crawler 메소드 결과를 dataString 에 저장한 후 komoran 객체의 메서드를 사용해 전체 테이터를 파싱한다.
- 이때 komoran 의 getMorphesByTags 메소드를 사용하는데 "NNP", "NNG", "NNB", "NP" 등은 komoran 에서 제공하는 품사표를 활용한 것이다. 이후 메소드의 결과는 String 타입의 List 인 analyzeList 로 저장한다.
- 여기서부터는 아주 중요하다!! analyzeList 의 결과에 대해서 특정 단어가 몇번이나 중복되는지 확인해서 저장해야 하기 때문이다. 따라서 List 로 된 결과를 HashMap 으로 바꿔서 중복된 '단어' 는 삭제하고, 대신 몇번이나 중복되었는지 횟수를 기록한다. 즉 HashMap 에서 key : 단어, value : 중복 횟수 형태로 저장해야 하는 것이다.
- 말이 어렵지 코드로하면 사실 간단한데, 중복 횟수를 확인하는 것은 Collection 의 frequency 메소드를 사용하면 된다. 즉 List 의 단어를 하나하나 꺼내온 뒤 Collections.frequency(전체 List, 값) 을 넣어주면 해당 '값' 이 전체 List 에서 몇번이나 반복되었는지를 int 타입으로 반환한다.
package HJproject.DataMining;
import kr.co.shineware.nlp.komoran.constant.DEFAULT_MODEL;
import kr.co.shineware.nlp.komoran.core.Komoran;
import kr.co.shineware.nlp.komoran.model.KomoranResult;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import java.util.*;
public class NaverParsing implements APIdata {
public HashMap<String, Integer> parsingData(String word) {
// System.getProperty 를 사용해서 파일이 실행되는 현재 위치 가져오기
// iDE 환경에서는 워크스페이스 경로를 가져오고, jar 파일의 경우 jar 파일 실행 경로를 가져옴
String path = System.getProperty("user.dir");
// System.out.println(path);
// Komoran 사용을 위한 초기화 && 선언
Komoran komoran = new Komoran(DEFAULT_MODEL.FULL);
// user Dictionary 사용을 위한 위치 경로 정의
komoran.setUserDic(path + "/userDictionary/koreanDic.user");
// NaverCrawler 클래스의 crawlerData 메소드를 사용해서 크롤링한 STring 을 얻어오기
String dataString = new NaverCrawler().crawler(word);
// 가져온 String을 komoran analyze 메소드에 넣기
KomoranResult komoranResult = komoran.analyze(dataString);
// 여기서 getMorphesByTags 사용하면 내가원하는 형태소만 뽑아낼 수 있음
List<String> analyzeList = komoranResult.getMorphesByTags("NNP", "NNG", "NNB", "NP");
// list 파일로 떨어진 analyzeList 를 HashMap 에 넣어서 중복된 데이터를 삭제하고
// Conllections.frequency 를 사용해서 몇 번이나 중복되었는지 분석하여 저장한다.
// 최종적으로 listHash 에는 단어=중복횟수 로 저장된다.
// Collections.frequency(Collections객체, 값)
HashMap<String, Integer> listHash = new HashMap<>();
for (String l : analyzeList) {
int num = Collections.frequency(analyzeList, l);
listHash.put(l, num);
}
// 데이터 정렬을 위한 코드
// List<Map.Entry<String, Integer>> list_entries = new ArrayList<Map.Entry<String, Integer>>(listHash.entrySet());
// Collections.sort(list_entries, new Comparator<Map.Entry<String, Integer>>() {
// @Override
// public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
// return o2.getValue().compareTo(o1.getValue());
// }
// });
// for(Map.Entry<String, Integer> l : list_entries){
// System.out.println(l);
// }
return listHash;
}
// 여기서부터는 단순 출력 확인용 코드입니다 안써도 무방해용
// public static void main(String[] args) {
//
// String path = System.getProperty("user.dir");
//// System.out.println(path);
//
// Komoran komoran = new Komoran(DEFAULT_MODEL.FULL);
// komoran.setUserDic(path+"/userDictionary/koreanDic.user");
//
//
// HashMap<String, Integer> crawlerData = new NaverParsing().parsingData("엘든링");
// JSONArray jsonArray = new JSONArray();
//
// for(String list : crawlerData.keySet()){
// System.out.println("key : "+list +"\t"+"value : "+crawlerData.get(list));
// JSONObject informationObject = new JSONObject();
// informationObject.put("text", list);
// informationObject.put("weight", crawlerData.get(list));
//
// jsonArray.add(informationObject);
// }
//
//
//
//
// System.out.println(jsonArray.toJSONString());
//
// }
}
3. DataController
- DataController 클래스는 전체적인 http 요청을 처리하기 위한 controller 클래스이다. MVC 에서 그 C 맞다.
- home 메소드는 말 그대로 home 으로 보내는 메소드이고, sendData 가 메인인 클래스이다.
- sendData 는 wordcloud 에서 단어를 가져오고 해당 단어로 크롤링하고 파싱을 하고 결과를 hashMap 로 반환한다. 이때 wordcloud 에서 받아오는 word 는 ajax 스타일로 가져온다.
- 이후 html 로 JSON 데이터를 보내는데 JSON 형태로 데이터를 보내기 위해 JSONArray 와 JSONObject 를 각각 선언하여, HashMap 으로부터 key 를 꺼내와서 {x : HashMap 의 key 값, value : HashMap의 key에 대한 value 값} 형태의 json 으로 저장한다. 이후 해당 json 객체를 다시 jsonArray 에 저장한다.
- x, value 인 이유는 anychart 가 이러한 x 와 value 형태를 사용하기 때문이다.
- [{"x":"저","value":7},{"x":"적","value":2},{"x":"다운로드","value":1}] 형태로 저장된다.
- 마지막으로 json 객체가 넣어진 jsonArray 를 wordcloud 에 ajax 스타일로 전달한다.
package HJproject.Hellospring.Controller;
import HJproject.DataMining.NaverParsing;
import lombok.RequiredArgsConstructor;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
@Controller
@RequiredArgsConstructor
public class DataController {
@GetMapping("/")
public String home(){
return "thymeleaf/dataplay/wordcloud";
}
@RequestMapping(value = "/getData", method = RequestMethod.GET)
public void sendData(HttpServletResponse res, HttpServletRequest req) throws IOException {
// 웹에서 get으로 요청할때 보내온 파라미터 중 word 파라미터를 가져옴
String word = req.getParameter("word");
// NaverParsing 클래스의 parsingData 를 실행하고 겨과를 HashMap 로 저장함
// 이때 파라미터로 웽에서 가져온 word 를 사용
HashMap<String, Integer> crawlerData = new NaverParsing().parsingData(word);
// 데이터 저장을 위한 json array
JSONArray jsonArray = new JSONArray();
for(String list : crawlerData.keySet()){
JSONObject informationObject = new JSONObject();
// JsonArray 에 저장하기 위해서 값을 하나씩 json 형태로 가져와서 x 에 key를 담고 , value 에는 value을 저장함
// 이때 anychart 의 경우 x 와 value 를 사용하지만
// JQcloud 의 경우 text 와 weight 를 사용한다.
// 이후 다시 array 에 담기
informationObject.put("x", list);
informationObject.put("value", crawlerData.get(list));
jsonArray.add(informationObject);
}
// 전달하는 값의 타입을 application/json;charset=utf-8 로 하고(한글을 정상적으로 출력하기 위함)
// printwriter 과 prrint 를 사용하여 값을 response 로 값을 전달함
// 이때 toJSONString 로 전당하는데 이는 추후 Jsonparsing 을 원활하게 하기 위해서
// pw 로 값을 전달하면 값이 response body 에 들어가서 보내짐
res.setContentType("application/json;charset=utf-8");
PrintWriter pw = res.getWriter();
pw.print(jsonArray.toJSONString());
//System.out.println(jsonArray.toJSONString());
}
}
4. WordCloud
- 오늘의, 이번 글의 메인!! word Cloud html 입니다.
- 전체적인 코드는 비교적 간단합니다. css, script, html 모두 고루고루 섞여 있습니다.
- wordcloud 를 만들기 위해서 사용하는 JS 라이브러리는 anychart, JQcloud 로 크게 2가지 입니다. 저는 이 중에서 anychart 를 사용해서 만들도록 하겠습니다. 기능도 그렇고, anychart 가 더 이뻐요ㅠ
- 이제 중요한 JS 와 ajax 부분에 대한 설명을 하겠습니다.
- 먼저 사이트 로딩시에는 #container 을 숨겨서 보이지 않도록 만듭니다. 처음부터 보이면 되게 이상해요ㅠ
- 다음으로 버튼을 검색 버튼을 클릭하면 search 함수가 실행되도록 합니다. search 함수는 본인이 입력한 단어 - word - 를 가져와서 ajax 로 서버에 보내줍니다.
- 가장 중요한 부분은 ajax 결과에 따른 함수 실행입니다. 서버에서 돌아오는 결과에 따라서 success 와 error 로 나눠집니다. success 는 서버에서 정상적인 결과 - json - 이 return 되었을 때 실행되고, error 은 말 그대로 ajax 로 보냈던 값이 뭔가 에러가 생겨서 서버에서 제대로된 값을 전달해주지 못할 경우 실행됩니다.
- 이때 서버에서 보내주는 결과란 success 와 error function 의 매개변수인 result 입니다. 즉 서버에서 보내주는 json 은 result 에 담아진다고 생각하면 됩니다.
- 이외 주의 깊게 보아야하는 부분은 flag 입니다. 이 부분은 검색이 2번 이상 되는 경우 기존에 검색되었던 내용은 삭제하고, 새로운 단어에 대한 검색 결과만 보여줄 수 있도록 합니다.
워드 클라우드 표현 방법!!!
워드 클라우드의 표현은 의외로 간단합니다. 바로 반복 횟수가 곧 글자의 크기가 되는 방식입니다.
예를 들어 {엘든링 : 52}, {PS5:40} 이라는 json 이 넘어왔다면 엘든링은 50px 의 글자 크기가 되고, PS5 는 40px 의 글자 크기가 됩니다. 따라서 굳이 크기 내림차순으로 json 을 따로 정리할 필요는 없고, 그저 어떤 글자가 몇번이나 반복되었는지가 명확하면 됩니다.
- 마지막으로 nychart 와 jqcloud 에 대한 내용은 해당 사이트와 제가 적어둔 주석들을 참고부탁드립니다
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- d3 -->
<!--
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://rawgit.com/jasondavies/d3-cloud/master/build/d3.layout.cloud.js" type="text/JavaScript"></script>
-->
<!-- JQCloud -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jqcloud/1.0.3/jqcloud.min.js" integrity="sha512-gZUG2nobmGEaF3G67OVAmD0lQGbkzN5t9tuiOndqzVNiWBLkCo4o6UBkBLkvKfTPWlVBJPI8dvkLJTwcJbKwvw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>-->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqcloud/1.0.3/jqcloud.css" integrity="sha512-WrGUNi0fHDkVluVxLEbsjpuzBTCiUyTJAFi20txqcKH4V8uEOaRQ+G8LnNMTuvmauaY5J05DzNayZ6RmUMy9FA==" crossorigin="anonymous" referrerpolicy="no-referrer" />-->
<!-- anyChart -->
<script src="https://cdn.anychart.com/releases/8.11.0/js/anychart-core.min.js"></script>
<script src="https://cdn.anychart.com/releases/8.11.0/js/anychart-tag-cloud.min.js"></script>
<style>
#container {
width: 800px;
height: 800px;
margin: 0;
padding: 0;
position: center;
}
#img {
margin-top: 50px;
}
.w-btn-outline {
/*width: 100px;*/
/*height: 30px;*/
position: relative;
padding: 15px 30px;
border-radius: 15px;
font-family: "paybooc-Light", sans-serif;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
text-decoration: none;
font-weight: 600;
transition: 0.25s;
}
.w-btn-red-outline {
border: 3px solid #ff5f2e;
color: #6e6e6e;
}
.w-btn-red-outline:hover {
background-color: #ff5f2e;
color: #e1eef6;
cursor: pointer;
}
#txt {
width: 200px;
height: 32px;
font-size: 15px;
border: 0;
border-radius: 15px;
outline: none;
padding-left: 10px;
background-color: rgb(233, 233, 233);
}
</style>
<script>
// 검색 버튼을 총 몇번 클릭했는지 확인하기 위한 변수
// 1번 이상 검색 시 true 로 변경
var flag = false;
$(function () {
// 사이트 첫 로딩 후 container 감추기
$("#container").hide();
// 버튼을 클릭하면 search 함수 실행
$("#search").on("click", search);
// 엔터 누르면 search 함수 실행
$(document).on("keydown", function(e){
if(e.keyCode == 13){
search();
}
})
})
function search() {
// 첫번째 검색시에는 flag 가 false 이나 1번 클릭한 후에는 flag 가 true 로 변경 후 아래 코드 실행
if (flag) {
// container 에 남아잇는 data 를 전부 지우고 감추기
$("#container")
.empty()
.hide();
console.log(flag)
}
flag = true;
console.log(flag);
// 검색어가 없으면 얼럿창
if ($("#txt").val() == "") {
alert("검색어를 정확히 넣어주세요")
return;
}
// serarch 버튼 누를 시 img 를 loading 로 변경
$("#img")
.attr("src", "/wordcloudImg/loading.gif")
.animate({
width: 300,
height: 300,
})
.css({
margin: 250
})
// text 태그에 넣은 값을 가져와서 word 에 json 형태로 저장
var word = {
word: $("#txt").val()
}
// ajax 방식으로 서버에 던짐
$.ajax({
url: "/getData", // 보내는 url
type: "get", // 보내는 방식 post
data: word, // 보내는 데이터 word 라는 json 형태
dataType: 'json', <!--서버로부터 받는 값의 데이터 형식-->
contentType: "application/json;charset=utf-8", // 서버로부터 받는 값의 콘텐츠 형태(인코딩형태?)
// ajax 로 성공적으로 데이터를 받는 경우 해당 데이터는 success 에 있는 function 을 타게 됨
// 이때 매개변수로 들어오는 값이 곧 서버에서 받은 값!!
success: function (result) {
// 서버에서 값을 받았음으로 loading 이미지는 필요없기 때문에 감추기!
$("#img").hide();
// 값을 출력할 container 모습 보이게 하기
$("#container").show();
// 서버에서 받은 result 라는 데이터를 String 형태로 만든후
// 다시 json 형태로 파싱하여 data 라는 변수에 저장한다.
var data = JSON.parse(JSON.stringify(result));
// console.log(data);
// 1. JQCloud 사용 => text : weight
// $("#wordcloud").jQCloud(data, {
// width : 800,
// height : 600,
// shape : 'rectangular',
// autoResize: true,
// classPattern: null,
// colors: ["#800026", "#bd0026", "#e31a1c", "#fc4e2a", "#fd8d3c", "#feb24c", "#fed976", "#ffeda0", "#ffffcc"],
// fontSize: {
// from: 0.1,
// to: 0.02
// }
// });
// 2. anyChart 사용 => t : value
var chart = anychart.tagCloud(data);
chart.title("My Word Cloud");
chart.container("container");
chart.hovered().fill("#8711c3");
chart.mode("spiral");
// 글자 돌아가는거 막게
chart.angles([0]);
chart.draw();
},
// 에러가 발생하면 - 대체적으로 백엔드에서 null 값이 return 되는 경우 - 경고창
error: function () {
alert("정상적이지 않은 요청입니다. 다시 시도해주세요")
location.reload();
}
})
}
</script>
</head>
<body>
<center>
<div id="div">
<img src="/wordcloudImg/search.jpg" id="img">
<div id="container" align="center"></div>
</div>
<input type="text" name="" id="txt">
<!--<input type="button" value="검색" id="search">-->
<input type="button" class="w-btn-outline w-btn-red-outline" id="search" value="검색">
</center>
</body>
</html>
Reference
- 형태소 분석기 komoran : 너무나도 감사한 open API
https://komorandocs.readthedocs.io/ko/latest/index.html
- 네이버 api 사용법 및 parsing : 어려웠던 검색 api 사용법 및 parsing 를 어떻게 해야하는지 방향을 잡을 수 있었던 글
https://needjarvis.tistory.com/658
- JQcloud
http://mistic100.github.io/jQCloud/index.html
- java JSON 다루기
- anychart
https://docs.anychart.com/Basic_Charts/Tag_Cloud
- JSON, CSS , JS 설명
- java 크롤링 정리
https://coldmater.tistory.com/125?category=734491
'토이 프로젝트 > Spring&Java 갖고놀기' 카테고리의 다른 글
DataPlay Project - 2 : 이미지 텍스트 번역기 (2) 코드 정리 (0) | 2022.06.11 |
---|---|
DataPlay Project - 2 : 이미지 텍스트 번역기 (1) 파파고 API & Tesseract OCR (0) | 2022.06.01 |
DataPlay project - 1 : java로 Word Cloud 생성기- 크롤링, API, JSON (0) | 2022.05.04 |
자바 리듬 게임 만들기(5) : 로그인 창, DAO, DTO, 점수창, score board (6) | 2022.04.19 |
자바 리듬 게임 만들기(4) (0) | 2022.03.23 |
댓글