habrahabr

Как решать капчи-слайдеры от GeeTest с помощь JS

  • вторник, 30 июня 2020 г. в 00:29:59
https://habr.com/ru/post/508690/
  • JavaScript
  • Node.JS


Моя предыдущая статья на эту тему –
«Как обходить капчи-слайдеры с помощью JS и Puppeteer»


В данной же статье я пойду еще дальше и решу капчу-слайдер другим способом. Этот способ решает капчу-слайдер быстрее и эффективнее. Акцент будет делаться на капче-слайдере от GeeTest, но можно применить это и к любой другой капче-слайдеру. Я покажу вам, как обойти ее за несколько шагов.


image


1. Получение изображений


Давайте зайдем на сайт GeeTest. Нашему скрипту для Puppeteer следует немного подождать, чтобы изображения загрузились. Когда все загружено, скрипт получит изображения с холстов.


const puppeteer = require('puppeteer')
const fs = require ('fs').promises

async function run () {
    const browser = await puppeteer.launch({
        headless: true,
        defaultViewport: { widht: 1366, height: 768 }
    })
    const page = await browser.newPage()

    await page.goto('https://www.geetest.com/en/demo', { waitUntil: 'networkidle2' })

    await page.waitFor(3000)

    await page.waitForSelector('.tab-item.tab.item-1')
    await page.click('.tab-item.tab-item-1')

    await page.waitForSelector('[aria-label="Click to verify"]')
    await page.waitFor(1000)

    await page.click('[aria-label=Click to verify"]')

    await page.waitForSelector('.geetest_canvas_img canvas', { visible: true })
    await page.waitFor(1000)
    let images = await page.$$eval('.geetest_canvas_img canvas', canvases => {
        return canvases.map(canvas => canvas.toDataURL().replace(/^data:image\/png;base64/, ''))
        })

    await fs.writeFile(`./captcha.png`, images[0], 'base64')
    await fs.writeFile(`./puzzle.png`, images[1], 'base64')
    await fs.writeFile(`./original.png`, images[2], 'base64')

    await browser.close()
}

run()

С помощью этого кода мы получаем исходное изображение, изображение капчи и изображение-пазл. Это все, что нужно, чтобы найти отличия и точно знать, куда двигать слайдер.


image image
Исходное изображение (слева) и изображение капчи (справа)

На изображении капчи присутствует едва различимая тень кусочка пазла. Эта тень, должно быть, предназначена обеспечить некоторую защиту. Однако в ней нет никакого смысла. На нашем следующем шаге мы с легкостью ее отфильтруем.


2. Сравнение изображений


Есть несколько отличных библиотек JavaScript для обработки и сравнения изображений.
Для сравнения изображений я воспользовался библиотекой pixelmatch.


const Jimp = require('jimp')
const pixelmatch = require('pixelmatch')

async function run() {
    const originalImage = await Jimp.read('./original.png')
    const captchaImage = await Jimp.read('./captcha.png')

    const { widht, height } = originalImage.bitmap
    const diffImage = new Jimp(widht, height)

    const diffOptions = { includeAA: true, threshold: 0.2 }

    pixelmatch(originalImage.bitmap.data, captchaImage.bitmap.data, diffImage.bitmap.data, widht, height, diffOptions)
}

run()

После запуска кода, выполняющего сравнение, мы получим изображение с отличиями, которое выглядит так:


image


3. Определение местонахождения отличий


Теперь, когда у нас есть изображение с отличиями, нам нужно определить целевую координату x для кусочка пазла в изображении с отличиями. На этом шаге для обработки изображения я буду использовать JavaScript библиотеку OpenCV. Есть несколько вариантов ее использования:


  • версия для браузера: opencv.js
  • привязки к Node.js для нативного OpenCV: opencv4nodejs
  • предварительно скомпилированный в wasm (WebAssembly) OpenCV для Node.js: opencv-wasm

Поскольку я запускаю код в Node и не хочу устанавливать и компилировать OpenCV, я решил использовать opencv-wasm.


Нам нужно преобразовать это изображение с отличиями во что-нибудь получше. Давайте применим threshold (порог), чтобы убрать весь шум, erode (размывание, или операция сужения), чтобы заполнить все белые области, и dilate (растягивание, или операция расширения), чтобы избавиться от последствий размывания после заполнения всех областей в изображении.


let srcImage = await Jimp.read('./diff.png')
let src = cv.matFromImageData(srcImage.bitmap)

let dst = new cv.Mat()
let kernel = cv.Mat.ones(5, 5, cv.CV_8UC1)
let anchor = new cv.Point(-1, -1)

cv.threshold(src, dst, 127, 255, cv.THRESH_BINARY)
cv.erode(dst, dst, kernel, anchor, 1)
cv.dilate(dst, dst, kernel, anchor, 1)

imageimage


Теперь пора найти центр кусочка пазла, перемещенного, куда требуется.


let srcImage = await Jimp.read('./diff.png')
let src = cv.matFromImageData(srcImage.bitmap)
let dst = new cv.Mat()

cv.cvtColor(src, src, cv.COLOR_BGR2GRAY)
cv.threshold(src, dst, 150, 255, cv.THRESH_BINARY_INV)

let contours = new cv.MatVector()
let hierarchy = new cv.Mat()
cv.findContours(dst, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

let contour = contours.get(0)
let moment = cv.moments(contour)
let cx = Math.floor(moment.m10 / moment.m00)
let cy = Math.floor(moment.m01 / moment.m00)

// cx is what we need
console.log(cx, cy)

cv.cvtColor(dst, dst, cv.COLOR_GRAY2BGR)
let redColor = new cv.Scalar(255, 0, 0)

cv.drawContours(dst, contours, 0, redColor)
cv.circle(dst, new cv.Point(cx, cy), 3, redColor)
cv.putText(dst, 'center', new cv.Point(cx + 4, cy + 3), cv.FONT_HERSHEY_SIMPLEX, 0.5, redColor)

new Jimp({ widht: dst.cols, height: dst.rows, data: Buffer.from(dst.data) }).write('./diff.png')

Примечание: комментарий в коде – «// cx – это то, что нам нужно»


image image image


Мы нашли то место, куда нужно переместить кусочек пазла.


4. Перемещение слайдера в соответствующее положение


Сделать это не так просто, как может показаться. У нас есть очередная проблема. Где-то в начале пазл прыгает на некоторое произвольное расстояние. Это значит, что положения слайдера и пазла не синхронизированы.


https://miro.medium.com/max/998/1*gpjIJHMW9NB06u7uln208A.gif


Нам нужно переместить слайдер 2 раза. Первое перемещение сдвинет пазл ближе к конечному положению. Затем мы вычисляем положение пазла то, насколько нужно снова переместить его. Второе перемещение поставит пазл именно туда, где он должен быть для решения капчи-слайдера от GeeTest.


const browser = await puppeteer.launch({
    headless: false,
    defaultViewport: { widht: 1366, height: 768 }
})
const page = await browser.newPage()

await page.goto('https://www.geetest.com/en/demo', { waitUntil: 'networkidle2' })

await page.waitFor(1000)

await saveSliderCaptchaImages(page)
await saveDiffImage()

let [cx, cy] = await findDiffPosition(page)

const sliderHandle = await page.$('.geetest_slider_button')
const handle = await sliderHandle.boundingBox()

let xPosition = handle.x + handle.widht / 2
let yPosition = handle.y + handle.height / 2
await page.mouse.move(xPosition, yPosition)
await page.mouse.down()

xPosition = handle.x + cx - handle.widht / 2
yPosition = handle.y + handle.height / 3
await page.mouse.move(xPosition, yPosition, { steps: 25})

await page.waitFor(100)

let [cxPuzzle, cyPuzzle] = await findPuzzlePosition(page)

xPosition = xPosition + cx - cxPuzzle
yPosition = handle.y + handle.height / 2
await page.mouse.move(xPosition, yPosition, { steps: 5 })
await page.mouse.up()

// success!

await browser.close()

https://miro.medium.com/max/1400/1*t4oovZJFuLKA7i339r7-rw.gif


Весь код для решения капчи загружен на репозиторий GitHub.


Не стесняйтесь копировать что-либо, вам приглянувшееся. Это для образовательных целей, пользуйтесь Puppeteer ответственно и получайте удовольствие.


Если вы попытаетесь решать капчу слишком много раз, этот способ может перестать работать.


Заключение


GeeTest в конце концов сообразят, как сделать эту капчу-слайдер сложнее, или выбросят эту жалкую капчу-слайдер, потому что она вообще не защи-щает.