본문 바로가기

programming/내가 만들고 싶어서 만든 것!

코로나 종합 어플리케이션 작성기 (Jsoup을 활용한 실시간 확진자 알림)

반응형

1. 개요

 

 학교에서 프로젝트를 진행하면 약간의 장학금과 3학점을 주는 프로젝트 활동을 진행하였다. 팀단위로 프로젝트를 개발하는 것 자체가 큰 경험이라고 생각하였고 비대면 수업으로 인해 시간도 많아졌다고 생각했기에 (실제로는 절대 아니었다.) 프로젝트에 참여하였다. 나는 데이터베이스를 구축하고 해당 데이터베이스에 저장된 데이터들을 가지고 지도에 위치를 표시하거나 실시간 동선 알림을 확인하는 등의 기능을 구현하였다. 다음은 크롤링을 통해 실시간 확진자 접촉 알림 기능을 구현한 과정을 설명하고자 한다.

 

2. 크롤링

 

 크롤링을 처음 접한건 1학년 겨울방학 때이다. 한참 알고리즘 문제들로 머리가 아프던 와중에 언어를 활용하는 프로젝트를 진행해보고 싶었고, 그 와중에 크롤링을 공부하기로 마음먹었다. 당시에는 파이썬으로 BeautifulSoup을 활용하여 크롤링을 구현하였다. 간단하게 크롤링을 설명하자면 태그로 구성된 웹페이지에서 원하는 정보를 가져오는 기술이라고 말할 수 있다. 사실 나도 엄청나게 자세히 아는 것은 아니니 잘 설명된 글을 읽어보는 것도 좋겠다.


크롤링: m.blog.naver.com/with_msip/221844584021

 

지루한 검색은 이제 그만! 정보를 빠르게 수집하는 '크롤링'

과제를 할 때 필요한 정보를 찾기 위해 오랜 시간 검색을 했던 경험을 한 번쯤은 해보셨을 것입니다. 이처...

blog.naver.com


 3. 구현과정

 

 이 기능의 목적은 자신이 2주동안 다니던 동선에서, 코로나 확진자가 발생하다면 그것을 알리기 위함에 있다. 여러 방법으로 코로나 확진자의 정보를 얻으려했지만 그저 그런 대학교 학생들이 만드는 앱을 위해 누가 이런 정보를 주겠는가? 그렇다고 포기할수는 없었다. 그래서 반년전의 기억을 떠올려 크롤링을 생각한 것이다. 안드로이드스튜디오를 활용해 Java로 앱을 구현하였기에, Java언어의 크롤링 오픈소스를 찾아보았고 다행이도 Jsoup을 발견할 수 있었다. 간단한 크롤링이기에 하루동안 공부해서 구현하였다.

 

- HTML코드 분석

 

 Jsoup의 존재유무를 확인하고 나서는 바로 어떤 웹사이트의 정보를 가져와야할지를 고민하였다. 계속 사용해야하기에 통일된 코드로 구성되며, 실시간으로 코로나 확진자 데이터가 올라오는 사이트를 찾아보았다. 여러사이트를 고민하다 네이버에서 제공하는 정보를 사용하기로 하였다.


네이버 페이지


 다음은 코로나를 검색하면 나오는 네이버의 웹 페이지이다. 파란박스부분을 보면 실시간 확진자가 지역과 함께 꾸준하게 업데이트 되는 것을 볼 수 있다. 그럼 어떤 태그들로 이 정보가 이루어져있는지 확인해보자. 다음은 해당하는 부분의 코드이다.



 <ul> 태그안에 <li> 태그로 각각의 도시들이 표시되어있다. 내가 원하는 것은 도시와 세부도시의 이름이다. 이들은 어떤 태그로 구성되어있을까? 다음 태그를 보자.



 <strong> 태그안에 message_area class 속성으로 내가원하는 도시와 세부도시의 정보가 표시되어있다. 그럼 이 특징을 가지고 정보를 뽑아내면 된다.

 

- 규칙찾기

 

 내가 원하는 정보는 도시와 세부도시에 대한 정보이다. 그러나 아쉽게도, <strong>태그 안에는 그 정보만이 아닌 확진자 발생 시간에 대한 정보도 포함되어있다. 위의 그림에서 <em> 태그로 감싸진 '1분전' 이라는 문자열을 볼 수 있을 것이다. 즉, <strong>태그로 인해 문자열을 가지고 온다면 다음과 같이 한칸 띄어진 형태로 정보가 수집된다는 뜻이다.


도시 세부도시 시간정보


 그럼 내가 해야할 일은 가지고온 정보 중에서, 필요없는 시간정보를 삭제하는 것이다. 그전에 확인해야할 다른 문제가 있다. 다음 사진을 확인해보자.



 위 태그를 보면 <strong> 태그에 도시에대한 정보만이 있을 뿐 세부도시에 대한 정보가 없는 경우가 있다. 위 그림에서도 광주만 적혀있지 세부도시에 대한 정보가 없다. 다음과 같은 경우는 <strong>태그를 크롤링 했을 때, 다음과 같은 형태로 정보를 가지고 오게 된다.


도시 시간정보


 즉 내가 작성해야하는 코드는 다음과 같은 알고리즘을 가져야한다.


  • <strong> 태그의 message_area class 속성으로 정보 가지고오기
  • 해당정보에 세부도시에 관한 정보가 있는지 판단
  • 정보가 있으면 그대로 정보저장
  • 정보가 없다면 '세부도시 없음'이라는 정보와 같이 도시정보 저장

- 소스코드

 

다음은 위의 순서도 형태로 작성한 나의 Java 코드이다.

 

doc = Jsoup.connect("https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=1&ie=utf8&query=%EC%BD%94%EB%A1%9C%EB%82%98+19").get();
Elements contents = doc.select(".message_area");
contents = contents.select("strong");
for(Element buff : contents){
	String[] cityBuff = buff.text().split(" ");
    String save = cityBuff[0] + " ";
    if(cityBuff[1].charAt(cityBuff[1].length() - 1) == '전'){
    	save += "세부도시 없음";
    }else{
    	save += cityBuff[1];
    }
    	citys.add(save);
}

 

 여기서 citys는 문자열을 저장하는 배열이다. 이렇게 저장된 배열을 데이터베이스에 저장된 정보와 같이 비교하여 확진자 동선알림 앱을 작성하였다. 다음은 내가 연습한 전체 소스코드이니 참고하기 바란다.

 

package com.cookandroid.study_jsoup;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;

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

import java.io.IOException;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    String city = "";
    TextView textView;
    ArrayList<String> citys = new ArrayList<>();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.number);
        final Bundle bundle = new Bundle();

        new Thread(){
            @Override
            public void run() {
                Document doc = null;
                try {
                    doc = Jsoup.connect("https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=1&ie=utf8&query=%EC%BD%94%EB%A1%9C%EB%82%98+19").get();
                    Elements contents = doc.select(".message_area");
                    contents = contents.select("strong");
                    for(Element buff : contents){
                        String[] cityBuff = buff.text().split(" ");
                        String save = cityBuff[0] + " ";
                        if(cityBuff[1].charAt(cityBuff[1].length() - 1) == '전'){
                            save += "세부도시 없음";
                        }else{
                            save += cityBuff[1];
                        }
                        citys.add(save);
                    }
                    for(String strBuff : citys){
                        city += strBuff + "\n";
                    }

                    bundle.putString("numbers", city);
                    Message msg = handler.obtainMessage();
                    msg.setData(bundle);
                    handler.sendMessage(msg);

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            Bundle bundle = msg.getData();
            textView.setText(bundle.getString("numbers"));
        }
    };
}
반응형