본문 바로가기
공부/java & Spring

[스프링에서 크롤링 데이터 수집하기] java selenium (feat. 메이플스토리 랭킹정보 수집)

by 고기 2023. 2. 7.

0. 하려는거

1. 크롬드라이버 다운로드

2. build.gradle 작성

3. 컨트롤러 작성

4. 서비스 작성

    4.1 웹 드라이버 핸들러

    4.2 핸들러 세팅

    4.3 데이터 파싱1

    4.4 데이터 파싱2

    4.5 데이터 파싱3

    4.6 세션 종료

5. 정리


 

자바는 파이썬에 비해 크롤링 자료가 적어서 답답했다.

크롤링에 대해 자바로 처음 시작하는 사람들은 좀 헤멜듯 싶기도 하고...

아무튼, 오늘 크롤링 예제로 사용될 페이지는 메이플스토리 캐릭터 정보 페이지다.

짧은 예제코드가 아니라 내가 작성한 프로젝트의 코드 일부이므로 감안해서 보면 된다.

 

0. 하려는거

※ jsoup의 문제점 및 셀레니움 사용 이유 이전 글 참고

https://1545154.tistory.com/93

 

[자바 크롤링] java jsoup의 문제점

프로젝트를 하다가 곤란한 상황을 만났다. jsoup를 사용해서 신나게 크롤링 코드를 짰는데 jsoup는 html element가 추가되는 경우, 그 값을 가져올 수 없었다. 그리고 아무래도 문제를 해결하기 위해서

1545154.tistory.com

 

해당 사진처럼 장비를 클릭할때마다 ajax같은 비동기통신을 통해 데이터를 가져온다.

이처럼 데이터를 가져오기 위해서는 클릭 이벤트가 필요하다.

jsoup로는 이러한 처리를 할 수 없기 때문에 셀레니움을 사용해서 데이터를 가져오는 것이 목표다.

 

1. 크롬드라이버 다운로드

 

먼저 해당 페이지에서 크롬드라이버를 다운로드 받는다.

https://chromedriver.chromium.org/downloads

 

ChromeDriver - WebDriver for Chrome - Downloads

Current Releases If you are using Chrome version 110, please download ChromeDriver 110.0.5481.30 If you are using Chrome version 109, please download ChromeDriver 109.0.5414.74 If you are using Chrome version 108, please download ChromeDriver 108.0.5359.71

chromedriver.chromium.org

 

셀레니움은 컴퓨터에 설치된 크롬 브라우저의 버전에 따라 드라이버를 받아야 해서 조금 귀찮다.

하기야 새로운 버전으로 업그레이드 될 일이 얼마나 있겠냐만(109 -> 110 이런식으로 앞자리)

다른 컴퓨터에서 프로젝트를 진행한다던가 그럴 때 아무튼 귀찮은건 귀찮다...

 

다운로드 받을때는 본인 컴퓨터에 설치된 크롬의 버전에 맞춰서 다운로드 받으면 된다.

버전이 없으면 적당히 앞자리만 맞춰서 다운로드 하면 된다.

 

어쨌든 다운로드 받아진 크롬드라이버는 프로젝트 홈 아래 chromedriver 폴더를 만들어 저장했다.

사진에서 확장자 없는건 mac용인데 서버 세팅한다고 다운로드 해둔거라 윈도우 사용자는 신경 안 써도 된다.

 

 

2. build.gradle 작성

본인 프로젝트에 dependencies 맨 아래 selenium만 추가 후 빌드시켜주자.

plugins {
	id 'org.springframework.boot' version '2.7.4'
	id 'io.spring.dependency-management' version '1.0.14.RELEASE'
	id 'java'
}

group = 'SV'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.jsoup:jsoup:1.15.3'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	/* spring-boot-devtools 스프링 빌드 자동화 위해 의존성 추가 */
	implementation 'org.springframework.boot:spring-boot-devtools:2.7.5'

	/* json */
	implementation 'com.googlecode.json-simple:json-simple:1.1.1'

	/* mybatis */
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'

	/* jdbc https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 */
	implementation 'com.oracle.database.jdbc:ojdbc8:21.7.0.0'
	implementation 'com.oracle.ojdbc:orai18n:19.3.0.0'

	/* swagger setting */
	implementation 'io.springfox:springfox-boot-starter:3.0.0'
	implementation "io.springfox:springfox-swagger-ui:3.0.0"

	/* selenium https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java */
	implementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '4.6.0'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

 

3. 컨트롤러 작성

프론트랑 통신하는 부분을 다루기엔 글이 너무 길어지니까 제외한다.

따라서 코드를 전부 설명하진 않겠지만 먼저 컨트롤러에서 하려는 기능을 간략하게 설명한다.

    1) 프론트단에서 ID를 입력받아서 컨트롤러로 전달 -> RequestParam value="id"

    2) 입력받은 ID에 대한 파일이 존재하는지 확인

    2-1) 파일이 존재할경우, 해당 파일을 파싱 후 JSON data로 변환

    2-2) 파일이 존재하지 않을 경우, ID에 대한 정보를 크롤링 후 JSON data로 변환 및 파일로 저장

    3) JSON data를 프론트단으로 전달

 

크롤링 코드는 40라인부터 보면 된다.

40~42라인까지 순서대로 크롬드라이버 세팅, 데이터 크롤링, 크롬드라이버 세션종료다.

/**************************************************************************************************************
 채워지는 값
 이름(id), 캐릭터 이미지(img), 직업(job), 레벨(lv), 경험치(exp), 인기도(famous), 길드(guild), 기본정보 링크(detailLink)
 메소(mapleMoney), 메이플포인트(maplePoint),
 스탯공격력(statAttack), HP(statHp), MP(statMp), STR(statStr), DEX(statDex), INT(statInt), LUK(statLuk),
 크리티컬데미지(criticalDamage), 보스공격력(bossAttack), 방어율무시(defenseIgnore), 상태이상내성(stateResistance),
 스탠스(stance), 방어력(defensePower), 이동속도(moveSpeed), 점프력(jumpPower), 스타포스(starForce),
 명성치(fameValue), 아케인포스(arcaneForce), 레전드리 어빌리티(ability), 하이퍼스탯(hyperStat),
 카리스마(charisma), 통찰력(insight), 의지(willPower), 손재주(dexterity), 감성(emotional), 매력(charm)
 농장이름(farmName), 농장레벨(farmLevel), 와르(farmMoney), 클로버(farmClover), 젬(farmGem)
 랭킹정보 링크(rankLink), 장비정보 링크(equipLink), 펫정보 링크(petLink)
 **************************************************************************************************************/
@GetMapping("/api/getMapleBasicInfo")
public String searchInfo(@RequestParam(value="id", required=false) String id,
                         @RequestParam(value="buttonChk", required=false) String buttonChk) throws IOException, InterruptedException, ParseException {
    log.info("idCheck !!" + id);
    log.info("buttonCheck !!" + buttonChk);
    JSONObject j = new JSONObject();

    int cnt = 0;
    log.info(id + " ---> " + cnt);


    String filePath = System.getProperty("user.dir")+File.separator+"data"+File.separator+id+".json";

    File f = new File(filePath);
    if(f.exists() && buttonChk.equals("조회")){
        log.info("o");
        Reader reader = new FileReader(filePath);

        JSONParser parser = new JSONParser();
        j = (JSONObject) parser.parse(reader);
    }
    else{
        log.info("x");
        
        /* get character info */
        characterCardService characterCardService = new characterCardService();
        
        characterCardService.settings(); // chrome driver setting
        j = characterCardService.getUserInfo(id);
        characterCardService.close(); // close chrome driver session

        /* file save */
        characterCardService.saveBinFile(id, j);
        Thread.sleep(2000);

    }

    log.info(j.toString());

    return j.toString();
}

 

4. 서비스 작성

인터페이스는 평소 작성하던대로 똑같이 작성하면 된다.

getUserInfo에서 데이터 크롤링을 수행한다.

그리고 saveBinFile에서 크롤링 데이터를 파일로 저장하는데 이건 크롤링이랑 상관없으니 넘긴다.

public interface characterCardServiceI {
    public abstract JSONObject getUserInfo(String name) throws InterruptedException;
    public abstract void saveBinFile(String name, JSONObject j) throws IOException;
}

 

4.1 웹 드라이버 핸들러

 

핵심은 24라인의 웹드라이버 핸들러다.

 

보통 스프링에서 크롤링을 수행한다 라고 하면 웹이니까

여러 사람이 동시에 크롤링을 수행할 것을 전제로 설계하는게 일반적일... 것이다. (아마) 적어도 난 그랬다.

스프링에서는 스레드 처리를 자동으로 해줘서 여러 사람이 동시에 눌러도 어느정도 처리가 된다.

뭐 해당 서버에서 부하가 일어날정도로 많은 요청이 있으면 힘들겠지만 어쨌든 된다.

 

그런 전제를 두고, 핸들러는 요청을 받을때마다 생성되어야 한다.

그래서 3. 컨트롤러 작성의 38라인에서 크롤링 요청을 받을때마다 서비스 객체를 생성해준다.

생성된 서비스 객체의 셀레니움 핸들러는 사용자의 요청을 처리해준다.

package SV.mh.service;

import org.json.simple.JSONObject;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class characterCardService implements characterCardServiceI{

    //WebDriver
    private WebDriver driver;    
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    // return json data
    JSONObject j = new JSONObject();
    
    //System Property SetUp
    public void settings() { ... }

    // get url : user's detail info
    @Override
    public JSONObject getUserInfo(String name) throws InterruptedException { ... }

    // getUserInfo -> callDetailUrl
    // get url : user's basic info
    public void callDetailUrl(String url) throws InterruptedException { ... }

    // getUserInfo -> callDetailEquip
    // get url : user's equipment info
    public void callDetailEquip(String detailEquip) throws InterruptedException { ... }

    // getUserInfo -> close
    public void close() { ... }

    // json data save to excel
    @Override
    public void saveBinFile(String name, JSONObject j) throws IOException { ... }
}

 

4.2 핸들러 세팅

 

먼저 크롬드라이버 핸들러 세팅을 해준다.

 

드라이버 경로는 위에서 저장했던 드라이버 경로를 지정해주면 된다.

File.separator를 사용하면 윈도우와 맥 구분없이 경로 지정이 가능하다.

윈도우면 그냥 "C:/User/~~~directoryPath~~~/yourProject/chromedriver.exe"로 지정해도 상관없다.

 

option을 줘서 크롬드라이버를 보이지 않게 하거나 창 크기를 조절하거나 할 수 있는데,

본인이 찾아보면 더 자세히 알 수 있으니 이 부분은 넘어간다.

//System Property SetUp
public void settings() {
    String WEB_DRIVER_ID = "webdriver.chrome.driver";
    String WEB_DRIVER_PATH = System.getProperty("user.dir")+File.separator+"chromedriver"+File.separator+"chromedriver"; // 다운받은 크롬드라이버 경로

    String w = System.getProperty("user.dir");

    // windows : driver_path += .exe
    if (w.substring(0,1).equals("C")){ 
        WEB_DRIVER_PATH += ".exe";
    }
    log.info(WEB_DRIVER_PATH);

    System.setProperty(WEB_DRIVER_ID, WEB_DRIVER_PATH);
    ChromeOptions options = new ChromeOptions();
//       options.setHeadless(true);
//       options.addArguments("--lang=ko");
//       options.addArguments("--no-sandbox");
//       options.addArguments("--disable-dev-shm-usage");
//       options.addArguments("--disable-gpu");

    driver = new ChromeDriver(options);

    // browser loading timeout 1s
    driver.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS);
}

 

4.3 데이터 파싱1

여기부터는 어떤식으로 기록해야할지 모르겠어서 주석을 사용해서 설명한다.

// get url : user's detail info
@Override
public JSONObject getUserInfo(String name) throws InterruptedException {
    
    // ex) name : 명회
    // "https://maplestory.nexon.com/Ranking/World/Total?c=명회" 를 검색한다    
    String url = "https://maplestory.nexon.com/Ranking/World/Total?c=" + name;
    driver.get(url);
    
    // ※ 해당 url로 검색 후 0.5초 쉬어주는 이유
    // 검색한 페이지가 로딩중이라면 다음 로직을 실행했을 때 
    // 아직 html 요소가 로딩되지 않아 에러가 발생하기 때문이다.
    Thread.sleep(500);

    int characterChk = 0;
    // 해당 path에 대한 element 길이를 얻는다.
    // 사진1 참고
    int cnt = driver.findElement(By.xpath("//*[@id=\"container\"]/div/div/div[3]/div[1]/table/tbody")).findElements(By.tagName("dt")).size();

    // search character list
    for (int x=0; x<cnt; x++){                
        String tmp = driver.findElement(By.xpath("//*[@id=\"container\"]/div/div/div[3]/div[1]/table/tbody/tr["+(x+1)+"]/td[2]/dl/dt/a")).getText(); // character name

        // find character
        if (tmp.equals(name) == true){
            characterChk = 1;

            // 캐릭터 정보를 얻는다
            // 사진2 참고            
            String detailUrl = driver.findElement(By.xpath("//*[@id=\"container\"]/div/div/div[3]/div[1]/table/tbody/tr["+(x+1)+"]/td[2]/dl/dt/a")).getAttribute("href"); // character detail url            
            String level = driver.findElement(By.xpath("//*[@id=\"container\"]/div/div/div[3]/div[1]/table/tbody/tr["+(x+1)+"]/td[3]")).getText(); // character level            
            String img = driver.findElement(By.xpath("//*[@id=\"container\"]/div/div/div[3]/div[1]/table/tbody/tr["+(x+1)+"]/td[2]/span/img[1]")).getAttribute("src"); // character img

            JSONObject info = new JSONObject();

            info.put("name", name);   // id
            info.put("level", level); // lv
            info.put("img", img);   // img

            j.put("characterInfo", info); // character info

            // detail url -> user basic info
            callDetailUrl(detailUrl);
            break;
        }
    }

    if (characterChk == 0){
        j.put("notFound","");
    }
    return j;
}

 

4.3.1 사진1

캐릭터 목록이 담긴 tbody의 path를 구한다.

캐릭터를 검색했을 때 검색된 캐릭터가 없을 경우 tbody의 길이는 0이 된다.

 

4.3.2 사진2

캐릭터 상세정보 url이 담긴 path를 구한다.

마찬가지로 캐릭터 이름, 캐릭터 img를 구한다.

 

4.4 데이터 파싱2

4.3과 마찬가지로 주석을 사용해서 설명한다.

// getUserInfo -> callDetailUrl
// get url : user's basic info
public void callDetailUrl(String url) throws InterruptedException {
    // 캐릭터 상세정보 url로 이동
    driver.get(url);
    Thread.sleep(500);

    // character detail info -> basic & stat
    // 캐릭터 스탯 정보를 얻는다.
    // 사진3 참고
    String[] tmp = driver.findElement(By.xpath("//*[@id=\"container\"]/div[2]/div[2]/div/div[2]")).getText().split("\n");

    // split ability
    String ability = "";
    int hyperStatCnt = 0;
    for (int x=51; x<tmp.length; x++){
        // get hyperStat location
        if (tmp[x].equals("하이퍼스탯")){
            hyperStatCnt = x+1;
            break;
        }
        ability+=tmp[x]+"\n";
    }

    // split hyperStat
    String hyperStat = "";
    for (int y=hyperStatCnt; y<tmp.length; y++){
        hyperStat+=tmp[y]+"\n";
    }

    // basic info setting -> json
    JSONObject basicInfo = new JSONObject();

    basicInfo.put("world",tmp[2]);  // 월드
    basicInfo.put("job",tmp[4]);    // 직업
    basicInfo.put("famous",tmp[6]); // 인기도
    basicInfo.put("guild",tmp[8]);  // 길드
    basicInfo.put("money",tmp[10]); // 메소
    basicInfo.put("maplePoint",tmp[12]); // 메이플포인트
    basicInfo.put("stat",tmp[15]); // 스탯공격력
    basicInfo.put("hp",tmp[17]);   // hp
    basicInfo.put("mp",tmp[19]);   // mp
    basicInfo.put("str",tmp[21]);  // str
    basicInfo.put("dex",tmp[23]);  // dex
    basicInfo.put("int",tmp[25]);  // int
    basicInfo.put("luk",tmp[27]);  // luk
    basicInfo.put("criticalDamage",tmp[29]);  // 크리티컬 데미지
    basicInfo.put("bossAttack",tmp[31]);      // 보스공격력
    basicInfo.put("defenseIgnore",tmp[33]);   // 방어율무시
    basicInfo.put("stateResistance",tmp[35]); // 상태이상내성
    basicInfo.put("stance",tmp[37]);      // 스탠스
    basicInfo.put("defensePower",tmp[39]);// 방어력
    basicInfo.put("moveSpeed",tmp[41]);   // 이동속도
    basicInfo.put("jumpPower",tmp[43]);   // 점프력
    basicInfo.put("starForce",tmp[45]);   // 스타포스
    basicInfo.put("fame",tmp[47]);        // 명성치
    basicInfo.put("arcaneForce",tmp[49]); // 아케인포스
    basicInfo.put("ability",ability);     // 어빌리티
    basicInfo.put("hyperStat",hyperStat); // 하이퍼스탯

    j.put("characterBasicInfo", basicInfo); // basic info

    // get character equip url
    // 캐릭터 장비정보 url을 얻는다
    String detailEquip = driver.findElement(By.xpath("//*[@id=\"container\"]/div[2]/div[1]/ul/li[3]/a")).getAttribute("href");
    // equip url -> user equip info
    callDetailEquip(detailEquip);
}

 

4.4.1 사진3

캐릭터의 기본, 스탯 정보가 담긴 path를 얻는다.

 

4.5 데이터 파싱3

드디어 대망의 클릭 이벤트까지 왔다. 라기엔 단순하게 클릭 이벤트를 주는 것 뿐이지만,,,

위와 마찬가지로 주석을 사용해서 설명한다.

// getUserInfo -> callDetailEquip
// get url : user's equipment info
public void callDetailEquip(String detailEquip) throws InterruptedException {
    // 캐릭터 장비정보 url로 이동
    driver.get(detailEquip);
    Thread.sleep(500);

    JSONObject equipInfo = new JSONObject();
    List<JSONObject> e = new ArrayList<JSONObject>();

    // equip element -> /li[1] ~ /li[30]
    for (int x=1; x<31; x++){
        JSONObject equip = new JSONObject();

        // 2, 4, 9, 26, 27, 29 -> this index isn't equip -> not check
        if ((x==2) || (x==4) || (x==9) || (x==26) || (x==27) || (x==29)){
            log.info(x + ": not equip ... pass");
        }
        // check other index
        else{
            // img src length
            int imgSize = driver.findElement(By.xpath("//*[@id=\"container\"]/div[2]/div[2]/div/div[2]/div[1]/ul/li["+x+"]/img")).getAttribute("src").length();

            // img src is blank -> not check
            if (imgSize == 0) {
                log.info(x + ": src is blank ... pass");
            }
            // this equip element(x) location click
            else{
                // click and delay settings
                // 사진4 참고
                driver.findElement(By.xpath("//*[@id=\"container\"]/div[2]/div[2]/div/div[2]/div[1]/ul/li["+x+"]")).click();
                Thread.sleep(500);

                // get this equip element's info -> text                
                String equipElement = driver.findElement(By.xpath("//*[@id=\"container\"]/div[2]/div[2]/div/div[2]/div[2]")).getText();
                // get this equip element's index -> index
                String img = driver.findElement(By.xpath("//*[@id=\"container\"]/div[2]/div[2]/div/div[2]/div[1]/ul/li["+x+"]/img")).getAttribute("src");

                equipElement = equipElement.replace("착용 가능한 직업 | 초보자, 전사, 마법사, 궁수, 도적, 해적\n", "")
                        .replace("REQ LEV\n","")
                        .replace("REQ STR\n","")
                        .replace("REQ LUK\n","")
                        .replace("REQ DEX\n","")
                        .replace("REQ INT\n","")
                        .replace("기타\n","")
                        .replace("교환 불가\n","")
                        .replace("고유장착 아이템\n","")
                        .replace("황금망치 제련 적용\n","")
                        .replace("플래티넘 카르마의 가위를 사용하면 1회 교환이 가능하게 할 수 있습니다.\n","")
                        .replace("검은 마법사와의 결전을 준비하는 테네브리스 원정대를 위해 연합에서 제작한 특별한 힘을 가진 반지이다. 테네브리스 원정대 반지 강화 주문서만 사용 가능\n","")
                        .replace("잠재능력 재설정 불가\n","")
                        .replace("검은 마법사의 군단장 아카이럼이 지녔다고 알려진 전설의 목걸이\n","")
                        .replace("메이플스토리 18주년 기념 훈장이다.\n","");

                log.info("----------");
                log.info("x: "+ x);
                log.info("img: "+ img);

                String[] infoSplit = equipElement.split("\n");
                log.info(equipElement);
                log.info("장비이름:" + infoSplit[0]);
                log.info("next:" + infoSplit[1]);

//                    equip.put("equipName",infoSplit[0]);
                equip.put("equipImg",img);

                int weaponStr = 0;

                for(int y=0; y<infoSplit.length;y++){
                    if (infoSplit[y].contains("장비분류 |")){
                        log.info(infoSplit[y]+"-"+infoSplit[y]);
                        equip.put("equipCategory",infoSplit[y]);
                        y=y+1;
                    }
                    else if (infoSplit[y].equals("잠재옵션")){
                        log.info(infoSplit[y]+"-"+infoSplit[y+1]);
                        equip.put("equipPotential",infoSplit[y+1]);
                        y=y+1;
                    }
                    else if (infoSplit[y].equals("에디셔널 잠재옵션")){
                        log.info(infoSplit[y]+"-"+infoSplit[y+1]);
                        equip.put("equipAdditionalPotential",infoSplit[y+1]);
                        y=y+1;
                    }
                    else if (infoSplit[y].equals("소울옵션")){
                        weaponStr = 1;
                    }
                }

                if(weaponStr == 0){
                    equip.put("equipName",infoSplit[0]);
                }
                else if(weaponStr == 1){
                    equip.put("equipName",infoSplit[0] + " " + infoSplit[1]);
                }

                equip.put("equipNum", x);
                e.add(equip);

                log.info("----------");
            }
        }
    }
    j.put("equipInfo",e);
}

 

4.5.1 사진4

아이템은 30개의 영역(5x6)으로 나뉘어있고, 각 영역을 클릭하면 오른쪽에 장비정보가 출력된다.

해당 영역을 클릭하면 비동기형식으로 서버에서 받아온 데이터를 html element에 추가하는 방식이다.

 

여기서 아이템 정보를 받아오는 방식은 이전 글에서 작성했었는데, 셀레니움을 사용하게 된 이유이기도 하다.

따라서 데이터를 파싱하기 위해서 각 아이템 영역을 한 번씩 클릭해서 데이터를 받아와야 한다.

 

4.6 세션 종료

먼저, close()는 열려있는 탭을 종료시키고 quit()은 모든 탭을 종료시킨다.

 

셀레니움 드라이버 커서를 get으로 열게되면 탭이 여러 개 생성되는데,

close()를 사용하면 남아있는 탭이 존재하게 되므로 현재 세션이 종료되지 않는다.

 

사실 개인 컴퓨터에서 혼자 사용하는 프로그램이면 적당히 사용하다가 종료시켜도 상관없지만,

웹 프로그램을 배포했을 시 다수의 사용자로 인해 종료되지 않은 세션이 많아지면 이는 메모리 누수로 이어진다.

따라서 현재 로직에서 quit()을 사용한 이유는 해당 세션에서 더 이상 어떤 탭도 열려있을 필요가 없기 때문이며

세션을 종료하기 위함이다.

 // getUserInfo -> close
public void close(){
    // driver.close();
    driver.quit();
}

 

5. 정리

여기까지 크롤링 코드는 끝이다.

이렇게 자바 언어로 크롤링 프로그램을 짜봤는데 이 정도 했으면 뭐... 다른 프로그램도 짤 수 있겠지.

근데 사실 시간을 잡아먹었던건 자바 언어로 크롤링 코드를 짜는 것보다 스프링에서 적용하는 과정이었다ㅋㅋㅋ

서비스 단에서 데이터를 받아와서 컨트롤러에서 받고 프론트단으로 넘겨주고 복잡하다복잡해...

 

그나저나 이렇게 구현을 해놓고 보니까 문제를 발견했는데...

이 크롤링이라는건 결국 남 서버에 요청을 보내는거라서 짧은 시간에 많은 요청을 보내면 차단먹는단 말이지.

그래서 이 문제를 어떻게 해결해야할지 골치를 썩였다.

 

스레드 개수를 줄여도 어차피 끊임없이 요청하면 차단당하는건 똑같으니...

그냥 서버 개수를 늘려서 처리하도록 하는 방식이 제일 베스트일 것 같긴 한다.

근데 뭐 그렇게 할 정도로 큰 프로그램도 아니고 애초에 할 수도 없으니 크롤링 프로젝트는 여기까지...

댓글