JavaScirpt/Node.js

[Node.js] Starting Node.js with Crawling (page capturing, electron, css selectors)

Tree_Park 2020. 12. 9. 09:46
728x90
인자로 지정한 페이지를 캡쳐하는 도구

- 저번에 이어 이번에는 커맨드 라인에서 인자로 URL을 넘겨주어 스크린샷을 찍는 프로그램을 작성할 것이다.

- CasperJS에서는 실행 시 지정한 인자가 casper.cli.args에 배열 형식으로 들어간다.

코드

// Capturing the web page  appointed by command line argument.

const casper = require('casper').create();
const utils = require('utils');

// Getting an argument.
let args = casper.cli.args;
if(args.length <1) {
    // Printing the useage.
    casper.echo("USES:");
    casper.echo("shot-tool URL [savepath]");
    casper.exit();
}

let savepath = "screenshot.png";
let url = args[0];
if (args.length >= 2) savepath = args[1];

// Executing CasperJS processing
casper.start();
casper.viewport(1024, 768);
casper.open(url);
casper.then(function() {
   this.capture(savepath, {
       top:0, left:0, width:1024, height:768
   }); 
});
casper.run();

결과

인자로 google url을 주어 캡쳐한 화면

 - 이번에는 배치 파일(셸 스크립트)를 만들어서 쉽게 사용해보자

코드

 

#!/bin/sh
SCRIPT_DIR=`dirname $0`
/usr/local/bin/casperjs $SCRIPT_DIR/shot-tool.js $*

 - goormide는 ubuntu 기반에서, 스크립트 파일을 실행하기 위해 모드를 바꾸어 주어야 한다.

   - chmod u+x shot-tool.sh

   - shot-tool https://google.com 

 

로그인 후의 데이터를 다운로드

- 티스토리에 로그인 한 후, 내 tistory 관리 페이지의 html을 얻어올 것이다.

코드

var casper = require('casper').create();
var utils = require('utils');

// url and login info variables
var url = "https://www.tistory.com/auth/login/";
var id = "";
var password = "";

casper.start();
casper.viewport(1024, 768);
casper.open(url);

casper.then(function() {
    this.capture('hello1.png', {
       top:0, left:0, width:1024, height:768 
    });
});
casper.then(function() {
     if(this.exists('a.btn_login.link_kakao_id')) {
        this.echo('btn exists!')
        this.click('a.btn_login.link_kakao_id')
   }
})
casper.then(function() {
   this.capture('hello.png', {
       top:0, left:0, width:1024, height:768 
    }); 
});
casper.then(function() {
 casper.sendKeys('input.tf_g.tf_email', '*************@***.***'); 
casper.sendKeys('input#id_password_3', '**********************888');
});

casper.then(function() {
    this.click('button.btn_g.btn_confirm.submit');
});

casper.then(function() {
    this.capture('hello2.png', {
       top:0, left:0, width:1000, height:1000 
    });
})

casper.then(function() {
    console.log(this.getCurrentUrl());
})
casper.then(function() {
    this.wait(5000, function(){this.echo('waiting')});
});

casper.then(function() {
    this.capture('hello3.png', {
       top:0, left:0, width:1000, height:1000 
    });
})

casper.then(function() {
    casper.open('https://devel-up-tree.tistory.com/manage/');    
})

casper.then(function() {
    this.capture('hello4.png', {
       top:0, left:0, width:1000, height:1000 
    });
    this.echo(this.getPageContent());
});

casper.run();

 

결과

hello.png
hello2.png
hello3.png
hello4.png

 

- 이번에는 인스타로 시험해 볼 것이다

코드

var casper = require('casper').create({
    verbose: true, 
    logLevel: 'debug',
    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.172 Safari/537.22',
    pageSettings: {
      loadImages:  false,         // The WebPage instance used by Casper will);
      loadPlugins: false}
    });
var utils = require('utils');
var fs = require('fs');

// url and login info variables
var url = "https://www.instagram.com/";
var id = "[your id or eamial]";
var pwd = "[your password]";

casper.start();
casper.viewport(1024, 768);
casper.open(url);

casper.then(function() {
    this.wait(1000, function(){this.echo('waiting login view')});
});
casper.then(function() {
    this.capture('insta1.png', {
       top:0, left:0, width:1024, height:768 
    });
});
casper.then(function(){
    casper.sendKeys('input[name=username]', id);
    casper.sendKeys('input[name=password]', pwd);
});
casper.then(function(){
    casper.click('button[type=submit]');
});

casper.waitForSelector('img[data-testid=user-avatar]', 
    function pass(){
        this.echo('resource has been loaded.');
    },
    function fail(){
        this.echo("resource has been nots loaded.");
    },
    3000
); 

casper.then(function() {
    this.open('https://www.instagram.com/accounts/onetap/');
})

casper.waitForSelector('img[data-testid=user-avatar]', 
    function pass(){
        this.echo('resource has been loaded.');
        this.click('button[type=button]:nth-child(1)');
    },
    function fail(){
        this.echo("resource has been not loaded.");
    },
    3000
); 

casper.then(function() {
    this.open('https://www.instagram.com/');
})

casper.waitForSelector('img[data-testid=user-avatar]', 
    function pass(){
        this.echo('resource has been loaded.');
        this.click('input[autocapitalize=none]');
  },
    function fail(){
        this.echo("resource has been nots loaded.");
    },
    3000
);

casper.waitForSelector('input[autocapitalize=none]', 
    function pass(){
        this.echo('resource has been loaded.');
        this.open(url + 'explore/tags/coffee');
  },
    function fail(){
        this.echo("resource has been nots loaded.");
    },
    3000
);

casper.waitForSelector('h1.K3Sf1', 
    function pass(){
        this.echo('resource has been loaded.');
        this.capture('insta2.png', {
            top:0, left:0, width:1024, height:768 
         });
  },
    function fail(){
        this.echo("resource has been nots loaded.");
    },
    3000
);

casper.then(function() {
    this.capture('insta3.png', {
       top:0, left:0, width:1024, height:768 
    });
});
casper.then(function() {
    console.log(this);
    console.dir(fs);
    fs.write("data.html", this.getPageContent(), 'w');
});

casper.run();

결과

insta1.png
insta3.png

- 페이지는 test.html 저장되었다.

 

CSS 선택자 지정 방법

- 선택자 기본 서식

서식

설명

*

모든 요소

태그명

태그명이 일치하는 요소 ex) p

.클래스명

클래스 속성 값이 일치하는 요소

#id명

id 속성 값이 일치하는 요소

 

- 선택자끼리의 관계를 지정하는 서식

서식

설명

선택자, 선택자

열거된 복수의 선택자 ex) h1, h2

선택자 선택자

하위 계층의 후손 요소 ex) div h1

선택자 > 선택자

바로 아래 계층의 자식 요소 ex) div>h1

선택자 A + 선택자 B

같은 계층에 선택자 A 바로 뒤에 있는 선택자 B 한 개 ex) h1+h2

선택자 A ~ 선택자 B

같은 계층에 선택자 A 바로 뒤에 있는 선택자 B 모두 ex) h1~h2

 

- 선택자의 속성에 따른 서식

서식

설명

요소[att]

특정 속성을 가지는 요소를 선택

요소[att=’val’]

att 속성의 값이 val인 요소, val 값이 전체 일치해야 함

요소[att~=’val’]

att 속성의 값에 val을 단어로(스페이스로 구분) 포함하는 요소

요소[att|=’val’]

att 속성의 값이 val이거나 val로 시작하고 뒤에 하이픈(-)이 있는 모든 요소

요소[att^=’val’]

att 속성의 값이 val로 시작하는 요소

요소[att$=’val’]

att 속성의 값이 val로 끝나는 요소

요소[att*=’val’]

att 속성의 값에 val을 포함하는 요소

 

- 위치나 상태를 지정하는 서식

서식

설명

:root

Document의 루트 요소

:nth-child(n)

동위 요소 중 n번째 위치한 요소

:nth-last-child(n)

동위 요소 중 뒤에서 n번째 위치한 요소

태그:nth-of-type(n)

동위 요소 중 지정한 태그 중 n번째 요소

:first-child

동위 요소 중 첫 번째 요소

:last-child

동위 요소 중 마지막 요소

태그:first-of-type(n)

동위 요소 중 지정한 태그 중 첫 번째 요소

태그:last-of-type(n)

동위 요소 중 지정한 태그 중 마지막 요소

:only-child

동위 요소 없이 한 개의 요소만 있을 때 해당 요소

태그:only-of-type

동위 요소 중 지정한 태그가 하나만 있을 때 해당 요소

:empty

내용이 빈 요소

:lang(code)

특정 언어 code로 된 요소

:not(s)

s 이외의 요소

:enabled

활성화된 UI 요소

:disabled

비활성화된 UI 요소

:checked

체크된 UI 요소

 

Electron으로 데스크톱 애플리케이션 작성

- Electron : Github의 'Atom'에서 사용하고 있는 라이브러리

  - 자바스크립트로 크로스 플랫폼 데스크톱 애플리케이션을 작성 가능

  - 쉽게 생각하면, '웹 브라우저 안에 Node.js'를 넣은 것

  - 실제로 'Chromium'과 'Node.js'를 조합해서 만들었다.

 

  - Node.js의 기능과 웹 브라우저의 기능을 모두 이용 가능.

 

- Electron의 장점과 단점

  - 장점

    - 웹 기술을 이용하여 네이티브 애플리케이션을 쉽게 만들 수 있는 것

    - Node.js의 API나 모듈을 그대로 사용 가능

 

  - 단점

    - 웹 브라우저인 Chromium을 그대로 포함해야 하므로 배포 사이즈가 커지기 때문에 간단한

      앱이라도 수십~수백 MB의 저장 공간이 필요하다.

 

- 자바스크립트로 렌더링되는 페이지로도 작성 가능하다.

  - 단순히 HTML을 다운로드하면 내용이 텅 비어있는 경우도 많지만

  - Electron을 사용하면 자바스크립트에 의한 렌더링을 한 후 콘텐츠를 추출 가능하다.

 

- Electron 설치 : npm install electron-prebuilt -g

 

- Electron으로 간단한 애플리케이션을 만드는 순서

  - 앱 설정과 메인 프로그램, 두 개의 파일을 준비

  - 두 파일을 하나의 디렉토리에 배치

  - 순서 1 : 애플리케이션에 필요한 파일 준비

    - easy-app이라는 디렉토리를 생성 후

    - pacakage.json이라는 이름으로 다음과 같은 내용의 JSON파일 생성

{
	"name": "easy-app",
	"version": "0.1.0",
	"main": "main.js"
}

    - easy-app 디렉토리 안에 다음과 같은 main.js 프로그램 작성

// Visiting wikipedia page 
const TARGET_URL = "https://ko.wikipedia.org/";

// Loading needed modules
const electron = require('electron');
const app =  electron.app;
const BrowserWindow = electron.BrowserWindow;

// Being called out event when ready.
app.on('ready', function(){
   // Creating main window
    win = new BrowserWindow({
        width: 800,
        height: 600
    });
    
    // Loading the TARGET_URL
    win.loadURL(TARGET_URL);
});

 

  - 실행 : electron ./easy-app/

 

- 직접 준비한 HTML을 Electron에 표시

  - Electron에서는 외부 사이트를 표시 가능할 뿐만 아니라, 스스로 마련한 HTML을 표시 가능하다.

코드 main.js

// 지정 URL 로드
    win.loadURL('file://' + __dirname + '/index.html');
    win.on('closed', function() {
        win = null;
    })

코드 index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Test App</title>
        <script>
            window.onload = function() {
                var info = document.getElementById("info");
                info.innerHTML =
                    "Node ver." + process.version + "<br>" +
                    "atom ver." + process.versions['electron'];
            };
        </script>
    </head>
    <body>
        <h1>test app</h1>
        <p id="info"></p>
    </body>
    
</html>

 

  - index.html 코드를 보면 일반 HTML 내의 자바스크립트에서 사용 불가능한 process 객체가 이용되고 있다.

 

- 메인 프로세스와 렌더링 프로세스 간의 통신

  - [Electron의 기본적인 흐름]

    - Electron 시작

    - 설정 파일(package.json)에 따라 메인 프로그램(자바 스크립트)을 실행

    - 메인 프로그램에서 브라우저 창을 생성

    - 브라우저 창에 임의의 HTML을 로드

 

   - 앱이 시작되면 HTML 파일 안에서도 자바스크립트가 실행

     - 메인 프로그램과 브라우저 안에서 실행되는 두 개의 JS 프로그램이 실행

     - 두 개의 프로그램이 다른 프로세스(콘텍스트)에서 실행된다는 점이 중요

     - Electron에서는 전자의 메인 프로그램을, 메인 프로세스라고 하고

     - HTML에서 실행되는 후자의 프로그램을, 렌더링 프로세스라고 한다.

 

     - 통상의 웹 브라우저에서는 보안을 위해 샌드박스 내에서 HTML이 실행된다.

     - 그래서 파일 같은 로컬 리소스에 접근 불가능하다.

     - Electron도 마찬가지로 HTML을 실행하는 렌더링 프로세스에서는 위험한 조작이 불가능하다.

     - 그러나 메인 프로세스에서는 Node.js의 API 등을 자유롭게 사용가능하다.

     - 렌더링 프로세스가 메인 프로세스에게 필요한 처리를 의뢰함으로써 웹 브라우저에서는 수행불가능

       하던 각종 처리가 가능하다.

 

     - 이를 위해 메인 프로세스와 렌더링 프로세스 간의 통신을 위한 IPC 모듈이 제공

 

 - 동기적인 IPC 통신

코드

<script>
            var electron = require('electron');
            var ipc = electron.ipcRenderer;
            
            // send values to the main process and then receive a result Synchronously.
            var res = ipc.sendSync('mul-sync', {a:30, b:2});
            alert("res=" + res);
</script>

  - sendSync() 메소드를 사용하여 메인 프로세스 측에 값을 송신하고

  - mul-sync라는 채널에 a와 b 두 개의 값을 자바스크립트 객체로 송신한다.(문자열 혹은 js객체)

 

  - mul-sync라는 채널 메시지를 받았을 때에 처리하는 이벤트 핸들러를 정의

  - event 인자의 returnValue 속성에 반환 값을 대입함으로써 렌더링 프로세스 측에 값을 전달.

 코드

<script>
            var electron = require('electron');
            var ipc = electron.ipcRenderer;
            
            // receive messages Synchronously.
            ipc.on('mul-sync', function(event, arg) {
                console.log(arg);
                event.returnValue = arg.a * arg.b;
            });
 </script>

 

 -IPC 통신을 수행하는 실제 프로그램

코드 main.js

// Loading needed modules
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const ipc = electron.ipcMain;

// Running main window.
const mainWindow = null;
// call events when ready.
app.on('ready', function(){
   // Creating main window
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600
    });
    mainWindow.loadURL('file://'+__dirname+'/index.html');
    mainWindow.on('closed', function() {
        mainWindow = null
    });
});

// Receiving messages synchronously
ipc.on('mul-sync', function(event, arg) {
    console.log(arg); // Printing a console
    // Returning to rendering process
    event.returnValue = arg.a * arg.b;
});

// Receiving message asynchronously
ipc.on('mul-async', function(event, arg) {
    console.log(arg); // Printing a console
    // Returning to rendering process
    let result = arg.a * arg.b;
    event.sender.send('mul-async-reply', result);
});

 

코드 index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Test App</title>
        <script>
            var electron = require('electron');
            var ipc = electron.ipcRenderer;
            var info;
            
            window.onload = function() {
                info = document.getElementById('info');
                testSync();
                testAsync();
            };
            
            // Sending synchronously
            function testSync() {
                // Getting a result by Sending arguments to main process
                var res = ipc.sendSync('mul-sync', {a:30, b:2});
                msg("sync result="+res);
            };
            
            // Sending asynchronous
            function testAsync() {
                // Sending arguments to main process 
                ipc.send('mul-async', {a:30, b:2});
                // if return a result by async trans communication
                ipc.on('mul-async-reply', function(evt arg) {
                   msg("async result="+arg); 
                });
            };
            
            function msg(msg) {
                info.innerHTML += msg + "<br>";
            }
            
        </script>
    </head>
    <body>
        <h1>test app</h1>
        <p id="info"></p>
    </body>
    
</html>