Node.js와 Puppeteer를 사용한 동적 웹사이트의 윤리적 스크래핑 가이드

게시 됨: 2022-03-10
빠른 요약 ↬ 많은 웹 스크래핑 작업의 경우 HTTP 클라이언트는 페이지의 데이터를 추출하기에 충분합니다. 그러나 동적 웹사이트의 경우 헤드리스 브라우저가 필수가 되는 경우가 있습니다. 이 튜토리얼에서는 Node.js와 Puppeteer를 기반으로 동적 웹사이트를 스크랩할 수 있는 웹 스크레이퍼를 빌드합니다.

웹 스크래핑이 실제로 무엇을 의미하는지에 대한 짧은 섹션부터 시작하겠습니다. 우리 모두는 일상 생활에서 웹 스크래핑을 사용합니다. 그것은 단지 웹사이트에서 정보를 추출하는 과정을 설명합니다. 따라서 인터넷에서 좋아하는 국수 요리의 레시피를 복사하여 개인 노트북에 붙여 넣으면 웹 스크래핑 을 수행하는 것입니다.

소프트웨어 업계에서 이 용어를 사용할 때 일반적으로 소프트웨어를 사용하여 이 수동 작업의 자동화를 참조합니다. 이전의 "국수 요리" 예에 충실하면 이 프로세스에는 일반적으로 두 단계가 포함됩니다.

  • 페이지 가져오기
    먼저 전체 페이지를 다운로드해야 합니다. 이 단계는 수동으로 스크랩할 때 웹 브라우저에서 페이지를 여는 것과 같습니다.
  • 데이터 파싱
    이제 웹사이트의 HTML에서 레시피를 추출하여 JSON이나 XML과 같은 기계가 읽을 수 있는 형식으로 변환해야 합니다.

과거에는 많은 회사에서 데이터 컨설턴트로 일했습니다. 몇 줄의 코드로 쉽게 자동화할 수 있음에도 불구하고 얼마나 많은 데이터 추출, 집계 및 보강 작업이 여전히 수동으로 수행되는지 보고 놀랐습니다. 그것이 바로 웹 스크래핑이 제게 의미하는 바입니다. 웹사이트에서 가치 있는 정보를 추출하고 표준화 하여 또 다른 가치 창출 비즈니스 프로세스를 촉진하는 것입니다.

이 기간 동안 회사에서 모든 종류의 사용 사례에 웹 스크래핑을 사용하는 것을 보았습니다. 투자 회사는 주로 금융 투자를 뒷받침하기 위해 제품 리뷰 , 가격 정보 또는 소셜 미디어 게시물과 같은 대체 데이터를 수집하는 데 중점을 두었습니다.

여기 한 가지 예가 있습니다. 한 클라이언트가 평가, 리뷰 작성자의 위치, 제출된 각 리뷰의 리뷰 텍스트를 포함하여 여러 전자 상거래 웹사이트의 광범위한 제품 목록에 대한 제품 리뷰 데이터를 긁어모으기 위해 저에게 접근했습니다. 결과 데이터를 통해 고객은 다양한 시장에서 제품의 인기도에 대한 추세를 파악할 수 있었습니다. 이것은 겉보기에 "쓸모 없는" 단일 정보가 더 많은 양과 비교할 때 어떻게 가치가 있는지를 보여주는 훌륭한 예입니다.

다른 회사는 리드 생성 을 위해 웹 스크래핑을 사용하여 판매 프로세스를 가속화합니다. 이 프로세스에는 일반적으로 주어진 웹사이트 목록에 대한 전화번호, 이메일 주소 및 연락처 이름과 같은 연락처 정보를 추출하는 작업이 포함됩니다. 이 작업을 자동화하면 영업 팀이 잠재 고객에게 접근하는 데 더 많은 시간을 할애할 수 있습니다. 따라서 판매 프로세스의 효율성이 증가합니다.

점프 후 더! 아래에서 계속 읽기 ↓

규칙에 충실

일반적으로 Linkedin 대 HiQ 사례의 관할권에서 확인된 바와 같이 공개적으로 사용 가능한 데이터를 웹 스크래핑하는 것은 합법적입니다. 그러나 나는 새로운 웹 스크래핑 프로젝트를 시작할 때 고수하고 싶은 윤리적인 규칙을 스스로 설정했습니다. 여기에는 다음이 포함됩니다.

  • robots.txt 파일을 확인 중입니다.
    일반적으로 페이지 소유자가 로봇 및 스크레이퍼가 액세스할 수 있는 페이지 소유자에 대한 명확한 정보를 포함하고 액세스해서는 안 되는 섹션을 강조 표시합니다.
  • 이용약관 읽기.
    robots.txt와 비교할 때 이 정보는 덜 자주 사용되지는 않지만 일반적으로 데이터 스크레이퍼를 처리하는 방법을 설명합니다.
  • 적당한 속도로 긁습니다.
    스크래핑은 대상 사이트의 인프라에 서버 부하를 생성합니다. 긁는 대상과 스크레이퍼가 작동하는 동시성 수준에 따라 트래픽이 대상 사이트의 서버 인프라에 문제를 일으킬 수 있습니다. 물론 서버 용량은 이 방정식에서 큰 역할을 합니다. 따라서 내 스크래퍼의 속도는 항상 내가 스크래핑하려는 데이터의 양과 대상 사이트의 인기도 사이의 균형입니다. 이 균형을 찾는 것은 "계획된 속도가 사이트의 유기적 트래픽을 크게 변화시킬 것인가?"라는 단일 질문에 답함으로써 달성할 수 있습니다. 사이트의 자연 트래픽 양이 확실하지 않은 경우 ahrefs와 같은 도구를 사용하여 대략적인 아이디어를 얻습니다.

올바른 기술 선택

실제로 헤드리스 브라우저로 스크래핑하는 것은 인프라에 큰 영향을 미치기 때문에 사용할 수 있는 가장 성능이 떨어지는 기술 중 하나입니다. 컴퓨터 프로세서의 하나의 코어는 대략 하나의 Chrome 인스턴스를 처리할 수 있습니다.

실제 웹 스크래핑 프로젝트에서 이것이 의미하는 바를 알아보기 위해 간단한 예제 계산 을 수행해 보겠습니다.

대본

  • 20,000개의 URL을 스크랩하려고 합니다.
  • 대상 사이트의 평균 응답 시간은 6초입니다.
  • 서버에는 2개의 CPU 코어가 있습니다.

프로젝트를 완료하는 데 16시간 이 걸립니다.

그래서 저는 동적 웹사이트에 대한 스크래핑 타당성 테스트를 할 때 항상 브라우저를 사용하지 않으려고 노력합니다.

다음은 내가 항상 통과하는 작은 체크리스트입니다.

  • URL의 GET 매개변수를 통해 필수 페이지 상태를 강제할 수 있습니까? 그렇다면 추가된 매개변수로 HTTP 요청을 간단히 실행할 수 있습니다.
  • 페이지 소스의 동적 정보 부분이 DOM의 어딘가에 있는 JavaScript 개체를 통해 사용할 수 있습니까? 그렇다면 일반 HTTP 요청을 다시 사용하고 문자열화된 개체의 데이터를 구문 분석할 수 있습니다.
  • XHR 요청을 통해 데이터를 가져오나요? 그렇다면 HTTP 클라이언트를 사용하여 엔드포인트에 직접 액세스할 수 있습니까? 그렇다면 HTTP 요청을 엔드포인트에 직접 보낼 수 있습니다. 많은 경우 응답이 JSON 형식으로 지정되어 삶이 훨씬 쉬워집니다.

모든 질문에 확실한 "아니오"로 대답하면 공식적으로 HTTP 클라이언트를 사용할 수 있는 옵션이 모두 소진됩니다. 물론 시도할 수 있는 사이트별 조정이 더 있을 수 있지만 일반적으로 헤드리스 브라우저의 느린 성능에 비해 이를 파악하는 데 필요한 시간이 너무 높습니다. 브라우저로 긁는 것의 장점은 다음과 같은 기본 규칙이 적용되는 모든 것을 긁을 수 있다는 것입니다.

브라우저로 접근할 수 있다면 스크랩해도 됩니다.

다음 사이트를 스크레이퍼의 예로 들어 보겠습니다. https://quotes.toscrape.com/search.aspx. 그것은 주제 목록에 대한 주어진 저자 목록에서 인용을 제공합니다. 모든 데이터는 XHR을 통해 가져옵니다.

동적으로 렌더링된 데이터가 있는 웹사이트
동적으로 렌더링된 데이터가 있는 예제 웹사이트. (큰 미리보기)

사이트의 기능을 자세히 살펴보고 위의 체크리스트를 살펴본 사람은 따옴표 끝점에서 직접 POST 요청을 만들어 검색할 수 있기 때문에 HTTP 클라이언트를 사용하여 따옴표를 실제로 스크랩할 수 있다는 것을 깨달았을 것입니다. 그러나 이 튜토리얼은 Puppeteer를 사용하여 웹사이트를 스크랩하는 방법을 다루기로 되어 있기 때문에 이것이 불가능한 척 할 것입니다.

전제 조건 설치

Node.js를 사용하여 모든 것을 빌드할 것이므로 먼저 새 폴더를 만들고 열고 내부에 다음 명령을 실행하여 새 Node 프로젝트를 만듭니다.

 mkdir js-webscraper cd js-webscraper npm init

npm이 이미 설치되어 있는지 확인하십시오. 설치 프로그램은 이 프로젝트에 대한 메타 정보에 대해 몇 가지 질문을 할 것입니다. Enter 키를 누르면 모두 건너뛸 수 있습니다.

Puppeteer 설치

우리는 이전에 브라우저로 긁는 것에 대해 이야기했습니다. Puppeteer는 프로그래밍 방식으로 헤드리스 Chrome 인스턴스 와 대화할 수 있는 Node.js API입니다.

npm을 사용하여 설치해 보겠습니다.

 npm install puppeteer

스크레이퍼 만들기

이제 scraper.js 라는 새 파일을 만들어 스크레이퍼를 빌드해 보겠습니다.

먼저 이전에 설치된 라이브러리인 Puppeteer를 가져옵니다.

 const puppeteer = require('puppeteer');

다음 단계로 Puppeteer에게 비동기 및 자체 실행 기능 내에서 새 브라우저 인스턴스를 열도록 지시합니다.

 (async function scrape() { const browser = await puppeteer.launch({ headless: false }); // scraping logic comes here… })();

참고 : 기본적으로 헤드리스 모드는 꺼져 있으므로 성능이 향상됩니다. 그러나 새 스크레이퍼를 만들 때 헤드리스 모드를 끄는 것을 좋아합니다. 이를 통해 브라우저가 진행 중인 프로세스를 따르고 렌더링된 모든 콘텐츠를 볼 수 있습니다. 이것은 나중에 스크립트를 디버그하는 데 도움이 됩니다.

열린 브라우저 인스턴스 내에서 이제 새 페이지를 열고 대상 URL로 이동합니다.

 const page = await browser.newPage(); await page.goto('https://quotes.toscrape.com/search.aspx');

비동기 함수의 일부로 await 문을 사용하여 다음 코드 줄을 진행하기 전에 다음 명령이 실행될 때까지 기다립니다.

이제 브라우저 창을 성공적으로 열고 페이지로 이동했으므로 원하는 정보를 스크랩할 수 있도록 웹사이트의 상태를 만들어야 합니다 .

사용 가능한 주제는 선택한 작성자에 대해 동적으로 생성됩니다. 따라서 먼저 'Albert Einstein'을 선택하고 생성된 주제 목록을 기다립니다. 목록이 완전히 생성되면 '학습'을 주제로 선택하고 두 번째 양식 매개변수로 선택합니다. 그런 다음 제출을 클릭하고 결과가 들어 있는 컨테이너에서 검색된 인용문을 추출합니다.

이제 이것을 JavaScript 로직으로 변환할 것이므로 이전 단락에서 이야기한 모든 요소 선택기의 목록을 먼저 만들어 보겠습니다.

작성자 선택 필드 #author
태그 선택 필드 #tag
제출 버튼 input[type="submit"]
견적 컨테이너 .quote

페이지와 상호 작용을 시작하기 전에 스크립트에 다음 줄을 추가하여 액세스할 모든 요소가 표시되는지 확인합니다.

 await page.waitForSelector('#author'); await page.waitForSelector('#tag');

다음으로 두 개의 선택 필드에 대한 값을 선택합니다.

 await page.select('select#author', 'Albert Einstein'); await page.select('select#tag', 'learning');

이제 페이지에서 "검색" 버튼을 눌러 검색을 수행할 준비가 되었으며 견적이 표시될 때까지 기다립니다.

 await page.click('.btn'); await page.waitForSelector('.quote');

이제 페이지의 HTML DOM 구조에 액세스할 것이므로 제공된 page.evaluate() 함수를 호출하고 따옴표가 들어 있는 컨테이너를 선택합니다(이 경우 하나만). 그런 다음 객체를 만들고 각 object 매개변수에 대한 폴백 값으로 null을 정의합니다.

 let quotes = await page.evaluate(() => { let quotesElement = document.body.querySelectorAll('.quote'); let quotes = Object.values(quotesElement).map(x => { return { author: x.querySelector('.author').textContent ?? null, quote: x.querySelector('.content').textContent ?? null, tag: x.querySelector('.tag').textContent ?? null, }; }); return quotes; });

모든 결과를 로깅하여 콘솔에 표시할 수 있습니다.

 console.log(quotes);

마지막으로 브라우저를 닫고 catch 문을 추가해 보겠습니다.

 await browser.close();

전체 스크레이퍼는 다음과 같습니다.

 const puppeteer = require('puppeteer'); (async function scrape() { const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); await page.goto('https://quotes.toscrape.com/search.aspx'); await page.waitForSelector('#author'); await page.select('#author', 'Albert Einstein'); await page.waitForSelector('#tag'); await page.select('#tag', 'learning'); await page.click('.btn'); await page.waitForSelector('.quote'); // extracting information from code let quotes = await page.evaluate(() => { let quotesElement = document.body.querySelectorAll('.quote'); let quotes = Object.values(quotesElement).map(x => { return { author: x.querySelector('.author').textContent ?? null, quote: x.querySelector('.content').textContent ?? null, tag: x.querySelector('.tag').textContent ?? null, } }); return quotes; }); // logging results console.log(quotes); await browser.close(); })();

다음을 사용하여 스크레이퍼를 실행해 보겠습니다.

 node scraper.js

그리고 우리는 간다! 스크레이퍼는 예상대로 인용 객체를 반환합니다.

웹 스크레이퍼의 결과
웹 스크레이퍼의 결과. (큰 미리보기)

고급 최적화

이제 기본 스크레이퍼가 작동합니다. 좀 더 심각한 스크래핑 작업을 준비하기 위해 몇 가지 개선 사항을 추가해 보겠습니다.

사용자 에이전트 설정

기본적으로 Puppeteer는 HeadlessChrome 문자열이 포함된 사용자 에이전트를 사용합니다. 꽤 많은 웹사이트가 이런 종류의 서명을 찾고 그러한 서명으로 들어오는 요청을 차단합니다 . 스크레이퍼가 실패할 수 있는 잠재적인 원인이 되는 것을 피하기 위해 저는 항상 다음 줄을 코드에 추가하여 사용자 지정 사용자 에이전트를 설정합니다.

 await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4298.0 Safari/537.36');

이것은 가장 일반적인 상위 5개 사용자 에이전트의 배열에서 각 요청과 함께 임의의 사용자 에이전트를 선택하여 더욱 향상될 수 있습니다. 가장 일반적인 사용자 에이전트 목록은 가장 일반적인 사용자 에이전트에서 찾을 수 있습니다.

프록시 구현

Puppeteer는 다음과 같이 실행 시 Puppeteer에 프록시 주소를 전달할 수 있으므로 프록시에 매우 쉽게 연결할 수 있습니다.

 const browser = await puppeteer.launch({ headless: false, args: [ '--proxy-server=<PROXY-ADDRESS>' ] });

sslproxys는 사용할 수 있는 많은 무료 프록시 목록을 제공합니다. 또는 순환 프록시 서비스를 사용할 수 있습니다. 프록시는 일반적으로 많은 고객(또는 이 경우 무료 사용자) 간에 공유되기 때문에 정상적인 상황보다 연결이 훨씬 더 불안정해집니다. 이것은 오류 처리 및 재시도 관리에 대해 이야기하기에 완벽한 순간입니다.

오류 및 재시도 관리

많은 요인으로 인해 스크레이퍼가 실패할 수 있습니다. 따라서 오류를 처리하고 오류가 발생한 경우 어떻게 해야 하는지 결정하는 것이 중요합니다. 스크레이퍼를 프록시에 연결했고 연결이 불안정할 것으로 예상하기 때문에(특히 무료 프록시를 사용하기 때문에) 포기하기 전에 네 번 재시도 하고 싶습니다.

또한 이전에 실패한 경우 동일한 IP 주소로 요청을 다시 시도하는 것은 의미가 없습니다. 따라서 우리는 작은 프록시 회전 시스템 을 구축할 것입니다.

우선, 우리는 두 개의 새로운 변수를 생성합니다:

 let retry = 0; let maxRetries = 5;

scraping scrape() 함수를 실행할 때마다 retry 변수를 1씩 늘립니다. 그런 다음 오류를 처리할 수 있도록 try 및 catch 문으로 전체 스크래핑 논리를 래핑합니다. 재시도 관리는 catch 함수 내에서 발생합니다.

이전 브라우저 인스턴스가 닫히고 재시도 변수가 maxRetries 변수보다 작으면 스크래핑 함수가 재귀적으로 호출됩니다.

이제 스크레이퍼는 다음과 같이 보일 것입니다.

 const browser = await puppeteer.launch({ headless: false, args: ['--proxy-server=' + proxy] }); try { const page = await browser.newPage(); … // our scraping logic } catch(e) { console.log(e); await browser.close(); if (retry < maxRetries) { scrape(); } };

이제 앞에서 언급한 프록시 회전자를 추가해 보겠습니다.

먼저 프록시 목록을 포함하는 배열을 생성해 보겠습니다.

 let proxyList = [ '202.131.234.142:39330', '45.235.216.112:8080', '129.146.249.135:80', '148.251.20.79' ];

이제 배열에서 임의의 값을 선택합니다.

 var proxy = proxyList[Math.floor(Math.random() * proxyList.length)];

이제 Puppeteer 인스턴스와 함께 동적으로 생성된 프록시를 실행할 수 있습니다.

 const browser = await puppeteer.launch({ headless: false, args: ['--proxy-server=' + proxy] });

물론 이 프록시 로테이터는 죽은 프록시에 플래그를 지정하도록 더 최적화될 수 있지만 이것은 확실히 이 튜토리얼의 범위를 벗어납니다.

이것은 스크레이퍼의 코드입니다(모든 개선 사항 포함):

 const puppeteer = require('puppeteer'); // starting Puppeteer let retry = 0; let maxRetries = 5; (async function scrape() { retry++; let proxyList = [ '202.131.234.142:39330', '45.235.216.112:8080', '129.146.249.135:80', '148.251.20.79' ]; var proxy = proxyList[Math.floor(Math.random() * proxyList.length)]; console.log('proxy: ' + proxy); const browser = await puppeteer.launch({ headless: false, args: ['--proxy-server=' + proxy] }); try { const page = await browser.newPage(); await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4298.0 Safari/537.36'); await page.goto('https://quotes.toscrape.com/search.aspx'); await page.waitForSelector('select#author'); await page.select('select#author', 'Albert Einstein'); await page.waitForSelector('#tag'); await page.select('select#tag', 'learning'); await page.click('.btn'); await page.waitForSelector('.quote'); // extracting information from code let quotes = await page.evaluate(() => { let quotesElement = document.body.querySelectorAll('.quote'); let quotes = Object.values(quotesElement).map(x => { return { author: x.querySelector('.author').textContent ?? null, quote: x.querySelector('.content').textContent ?? null, tag: x.querySelector('.tag').textContent ?? null, } }); return quotes; }); console.log(quotes); await browser.close(); } catch (e) { await browser.close(); if (retry < maxRetries) { scrape(); } } })();

짜잔! 터미널 내부에서 스크레이퍼를 실행하면 따옴표가 반환됩니다.

인형극의 대안으로서의 극작가

Puppeteer는 Google에서 개발했습니다. 2020년 초 Microsoft는 Playwright라는 대안을 출시했습니다. Microsoft는 Puppeteer-Team에서 많은 엔지니어를 헤드헌팅했습니다. 따라서 Playwright는 이미 Puppeteer에서 작업한 많은 엔지니어에 의해 개발되었습니다. 블로그에 새로 등장한 것 외에도 Playwright의 가장 큰 차별화 포인트는 Chromium, Firefox 및 WebKit(Safari)를 지원하는 크로스 브라우저 지원입니다.

성능 테스트(Checkly에서 수행한 것과 같은)에 따르면 Puppeteer는 일반적으로 Playwright와 비교하여 약 30% 더 나은 성능을 제공합니다.

하나의 브라우저 인스턴스로 여러 장치를 실행할 수 있다는 사실과 같은 다른 차이점은 웹 스크래핑의 맥락에서 실제로 가치가 없습니다.

리소스 및 추가 링크

  • 인형극 문서
  • 인형극 및 극작가 배우기
  • Zenscrape의 Javascript를 사용한 웹 스크래핑
  • 가장 일반적인 사용자 에이전트
  • 인형극 대 극작가