puppeteer를 이용한 웹 크롤링 (1)

lin·2022년 7월 22일
0

웹크롤링

목록 보기
2/2
post-thumbnail

1주차 교육 예제는 !!
대한약사회에서 운영하는 휴일지킴이약국이라는 사이트를 통해 약국 정보 추출 입니다

해당 사이트에서 약국 리스트를 추출하기 위해서 해야하는 단계는 아래와 같다.

  1. 시,도 선택 (click event)
  2. 군,구 선택 (click event)
  3. 페이지 수 확인
  4. 이후 페이지 이동하며 약국 리스트 추출
  5. 해당 데이터를 JSON 파일로 저장

혼자 하는 생각 - 활용, 재가공하여 사용하지말자

제5조 (게시물의 저작권)
휴일지킴이약국 홈페이지(www.pharm114.or.kr)에서 제공되는 "휴일지킴이약국정보"는 국민의 의약품 접근성 확대를 위해 "약사회"
에서 제공하는 정보입니다.
다만, "휴일지킴이약국정보"의 수집, 가공, 이용 등 관리에 대한 저작권은 게시자인 "약사회"에 있으므로 게시된 정보 및 검색
결과를 수집하여 획득한 데이터를 게시자의 사전동의 없이 영리 또는 비영리 목적으로 활용 또는 재가공하여 사용할 수
없습니다.


📒 시,도 선택

사이트에 접속해서 페이지 소스를 보면 ul 태그 아래 시,도가 리스트로 쭉 나와있다. class가 있기 때문에 해당 리스트를 뽑아 활용 가능할 듯 한데, 지금 당장의 목표는 시도,군구 까지 내가 지정하면 클릭해서 들어가는 정도이다.


const evalCode = async function () {
  await page.evaluate(function (sido) {
    document.querySelector(`#continents > li.${sido} > a`).click();
  }, sido);
};

page.evaluate()를 통해 브라우저 콘솔에다가 document를 이용한 명령어를 보낼 수 있다.

📒 군,구 선택

const evalCity = async function () {
  //해당 엘리먼트를 찾을 때까지 기다림
  await page.waitForSelector(`#container #continents > li.${sigungu} > a`);
  await page.evaluate(function (sigungu) {
    document
      .querySelector(`#container #continents > li.${sigungu} > a`)
      .click();
  }, sigungu);
};

페이지 로딩 중에 바로 evaluate 하면 오류가 발생한다. waitForSelector를 이용해 해당 엘리먼트를 찾을 때 까지 기다릴 수 있다.

📒 페이지 수 확인

const pageSelector = '원하는 태그 부모 선택자'
let pageLength = 0;
const getPageLength = async function () {
  await page.waitForSelector(pageSelector);

  pageLength = await page.evaluate(function (pageSelector) {
    const result = document.querySelector(pageSelector).children.length;
    return result;
  }, pageSelector);
};

해당 사이트를 확인해보면 각 페이지는 <td> 태그 안에서 <a>가 나열되어있는 형태이다.
따라서 페이지 개수를 확인하기 위해서는 부모 태그를 선택하여 자식 태그의 수를 확인하면 된다.
이후 getData()에서도 page 관련 선택자를 사용하여 이동해야하기때문에 const 변수로 사전에 미리 선언해두었다.

📒 페이지 이동하며 데이터 추출

const getData = async function () {
  //페이지 loop
  for (let i = 1; i <= pageLength; i++) {
    
    await page.waitForSelector(pageSelector);

    const infoArr = await page.evaluate(
      function (i, sido, sigungu) {
        //약국 정보 테이블의 각 열을 뽑아오는 구문
        var trArrr = document.querySelectorAll(
          "#printZone > table:nth-child(2) > tbody tr"
        );
        var returnData = [];

        for (var i = 0; i < trArrr.length; i++) {
          var currentTr = trArrr[i];

          var name = currentTr
            .querySelector("td")
            ?.innerText.replaceAll("\n", "")
            .replaceAll("\t", "");
          var address = currentTr
            .querySelectorAll("td")[2]
            ?.innerText.replaceAll("\n", "")
            .replaceAll("\t", "");
          var tel = currentTr
            .querySelectorAll("td")[3]
            ?.innerText.replaceAll("\n", "")
            .replaceAll("\t", "");
          var open = currentTr
            .querySelectorAll("td")[4]
            ?.innerText.replaceAll("\n", "")
            .replaceAll("\t", "");

          var jsonData = {
            name,
            address,
            tel,
            sido,
            sigungu,
            open, //변수와 이름 같으면 생략 가능
          };

          if (jsonData.address != undefined) {
            returnData.push(jsonData);
          }
        }

        return returnData;
      },
      i,
      sigungu,
      sido
    );

    finalData = finalData.concat(infoArr);
    console.log(finalData.length);

    if (pageLength != i) {
      await page.evaluate(
        function (i, pageSelector) {
          document.querySelector(pageSelector).children[i].click();
        },
        i,
        pageSelector
      );
    }
  }
  browser.close();
};

📒 JSON 파일로 저장

const writeFile = async function () {
  const stringData = JSON.stringify(finalData);
  const exist = fs.existsSync(`./json/${sido}`);
  if (!exist) {
    fs.mkdir(`./json/${sido}`, { recursive: true }, function (err) {
      console.log(err);
    });
  }
  const filePath = `./json/${sido}/${sigungu}.json`;

  await fs.writeFileSync(filePath, stringData);
};

🎉 실행결과

데이터가 제대로 추출되어 JSON 파일로 저장이 되었다!

profile
BE

0개의 댓글