Day 8 : 가상 DOM 생성, 웹 스크래핑, 및 JSON 데이터 MySQL 저장 (후 많다 많아)

2023. 5. 29. 15:46International Sign Lang 프로젝트/백엔드

728x90
 

Day 7 : 왜 굳이 웹 서버 프레임워크를 사용해야 하나?

2023.05.24 - [Project ISL/프론트엔드] - Day 6 : 첫걸음은 '레이아웃 설계'로부터 Day 6 : 첫걸음은 '레이아웃 설계'로부터 2023.05.18 - [Project ISL/프론트엔드] - Day 5 : 세계지도 인터랙션, 국가별 호버 이벤트

xpmxf4.tistory.com

이전 글을 읽고 와주시면 감사하겠습니다 :)


오늘의 WORK

오늘의 주제는 뭘까요?

오늘은 3 가지의 작업을 완료했습니다.

  1. world-map.html 에 존재하는 나라들의 국가코드(ISO 3166-1 alpha-2)를 모으기
  2. 해당 국가코드들의 풀네임을 매핑한 json 생성
  3. 이렇게 생성한 json 파일을 JavaScript로 DB에 넣기

world-map.html 에서 국가코드 긁어오기

저의 world-map.html 의 주된 구조는 다음과 같습니다.

<body>
    <!-- 세계 지도 -->
    <svg id="worldMap" version="1.1" width="1200" xmlns="http://www.w3.org/2000/svg"
        style="overflow: hidden; position: relative; left: -0.5px;"
        viewBox="0 0 1400 599.1070366699703" preserveAspectRatio="xMidYMid meet">
        <!--원래 xMinYMin 이였음-->
        <rect x="-4035.01" y="-1726.15" width="10090" height="4317.85" r="0" rx="0" ry="0"
            fill="#ffffff" stroke="none" transform="matrix(0.6938,0,0,0.6938,0,0)"
            stroke-width="1.4414285714285715"
            style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); fill-opacity: 0;"
            fill-opacity="0" />
        <path fill="#88a4bc" stroke="#ffffff"
            d="M1166.7,673.5H1162.6000000000001L1162.3000000000002,670.6L1161.7000000000003,667.7L1161.3000000000002,665.4000000000001L1162.7000000000003,658.3000000000001L1161.6000000000004,653.7L1159.4000000000003,644.7L1165.6000000000004,637.4000000000001L1167.3000000000004,632.8000000000001L1168.1000000000004,632.2L1169.0000000000005,628.4000000000001L1168.2000000000005,626.5000000000001L1168.6000000000006,621.7000000000002L1169.9000000000005,617.3000000000002L1170.3000000000006,609.1000000000001L1167.5000000000007,607.1000000000001L1164.8000000000006,606.6000000000001L1163.7000000000007,605.0000000000001L1161.1000000000008,603.7000000000002L1156.4000000000008,603.8000000000002L1156.2000000000007,601.4000000000002L1155.8000000000006,596.8000000000002L1173.0000000000007,591.5000000000002L1176.2000000000007,594.6000000000003L1177.7000000000007,594.0000000000002L1179.9000000000008,595.6000000000003L1180.1000000000008,598.2000000000003L1178.8000000000009,601.2000000000003L1179.000000000001,605.7000000000003L1182.500000000001,609.7000000000003L1184.400000000001,605.2000000000003L1186.900000000001,603.9000000000003L1186.800000000001,595.6000000000004L1184.600000000001,591.0000000000003L1182.700000000001,588.9000000000003H1182.3000000000009L1181.700000000001,581.6000000000004L1183.200000000001,575.5000000000003L1185.400000000001,575.3000000000003L1192.100000000001,577.1000000000003L1193.600000000001,576.3000000000003L1197.5000000000011,576.1000000000003L1199.600000000001,574.2000000000003L1203.0000000000011,574.3000000000003L1209.2000000000012,571.8000000000003L1213.800000000001,568.1000000000003L1214.7000000000012,570.9000000000002L1214.2000000000012,577.3000000000002L1214.7000000000012,583.0000000000002L1214.5000000000011,593.0000000000002L1215.300000000001,596.1000000000003L1213.400000000001,600.7000000000003L1211.000000000001,605.2000000000003L1207.3000000000009,609.2000000000003L1202.000000000001,611.6000000000003L1195.500000000001,614.7000000000003L1188.900000000001,621.6000000000003L1186.700000000001,622.8000000000003L1182.500000000001,627.4000000000003L1180.200000000001,628.8000000000003L1179.400000000001,633.4000000000003L1181.800000000001,638.3000000000003L1182.7000000000012,642.0000000000003V644.0000000000003L1183.7000000000012,643.6000000000004L1183.2000000000012,649.9000000000003L1182.1000000000013,652.9000000000003L1183.3000000000013,654.0000000000003L1182.3000000000013,656.7000000000004L1179.9000000000012,659.0000000000003L1175.2000000000012,661.1000000000004L1168.300000000001,664.6000000000004L1165.800000000001,667.0000000000003L1166.100000000001,669.7000000000004L1167.400000000001,670.1000000000004Z"
            class="sm_state sm_state_MZ" opacity="1" stroke-opacity="1" stroke-width="3.78375"
            stroke-linejoin="round" transform="matrix(0.6938,0,0,0.6938,0,0)"
            style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); opacity: 1; cursor: pointer; stroke-opacity: 1; stroke-linejoin: round; fill-opacity: 1;"
            fill-opacity="1" />

수많은 path 태그들의 반복이고 

path 태그들의 class 속성의 2 번째에 국가코드가 들어가 있습니다.

위 경우네는 st_state_MZ라고 되어 있고 이는 모잠비크의 국가 코드이죠!

 

즉, 저의 태스크는 다음으로 정의됩니다.

world-map.html의 모든 path 태그들의 2 번째 클래스를 가져오기

그리고 다음과 같이 함수를 짰습니다.

const { JSDOM } = jsdom;
const fs = require('fs');

function getCountryCodesFromHTML() {
    return new Promise((resolve, reject) => {
        fs.readFile('../html/world-map.html', 'utf8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                const dom = new JSDOM(data, { resources: "usable", runScripts: "outside-only", url: "file://" });
                const document = dom.window.document;
                var paths = document.getElementsByTagName('path');
                var countryCodes = [];

                for (var i = 0; i < paths.length; i++) {
                    var classes = paths[i].className.baseVal.split(' ');
                    for (var j = 0; j < classes.length; j++) {
                        if (classes[j].startsWith('sm_state_')) {
                            var countryCode = classes[j].substring(9);
                            countryCodes.push(countryCode);
                        }
                    }
                }
                resolve(countryCodes);
            }
        });
    });
}

프로미스 객체를 활용한 이유는

위 함수가 실행된 이후 바로 크롤링을 해서 

국가 코드의 풀네임을 매핑하기 위해서입니다!

 

잠깐 jsdom 이란? 왜 사용하나요
jsdom is a pure-JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js. In general, the goal of the project is to emulate enough of a subset of a web browser to be useful for testing and scraping real-world web applications.

[해석] 
'jsdom'은 JavaScript로 구현된 가상 DOM입니다.
이 라이브러리는 브라우저 환경이 없는 곳에서도 HTML과 DOM API를 사용하게 해 주므로,
브라우저 기반 라이브러리나 프레임워크를 테스트하거나 웹 스크래핑을 수행하는 데 유용합니다.

[출처] : https://github.com/jsdom/jsdom

위의 코드는 크롤링을 수행하는 게 아니라

실행시키지 않은 html 파일을 열어 모든 path 태그의 2 번째 class를 읽어야 한다는 것입니다.

하지만 html 파일도 결국엔 문자로 이루어진 파일이기 때문에 DOM 이 생성되지 않아 클래스의 개념이 없습니다.

즉 class라는 개념을 컴퓨터는 알 수가 없다는 거죠.

 

이 문제를 해결하기 위해서 가상, 즉 가짜 DOM을 하나 만들어야 합니다.

이를 도와주는 라이브러리가 바로 JSDOM인 거죠!

 

추출한 국가 코드의 풀네임 매핑

이제 html 파일에서 국가 코드들을 추출했다면

매핑을 할 풀네임을 가져와야겠죠?

이제는 크롤링을 수행해야 할 시간입니다.

이를 위해 cheerio를 사용했습니다!

The fast, flexible, and elegant library for parsing and manipulating HTML and XML.

[해석] : 빠르고, 유연하며, 서버를 위해 특별히 설계된 핵심 jQuery의 경량화된 구현체
[출처] : https://github.com/cheeriojs/cheerio

jQuery 스타일의 문법으로 DOM 조작 및 탐색을 가능하게 하고 

브라우저 환경이 없어도 HTML 문서를 파싱하고 조작할 수 있습니다.

 

이런 cheerio 를 사용해 구현한 코드는 다음과 같습니다!

const axios = require('axios')
const cheerio = require('cheerio')

function getCountryNamesFromWeb(codes) {
    return axios.get('https://www.iban.com/country-codes')
        .then((response) => {
            if (response.status === 200) {
                const html = response.data;
                const $ = cheerio.load(html);
                let countryNames = {};
                $('table').first().find('tbody tr').each(function (index, element) {
                    const code = $(element).find('td').eq(1).text();
                    let name = $(element).find('td').eq(0).text();

                    // Remove "(the)" from the country name
                    name = name.replace(/\(the\)/g, '').trim();

                    if (codes.includes(code)) {
                        countryNames[code] = name;
                    }
                });
                return countryNames;
            }
        });
}

파라미터로 받는 codes는 위에 나온 countryCodesFromHTML 함수의 리턴값입니다.

https://www.iban.com/country-codes에 쿼리를 날리고, 이에 대한 결과물을 cheerio로 load 해서

크롤링을 수행합니다.

 

만약 크롤링한 국가코드가 world-map.html 상에 없는 코드라면 포함시키지 않았습니다!

둘이 합친 코드

getCountryCodesFromHTML()
    .then(getCountryNamesFromWeb)
    .then(countries => {
        fs.writeFile("../json/countries.json", JSON.stringify(countries, null, 2), "utf8", function (err) {
            if (err) {
                console.error(err)
            } else {
                console.log("Country codes have been successfully saved to countries.json")
            }
        })
    })
    .catch(console.error);

아까서 말했던 것처럼 먼저 world-map.html 에서 국가코드를 추출한 후

크롤링을 수행해서 존재하는 국가코드에 대해서만 풀네임을 저장하고,

마지막에 이 결과물을 coutries.json 에다가 저장했습니다!

 

아직 끝나지 않았습니다... DB에 넣기

일일이 넣을까...?라는 인간적(?)인 고민에 빠지기도 했지만

모름지기 개발자라면 자동화에 목매야 하는 법.

이 또한 JavaScript로 한번 구현해 봤습니다.

 

const fs = require('fs');
const mysql = require('mysql2');

// Create MySQL connection
const connection = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "비밀",
    database: "ISL",
})

// Connect to MySQL
connection.connect((err) => {
    if (err) throw err
    console.log("Connected to the database.")

    // Read countries.json
    fs.readFile("src/json/countries.json", "utf8", (err, data) => {
        if (err) throw err

        const countries = JSON.parse(data)

        // Insert each country into the database
        for (let code in countries) {
            let fullName = countries[code]

            let query = "INSERT INTO country (code, full_name) VALUES (?, ?)"
            connection.query(query, [code, fullName], (err, result) => {
                if (err) throw err
                console.log(`Inserted ${fullName} into the database.`)
            })
        }
    })
})

먼저 mysql 커넥션을 생성해 주고,

이 커넥션 안에서 JSON.parse를 통한 countries.json을 파싱해

차례대로 데이터베이스 안에다가 넣어줬습니다!

 

TIL

오랜만에 프로미스 객체를 다루느라 시간이 오래걸렸다...

그리고 JavaScript 로 외부에서 DB에다가 데이터를 넣어본 것도 처음이라

역시 개발의 마스터의 길은 멀구나,,, 싶습니다...

 

 

 

GitHub - xpmxf4/ISL_FrontEnd

Contribute to xpmxf4/ISL_FrontEnd development by creating an account on GitHub.

github.com

 

GitHub - xpmxf4/ISL_BackEnd

Contribute to xpmxf4/ISL_BackEnd development by creating an account on GitHub.

github.com

위 글을 보신다면 가서 스타 한번씩만 부탁드립니다!

그리고 언제나 피드백은 환영입니다!

728x90