자동 글등록 봇 만들기 -디씨봇- (2)

2019년 2월 26일 화요일

실제예제

실전예제로 대표적으로 자동등록 방지가 복잡하게 되어있는 커뮤니티 디씨인사이드에 등록을 해보자.

bot.js

'use strict'

const phantom = require('phantom')
const debug = console.log

const timeout = (ms) => new Promise((res) => setTimeout(res, ms))

var account = '[userid]'
var password = '[password]'

// 셀렉터 대기
async function waitForSelector(page, selector, timeOutMillis) {
  return new Promise((res) => {
    var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000
    var start = new Date().getTime()

    var timer = setInterval(async () => {
      //console.log('wait..');

      if (new Date().getTime() - start >= maxtimeOutMillis) {
        debug("'waitFor()' timeout")
        clearTimeout(timer)
        res(false)
      }

      var ret = await page.evaluate(function (selector) {
        return document.querySelectorAll(selector).length
      }, selector)
      if (ret) {
        clearTimeout(timer)
        res(true)
      }
    }, 250)
  })
}

// 로그인
async function dc_login(page) {
  debug('--dc_login')
  var url = 'http://m.dcinside.com/auth/login?r_url=http%3A%2F%2Fm.dcinside.com%2Faside'
  const status = await page.open(url)
  var ret = false

  ret = await waitForSelector(page, '#user_id')

  if (!ret) return false

  await page.evaluate(
    function (account, password) {
      var form = document.getElementById('login_process')
      form.user_id.value = account
      form.user_pw.value = password
      document.getElementById('login_ok').click()
    },
    account,
    password
  )

  ret = await waitForSelector(page, '.login-box')

  if (!ret) {
    debug('timeout!')
  }
  debug(ret)

  //await page.render('login.png')

  if (!ret) return false

  return true
}

// 글작성
async function dc_writer(page, gall, subject, memo) {
  debug('--dc_writer')
  var url = 'http://m.dcinside.com/write/' + gall
  const status = await page.open(url)
  var ret = false

  ret = await waitForSelector(page, '.gall-tit')
  if (!ret) {
    return 'timeout'
  }

  ret = await page.evaluate(function (selector) {
    return document.querySelectorAll(selector).length
  }, '#name')
  if (ret) {
    return 'logout'
  }

  await page.evaluate(
    function (subject, memo) {
      var form = document.getElementById('writeForm')

      document.getElementById('subject').value = subject
      document.getElementById('textBox').innerHTML = memo
      document.getElementById('memo').value = memo

      write_submit()
    },
    subject,
    memo
  )

  await timeout(3000)

  //await page.render('write.png')

  return 'write'
}

// 메인루프
;(async () => {
  const instance = await phantom.create()

  try {
    var ret = false

    var page = await instance.createPage()
    ret = await dc_login(page)
    if (!ret) {
      debug('login fail')
      throw new Error('login fail')
    }
    await page.close()

    var running = true
    while (running) {
      debug('--running')
      page = await instance.createPage()

      var gallid = '[갤러리ID]'
      var subject = '제목입니다.'
      var memo = '내용입니다.<br/>test content<br/>test content'

      var ret = await dc_writer(page, gallid, subject, memo)
      debug(ret)
      if (ret == 'write') {
        // 성공
      }

      await page.close()
      await timeout(60 * 60 * 1000) //한시간 대기
    }

    debug('--end')
  } catch (e) {
    debug(e)
  }

  await instance.exit()
})()

아래의 변수에는 실제 사용하는 아이디/패스워드/갤러리ID를 입력한다.

var account = '[userid]'
var password = '[password]'
var gallid = '[갤러리ID]'

주요 함수 설명

timeout Pomise를 이용한 setTimeout이다. ms만큼 대기한다.

waitForSelector phantomjs page에 selector에 해당하는 DOM객체가 나타날때까지 대기한다. 주로 페이지 이동, UI액션후에 대기할때 사용하기위해 만들었다. timeOutMillis이 지나면 대기시간 초과로 실패한다.

page.evaluate phantomjs 에서 제공하는 함수로 webpage내에서 javascript를 실행한다. 코드상으로는 연결되서 동작하는 것처럼보이지만.. 사실상 함수를 텍스트로 전달하는식이다. (closure로 변수 접근불가) 따라서 변수를 별도로 넘겨줘야한다.

await page.evaluate(
  function (account, password) {
    var form = document.getElementById('login_process')
    form.user_id.value = account
    form.user_pw.value = password
    document.getElementById('login_ok').click()
  },
  account,
  password
)

변수 account, password를 전달하기위해 evaluate의 2번째, 3번째 파라메터에 넣고 evaluate로 전달하는 자바스크립트 함수에서 넘겨받는다. 여기서 자바스크립트 함수인

function (account, password) {
    var form = document.getElementById('login_process');
    form.user_id.value = account;
    form.user_pw.value = password;
    document.getElementById('login_ok').click();
}

이 부분은 텍스트로 봐야한다. 브라우저내에 스크립트로 실행된다. dcinside.com에서 jquery를 사용하고 있으므로

function (account, password) {
    var form = $('#login_process')[0];
    form.user_id.value = account;
    form.user_pw.value = password;
    $('#login_ok').click();
}

이런식으로도 jquery도 사용가능하다.

동작함수

dc_login 로그인을 한다. 로그인 페이지로 이동하고 page.evaluate를 통해 자바스크립트로 아이디/패스워드를 입력하고 로그인 버튼을 클릭한다.

dc_writer 글을 작성한다. 글쓰기 페이지로 이동하고 page.evaluate를 통해 자바스크립트로 제목/내용을 입력하고 글쓰기버튼을 눌렀을때 실행되는 write_submit()를 호출하여 글을 등록한다.

메인루프

글을 작성하고 한시간을 대기하는 무한 loop구조다.

실행

node bot.js

node.js로 짜여진만큼 기타 모듈을 이용하여 (request,cheerio등) 정상적으로 등록되었는지 목록페이지를 읽어와서 확인을 하거나 다른 페이지의 캡쳐화면을 첨부파일로 추가하거나하는 기능을 쉽게 덧붙일 수 있다.

raspberry pi 이용

라즈베리파이를 이용하면 적은 비용으로 이용할 수 있다. 저전력으로 24시간 켜놔도 전기세 부담이 없다. 고맙게도 phantomjs를 라즈베리파이용으로 미리 컴파일해서 올려둔 프로젝트가 있으니 바로 clone받아 이용할 수 있다.

라즈베리파이로 포팅된 phantomjs https://github.com/piksel/phantomjs-raspberrypi.git

  1. stretch
git clone https://github.com/piksel/phantomjs-raspberrypi.git
  1. jessie
git clone -b jessie https://github.com/piksel/phantomjs-raspberrypi.git

실행파일복사

ln -s phantomjs-raspberrypi/bin/phantomjs /usr/bin/phantomjs

or

mv phantomjs-raspberrypi /usr/local/
ln -s /usr/local/phantomjs-raspberrypi/bin/phantomjs /usr/bin/
phantomjs -v

한글폰트 설치

apt-get install fonts-nanum

node.js 설치

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

or

wget https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-armv6l.tar.xz
tar xzvf node-v10.15.1-linux-armv6l.tar.xz
cd node-v10.15.1-linux-armv6l
sudo cp -R * /usr/local/