Crawling - puppeteer

김세겸·2023년 2월 16일
0

Crawing

목록 보기
1/3
post-thumbnail

1. 들어가며

현재 프로젝트 멤버를 모아 프로젝트를 진행중에 있다. 진행중인 프로젝트는 대구광역시 아동급식카드가맹점을 지도에서 보여주는 서비스이다.

이 프로젝트를 진행하기 위해 정부에서 지원해주는 공공데이터를 찾아지만 우리가 서비스 할려는 기능에 비해 정보가 적어 크롤링을 통해 필요한 정보를 채우고자 한다.

2. csv파일 가공

const fs = require("fs");
const path = require("path");

const file_name = "20221125.csv";
const file_path = path.join(__dirname, file_name);

console.log(file_path);

// fs를 통해 파일을 읽어옴
const csv = fs.readFileSync(file_path, "utf-8");
// row별로 csv파일을 나눔
const rows = csv.split("\r\n");

for( const row of rows) {
    // row 형태
    // 가맹점ID,가맹점명,가맹점유형코드,시도명,시군구명,시군구코드,소재지도로명주소,소재지도로명주소상세정보 ...
    const split = row.split(",");
    const storeName = split[1];
    const sotreType = split[2];
    const storeAddress = split[6]
	
    // 가게 타입이 20인경우 편의점 or 마트 그래서 메뉴 정보가 필요 없기애 뺴주는 로직
    if(sotreType === "20") {
        continue
    }
    console.log(storeName + storeAddress);
}

가게명 만으로 검색하기에는 검색결과가 많이 나오는 경우가 있었다. 그래서 가게명 + 도로명 주소로 검색해 주기로 하였다.

검색결과 잘나오는 것을 확인 하였다. 이제 이 데이터를 가지고 자동으로 검색결과를 가져오도록 세팅해 보자.

3. puppeteer

npm i puppeteer


puppeteer 공식문서에서 제공하는 예제이다. 이걸 참조해서 내 코드에 추가해 보자!

1. iframe

현재 검색 결과가 없을경우의 예외처리를 위해 검색결과가 없을 때 나오는 문구를 기준을 잡으로려고 한다.
하지만 모든 검색결과에서 검색결과가 없을 때 나오는 문구를 찾아 내지 못하고 있다.

검색 결과가 없음을 나타내는 문구는 현재 iframe안에 있다. 그러므로 page가 아닌 iframe에서 잡아 와야 한다.

2. ifram잡기

진짜 별별짓을 다해서 iframe을 잡아왔다...
대충 상황을 설명하자면 iframe가 엄청 많았고 그중에 잘못된 ifram을 가져와서 테스트 하는바람에 계속 element를 못가져 오는 거였다.
그래서 결국 try...catch문으로 감싸서 해결하였다.

const fs = require("fs");
const path = require("path");
const puppeteer = require("puppeteer");
const cheerio = require("cheerio");

const file_name = "20221125.csv";
const file_path = path.join(__dirname, file_name);

console.log(file_path);

const csv = fs.readFileSync(file_path, "utf-8");
const rows = csv.split("\r\n");

init(rows);

async function init(rows) {
    let index = 0
    for( const row of rows) {
        // row 형태
        // 가맹점ID,가맹점명,가맹점유형코드,시도명,시군구명,시군구코드,소재지도로명주소,소재지도로명주소상세정보,소재지지번주소,소재지지번주소상세정보,전화번호,사업자번호,종사업자번호,대표자명,사업자상태,사업자상태변경일,프랜차이즈명,일반직영,음식점분류,시설정보구분,위도,경도,평일운영시작시각,평일운영종료시각,토요일운영시작시각,토요일운영종료시각,공휴일운영시작시각,공휴일운영종료시각,배달시작시각,배달종료시각,아침점심저녁구분,배달가능여부,관리기관,관리기관전화번호,데이터기준일자
        const split = row.split(",");
        const storeName = split[1];
        const sotreType = split[2];
        const storeAddress = split[6]
    
        if(sotreType === "20") {
            continue
        }
        console.log(storeName + " " + storeAddress);
    
        const searchName = storeName + " " + storeAddress;
        // if(index > 1){
            await searchPuppeteer(searchName);
        // }
        index++
    }
}


// puppeteer & cheerio
async function searchPuppeteer(searchName) {
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();
    await page.setViewport({width: 1000, height: 10000});
    await page.goto("https://map.naver.com/v5/search/" + searchName);
    wait(1);

    let frame;
    const timer = setTimeout(() => {
        page.close();
    }, 5000);
    timer;
    try{
        frame = await page.waitForFrame(async frame => {
            return frame.name() === 'entryIframe';
        });
        clearTimeout(timer);
    }catch{
        return;
    }

    await frame.click('.flicking-camera > a:nth-child(2)');

    // page.close();

}

function wait(sec) {
    let start = Date.now(), now = start;
    while (now - start < sec * 1000) {
        now = Date.now();
    }
}

puppetier가 없는 요소를 잡아오는 에러를 내는데 시간이 너무 오래걸려서 setTimeout함수를 활용하여 page를 닫고 강제로 에러를 발생시켜 주었다.
이제 메뉴 버튼까지 누르는 로직을 짯으니 메뉴 정보를 가져와 보자.

3. 메뉴정보 가져오기

// puppeteer & cheerio
// ... 생략
const result = {};
async function searchPuppeteer(searchName) {
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();
    await page.setViewport({width: 1000, height: 10000});
    await page.goto("https://map.naver.com/v5/search/" + searchName);
    wait(1);

    let frame;
    const timer = setTimeout(() => {
        browser.close();
    }, 5000);
    timer;
    try{
        frame = await page.waitForFrame(async frame => {
            return frame.name() === 'entryIframe';
        });
        clearTimeout(timer);
    }catch{
        console.log(searchName +' 검색 결과가 없습니다.');
        result[searchName] = [];
        return;
    }

    await frame.click('.flicking-camera > a:nth-child(2)');
    try {
        await frame.click('#app-root > div > div > div > div:nth-child(7) > div > div.place_section.no_margin > div.lfH3O > a');  
    } catch (error) {
        // const content = await frame.content();
        // const $ = await cheerio.load(content);
        // const list = $('.P_Yxm');
        console.log('메뉴 더보기 버튼이 없습니다.');
        const list = await frame.$$(".P_Yxm");
        const lists = await frame.waitForSelector(".P_Yxm")
        console.log(list, lists);
    }

    // page.close();

}

메뉴 버튼과 메뉴 더보기 버튼 클릭까지 문제 없이 진행되던 와중에 현재는 메뉴 안에 있는 메뉴 리스트 정보를 찾지 못하고 있다.
$$, $$eval, waitForSelector, cheerio 등등 여러가지를 써보았지만 현재는 답이 보이지 않는다.

해결책

$$로 가져오는 값은 puppeteer의 ElementHandle 객체 이다. 그래서 아무 값도 가져오지 않는것처럼 보였던 것이다. 이것을 해결하기 위해서는 가져온 ElementHandle 객체로 evaluate함수를 돌려 주면 된다.

const list = await frame.$$("li");
for(const li of list) {
  const html = await li.evaluate((li) => {
    return li.innerHTML;
  })
  console.log(html);
}

이것을 통해 메뉴 리스트의 정보 값을 가져 올 수 있게 되었다 이제 가공해서 저장해 보도록 하자.

4. 메뉴 정보 가공

메뉴 버튼을 눌렀을때 나오는 리스트의 class명이 여러개라서 메뉴정보를 가지고 있는 element를 가져오는데 시간이 좀 걸렸다.

const fs = require("fs");
const path = require("path");
const puppeteer = require("puppeteer");
const cheerio = require("cheerio");

const file_name = "20221125.csv";
const file_path = path.join(__dirname, file_name);

console.log(file_path);

const csv = fs.readFileSync(file_path, "utf-8");
const rows = csv.split("\r\n");

init(rows);

async function init(rows) {
    let index = 0
    for( const row of rows) {
        // row 형태
        // 가맹점ID,가맹점명,가맹점유형코드,시도명,시군구명,시군구코드,소재지도로명주소,소재지도로명주소상세정보,소재지지번주소,소재지지번주소상세정보,전화번호,사업자번호,종사업자번호,대표자명,사업자상태,사업자상태변경일,프랜차이즈명,일반직영,음식점분류,시설정보구분,위도,경도,평일운영시작시각,평일운영종료시각,토요일운영시작시각,토요일운영종료시각,공휴일운영시작시각,공휴일운영종료시각,배달시작시각,배달종료시각,아침점심저녁구분,배달가능여부,관리기관,관리기관전화번호,데이터기준일자
        const split = row.split(",");
        const storeName = split[1];
        const sotreType = split[2];
        const storeAddress = split[6]
    
        if(sotreType === "20") {
            continue
        }
        console.log(storeName + " " + storeAddress);
    
        const searchName = storeName + " " + storeAddress;
        if(index > 1){
            await searchPuppeteer(searchName);
        }
        index++
    }
}


// puppeteer & cheerio
const result = {};
async function searchPuppeteer(searchName) {
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();
    await page.setViewport({width: 1000, height: 10000});
    await page.goto("https://map.naver.com/v5/search/" + searchName);
    wait(1);

    let frame;
    // 검색결과가 없는경우 빠르게 브라우저를 닫아주기 위한 timer.
    const timer = setTimeout(() => {
        browser.close();
    }, 5000);
    timer;
    // 검색결과가 있는지 없는지 try catch문
    try{
        frame = await page.waitForFrame(async frame => {
            return frame.name() === 'entryIframe';
        });
        clearTimeout(timer);
    }catch{
        console.log(searchName +' 검색 결과가 없습니다.');
        result[searchName] = [];
        return;
    }

    wait(1);

    // 검색 결과가 있어도 menu정보가 없을 경우를 대비한 예외처리 
    const menuBtn = await frame.$eval('.flicking-camera > a:nth-child(2) > span', el => el.innerText);
    if(menuBtn !== '메뉴') {
        console.log("메뉴 버튼이 없습니다.");
        result[searchName] = [];
        browser.close();
        return;
    }

    // menu정보가 있을경우 menu버튼 클릭
    await frame.click('.flicking-camera > a:nth-child(2)');

    // 메뉴 더보기 버튼이 있을경우 클릭
    let menu;
    try {
        await frame.click('#app-root > div > div > div > div:nth-child(7) > div > div.place_section.no_margin > div.lfH3O > a');  
    } catch (error) {
        console.log('메뉴 더보기 버튼이 없습니다.');
        let ul;
        // 네이버에서 주문이 가능한 경우 메뉴정보가 담긴 클래스가 달라서 나눈 예외문
        try {
            ul = await frame.waitForSelector('.ZUYk_');
            menu = await getMenu('.Sqg65', '.SSaNE', ul);
        } catch (error) {
            ul = await frame.waitForSelector('.list_place_col1');
            menu = await getMenu('.name', '.price', ul);
        }
    }

    browser.close();
    result[searchName] = menu;
    console.log(result);
}

function wait(sec) {
    let start = Date.now(), now = start;
    while (now - start < sec * 1000) {
        now = Date.now();
    }
}

async function getMenu(nameClass, priceClass, ul) {
    const arr = [];
        const allName = await ul.$$(nameClass);
        const allPrice = await ul.$$(priceClass);
        for(const i in allName) {
            const name = await allName[i].evaluate(n => n.innerText);
            const price = await allPrice[i].evaluate(p => p.innerText);
            arr.push({name, price})
        }
        return arr;
}

현재 코드 전체이다. 어찌어찌 예외처리를 해서 잘 담기는거 까지 확인을 했지만 내 생각보다 예외처리가 더 필요하다.
더보기 버튼이 한번이 아니라 여러번 눌러야 다나오는 경우, 더보기 버튼이 아니라 메뉴 카테고리 별로 메뉴가 나눠진 경우 이러한 경우를 다 예외처리 한다면 시간이 너무 오래 걸릴꺼 같은데 방법을 찾아봐야 겠다.

0개의 댓글