헤드리스 브라우저를 사용한 웹 스크래핑: Puppeteer Tutorial

게시 됨: 2022-03-11

이 기사에서는 헤드리스 브라우저 를 사용하는 다소 비전통적인 방법으로 웹 스크래핑(웹 자동화)을 수행하는 것이 얼마나 쉬운지 확인할 것입니다.

헤드리스 브라우저란 무엇이며 왜 필요한가요?

지난 몇 년 동안 웹은 베어 HTML과 CSS로 구축된 단순한 웹사이트에서 진화했습니다. 이제 종종 Angular 또는 React와 같은 프레임워크로 빌드되는 아름다운 UI가 있는 훨씬 더 많은 대화형 웹 앱이 있습니다. 다시 말해, 오늘날 JavaScript는 웹 사이트에서 상호 작용하는 거의 모든 것을 포함하여 웹을 지배합니다.

우리의 목적을 위해 JavaScript는 클라이언트 측 언어입니다. 서버는 HTML 응답에 삽입된 JavaScript 파일 또는 스크립트를 반환하고 브라우저는 이를 처리합니다. 이제 우리가 일종의 웹 스크래핑 또는 웹 자동화를 수행하는 경우 문제가 됩니다. 왜냐하면 우리가 보거나 스크래핑하려는 콘텐츠가 실제로 JavaScript 코드에 의해 렌더링되고 원시 HTML 응답에서 액세스할 수 없기 때문입니다. 서버가 제공하는 것입니다.

위에서 언급했듯이 브라우저 JavaScript를 처리하고 아름다운 웹 페이지를 렌더링하는 방법을 알고 있습니다. 이제 스크래핑 요구 사항에 대해 이 기능을 활용할 수 있고 프로그래밍 방식으로 브라우저를 제어할 수 있는 방법이 있다면 어떨까요? 이것이 바로 헤드리스 브라우저 자동화가 시작되는 곳입니다!

목이 없는? 실례합니다? 예, 이것은 그래픽 사용자 인터페이스(GUI)가 없다는 것을 의미합니다. 예를 들어 마우스나 터치 장치를 사용하여 평소처럼 시각적 요소와 상호 작용하는 대신 CLI(명령줄 인터페이스)를 사용하여 사용 사례를 자동화합니다.

헤드리스 크롬과 퍼피티어

Zombie.js 또는 Selenium을 사용하는 헤드리스 Firefox와 같이 헤드리스 브라우징에 사용할 수 있는 웹 스크래핑 도구가 많이 있습니다. 그러나 오늘 우리는 2018년 초에 출시된 비교적 새로운 플레이어인 Puppeteer를 통해 헤드리스 Chrome을 탐색할 것입니다 . 편집자 주: 또 다른 새로운 플레이어인 Intoli의 원격 브라우저를 언급할 가치가 있지만 다른 플레이어의 주제가 되어야 합니다. 기사.

Puppeteer는 정확히 무엇입니까? 헤드리스 Chrome 또는 Chromium을 제어하거나 DevTools 프로토콜과 상호 작용하기 위해 고급 API를 제공하는 Node.js 라이브러리입니다. Chrome DevTools 팀과 멋진 오픈 소스 커뮤니티에서 유지 관리합니다.

이야기로 충분합니다. 이제 코드로 뛰어 들어가 Puppeteer의 헤드리스 브라우징을 사용하여 웹 스크래핑을 자동화하는 방법의 세계를 탐험해 봅시다!

환경 준비

우선 컴퓨터에 Node.js 8+가 설치되어 있어야 합니다. 여기에 설치할 수 있습니다. 또는 저와 같은 CLI 애호가이고 Ubuntu에서 작업하고 싶다면 다음 명령을 따르십시오.

 curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs

또한 시스템에서 사용 가능하거나 사용하지 못할 수 있는 일부 패키지가 필요합니다. 안전을 위해 다음을 설치하십시오.

 sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3

헤드리스 Chrome 및 Puppeteer 설정

Puppeteer를 npm 과 함께 설치하는 것이 좋습니다. 라이브러리와 함께 작동하는 안정적인 최신 Chromium 버전도 포함되어 있기 때문입니다.

프로젝트 루트 디렉터리에서 다음 명령을 실행합니다.

 npm i puppeteer --save

참고: Puppeteer는 백그라운드에서 Chromium을 다운로드하고 설치해야 하므로 시간이 걸릴 수 있습니다.

자, 이제 모든 설정과 구성이 완료되었으므로 즐거운 시간을 보내십시오!

자동 웹 스크래핑에 Puppeteer API 사용

기본 예제로 Puppeteer 자습서를 시작하겠습니다. 헤드리스 브라우저가 선택한 웹사이트의 스크린샷을 찍도록 하는 스크립트를 작성할 것입니다.

screenshot.js 라는 프로젝트 디렉토리에 새 파일을 만들고 즐겨 사용하는 코드 편집기에서 엽니다.

먼저 스크립트에서 Puppeteer 라이브러리를 가져오겠습니다.

 const puppeteer = require('puppeteer');

다음으로 명령줄 인수에서 URL을 가져옵니다.

 const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }

이제 Puppeteer는 약속 기반 라이브러리라는 점을 명심해야 합니다. Puppeteer는 내부에서 헤드리스 Chrome 인스턴스에 대한 비동기 호출을 수행합니다. async/await를 사용하여 코드를 깔끔하게 유지합시다. 이를 위해 먼저 async 함수를 정의하고 여기에 모든 Puppeteer 코드를 넣어야 합니다.

 async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

전체적으로 최종 코드는 다음과 같습니다.

 const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

프로젝트의 루트 디렉터리에서 다음 명령을 실행하여 실행할 수 있습니다.

 node screenshot.js https://github.com

잠깐, 붐! 헤드리스 브라우저는 screenshot.png .png라는 파일을 생성했고 그 안에 렌더링된 GitHub 홈페이지를 볼 수 있습니다. 좋습니다. 작동하는 Chrome 웹 스크레이퍼가 있습니다!

잠시 멈추고 위의 run() 함수에서 어떤 일이 발생하는지 살펴보겠습니다.

먼저 새로운 헤드리스 브라우저 인스턴스를 시작한 다음 새 페이지(탭)를 열고 명령줄 인수에 제공된 URL로 이동합니다. 마지막으로 스크린샷을 찍을 때 Puppeteer의 기본 제공 방법을 사용하며 저장해야 하는 경로만 제공하면 됩니다. 또한 자동화가 완료된 후 헤드리스 브라우저를 닫아야 합니다.

이제 기본 사항을 다루었으므로 좀 더 복잡한 것으로 넘어가 보겠습니다.

두 번째 인형극 스크래핑 예

Puppeteer 튜토리얼의 다음 부분에서는 Hacker News의 최신 기사를 스크랩하고 싶다고 가정해 보겠습니다.

ycombinator-scraper.js 라는 새 파일을 만들고 다음 코드 조각을 붙여넣습니다.

 const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);

좋습니다. 이전 예제와 비교하여 여기에 더 많은 작업이 있습니다.

가장 먼저 알 수 있는 것은 run() 함수가 이제 약속을 반환하므로 async 접두사가 약속 함수의 정의로 이동되었다는 것입니다.

또한 모든 코드를 try-catch 블록으로 래핑하여 약속을 거부하는 오류를 처리할 수 있습니다.

마지막으로, evaluate() 라는 Puppeteer의 내장 메서드를 사용하고 있습니다. 이 방법을 사용하면 DevTools 콘솔에서 실행하는 것처럼 사용자 정의 JavaScript 코드를 실행할 수 있습니다. 해당 함수에서 반환된 모든 것은 약속에 의해 해결됩니다. 이 방법은 정보를 스크랩하거나 사용자 지정 작업을 수행할 때 매우 편리합니다.

evaluate() 메서드에 전달된 코드는 https://news.ycombinator.com/에서 볼 수 있는 스토리 URL을 나타내는 urltext 필드가 있는 개체 배열을 구축하는 매우 기본적인 JavaScript입니다.

스크립트의 출력은 다음과 같습니다(하지만 원래 30개 항목이 있음).

 [ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]

꽤 깔끔해요!

좋아, 앞으로 나아가자. 반품된 항목은 30개에 불과했지만 사용 가능한 항목은 더 많습니다. 다른 페이지에 있을 뿐입니다. 결과의 다음 페이지를 로드하려면 "더보기" 버튼을 클릭해야 합니다.

페이지 매김 지원을 추가하기 위해 스크립트를 약간 수정해 보겠습니다.

 const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

여기에서 수행한 작업을 검토해 보겠습니다.

  1. 주요 run() 함수에 pagesToScrape 라는 단일 인수를 추가했습니다. 이것을 사용하여 스크립트가 스크랩할 페이지 수를 제한합니다.
  2. 현재 보고 있는 결과 페이지 수를 나타내는 currentPage 라는 새로운 변수가 하나 더 있습니다. 처음에는 1 로 설정되어 있습니다. 또한 evaluate() 함수를 while 루프로 래핑하여 currentPagepagesToScrape 보다 작거나 같은 한 계속 실행되도록 했습니다.
  3. while 루프를 다시 시작하기 전에 새 페이지로 이동하고 페이지가 로드되기를 기다리는 블록을 추가했습니다.

헤드리스 브라우저가 "더보기" 버튼을 클릭하도록 하기 위해 page.click() 메서드를 사용했음을 알 수 있습니다. 또한 waitForSelector() 메서드를 사용하여 페이지 내용이 로드될 때까지 논리가 일시 중지되었는지 확인했습니다.

둘 다 즉시 사용할 수 있는 고급 Puppeteer API 메서드입니다.

Puppeteer로 스크래핑하는 동안 발생할 수 있는 문제 중 하나는 페이지가 로드되기를 기다리는 것입니다. Hacker News는 비교적 간단한 구조를 가지고 있으며 페이지 로드가 완료될 때까지 기다리는 것이 상당히 쉬웠습니다. 더 복잡한 사용 사례의 경우 Puppeteer는 GitHub의 API 설명서에서 탐색할 수 있는 다양한 기본 제공 기능을 제공합니다.

이것은 모두 매우 훌륭하지만 Puppeteer 튜토리얼은 아직 최적화를 다루지 않았습니다. Puppeteer를 더 빠르게 실행하는 방법을 살펴보겠습니다.

Puppeteer 스크립트 최적화하기

일반적인 아이디어는 헤드리스 브라우저가 추가 작업을 수행하지 않도록 하는 것입니다. 여기에는 이미지 로드, CSS 규칙 적용, XHR 요청 실행 등이 포함될 수 있습니다.

다른 도구와 마찬가지로 Puppeteer의 최적화는 정확한 사용 사례에 따라 달라지므로 이러한 아이디어 중 일부는 프로젝트에 적합하지 않을 수 있습니다. 예를 들어 첫 번째 예에서 이미지 로드를 피했다면 스크린샷이 원하는 대로 표시되지 않았을 수 있습니다.

어쨌든 이러한 최적화는 첫 번째 요청에서 자산을 캐싱하거나 웹사이트에서 시작된 HTTP 요청을 완전히 취소하여 수행할 수 있습니다.

먼저 캐싱이 어떻게 작동하는지 봅시다.

새로운 헤드리스 브라우저 인스턴스를 시작하면 Puppeteer가 해당 프로필에 대한 임시 디렉터리를 생성한다는 점에 유의해야 합니다. 브라우저가 닫히면 제거되고 새 인스턴스를 시작할 때 사용할 수 없습니다. 따라서 저장된 모든 이미지, CSS, 쿠키 및 기타 개체에 더 이상 액세스할 수 없습니다.

Puppeteer가 쿠키 및 캐시와 같은 데이터를 저장하기 위해 사용자 지정 경로를 사용하도록 강제할 수 있습니다. 이 경로는 만료되거나 수동으로 삭제될 때까지 다시 실행할 때마다 재사용됩니다.

 const browser = await puppeteer.launch({ userDataDir: './data', });

많은 CSS와 이미지가 첫 번째 요청 시 데이터 디렉토리에 캐시되고 Chrome에서 이를 반복해서 다운로드할 필요가 없기 때문에 성능이 크게 향상될 것입니다.

그러나 이러한 자산은 페이지를 렌더링할 때 계속 사용됩니다. Y Combinator 뉴스 기사를 스크랩할 때 이미지를 포함한 시각적 요소에 대해 걱정할 필요가 없습니다. 우리는 베어 HTML 출력에만 관심이 있으므로 모든 요청을 차단하도록 합시다.

운 좋게도 Puppeteer는 사용자 정의 후크를 지원하기 때문에 이 경우 작업하기에 매우 좋습니다. 모든 요청에 ​​대해 인터셉터를 제공하고 실제로 필요하지 않은 요청은 취소할 수 있습니다.

인터셉터는 다음과 같이 정의할 수 있습니다.

 await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });

보시다시피 시작되는 요청을 완전히 제어할 수 있습니다. resourceType 에 따라 특정 요청을 허용하거나 중단하는 사용자 정의 로직을 작성할 수 있습니다. 또한 request.url 과 같은 다른 많은 데이터에 액세스할 수 있으므로 원하는 경우 특정 URL만 차단할 수 있습니다.

위의 예에서는 리소스 유형이 "document" 인 요청만 필터를 통과하도록 허용합니다. 즉, 원본 HTML 응답 외에 모든 이미지, CSS 및 기타 모든 것을 차단합니다.

최종 코드는 다음과 같습니다.

 const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

속도 제한으로 안전 유지

헤드리스 브라우저는 매우 강력한 도구입니다. 거의 모든 종류의 웹 자동화 작업을 수행할 수 있으며 Puppeteer를 사용하면 이 작업을 훨씬 더 쉽게 수행할 수 있습니다. 모든 가능성에도 불구하고 우리는 시스템을 남용하지 않도록 웹사이트의 서비스 약관을 준수해야 합니다.

이 측면은 아키텍처와 더 관련이 있으므로 이 Puppeteer 자습서에서 자세히 다루지 않겠습니다. 즉, Puppeteer 스크립트의 속도를 늦추는 가장 기본적인 방법은 여기에 sleep 명령을 추가하는 것입니다.

js await page.waitFor(5000);

이 명령문은 스크립트를 5초(5000ms) 동안 잠자기 상태로 만듭니다. browser.close() 전에 아무 곳에나 넣을 수 있습니다.

타사 서비스 사용을 제한하는 것과 마찬가지로 Puppeteer 사용을 제어하는 ​​더 강력한 방법이 많이 있습니다. 한 가지 예는 제한된 수의 작업자로 대기열 시스템을 구축하는 것입니다. Puppeteer를 사용하고 싶을 때마다 새 작업을 대기열에 푸시하지만 해당 작업에 대해 작업할 수 있는 작업자는 제한되어 있습니다. 이는 타사 API 속도 제한을 처리할 때 상당히 일반적인 방법이며 Puppeteer 웹 데이터 스크래핑에도 적용할 수 있습니다.

빠르게 움직이는 웹에서 꼭두각시 인형의 위치

이 Puppeteer 튜토리얼에서는 웹 스크래핑 도구로서의 기본 기능을 시연했습니다. 그러나 헤드리스 브라우저 테스트, PDF 생성 및 성능 모니터링을 비롯한 훨씬 더 광범위한 사용 사례가 있습니다.

웹 기술은 빠르게 발전하고 있습니다. 일부 웹 사이트는 JavaScript 렌더링에 너무 의존하여 웹 사이트를 스크랩하거나 일종의 자동화를 수행하기 위해 간단한 HTTP 요청을 실행하는 것이 거의 불가능해졌습니다. 다행스럽게도 Puppeteer와 같은 프로젝트와 그 뒤에 있는 멋진 팀 덕분에 헤드리스 브라우저가 모든 자동화 요구 사항을 처리하기 위해 점점 더 액세스할 수 있게 되었습니다!