NEWS
Передача показаний приборов учёта на web-сайт
-
Задача : передавать данные по приборам учёта на сайты УК.
Условие: на web-сайте не должно быть капчи.
Решение:
Установить chromium браузер. Chromium для debian 10 ставится командой:
"apt-get install chromium chromium-l10n".В свойства драйвера javascript дописываем библиотеку "puppeteer".
Алгоритм навигации и заполнения:
Для навигации по сайту используем селекторы.
Приоритет при навигации по сайту с помощью библиотеки puppeteer:- Если элемент страницы содержит id, например 'id=sendCountersValues', используем его. Самый лучший вариант. При копипасте через десктопный браузер селектор будет выглядеть так: '#sendCountersValues'. Однако некоторые id - автогенерируемые, необходимо убедиться что он не меняется в новой сессии браузера или при новой передачи данных.
- Через копипаст селектора как показано на картинках step1_selector.png, step2_selector.png. Главное чтобы скрипт при работе с таким селектором не выдавал ошибок. Селектор будет например таким: '#authorization_form > div:nth-child(2) > div > input'.
- По атрибуту 'name='. Селектор будет например таким: 'input[name=login]'. Возможная проблема - когда несколько данных имеют одинаковый 'name='. Этот случай рассмотрен ниже.
- По классу, например элемент имеет класс 'btn-lg'. Селектор будет тогда таким: '.btn-lg'. Уникальность так же не гарантирована, в этом случае подход такой же как в п. 3.
- Навигация курсором мыши или клавиатурой (Tab). Тупо, но работает когда по другому не получается.
Внимание!
После каждого ошибочного запуска скрипта в памяти остаётся процесс "chromium". Проверка: " ps aux|grep chromium"
Рекомендую убивать процессы командой "killall chromium"
Иначе вся оперативная память забъётся и система будет тормозить и глючить до перезагрузки.Скрипт с комментариями:
const puppeteer = require('puppeteer'); var util = require('util') //Запускаем раз в месяц 16 числа в 8:40 schedule("40 8 16 * *", function () { send_data(); }); //schedule end async function send_data() { //Присвоение переменным скрипта данных приборов учёта const Bath_cold = getState('modbus.0.holdingRegisters.12328_Bathroom_stat_cold_water').val/100; const Bath_warm = getState('modbus.0.holdingRegisters.12330_Bathroom_stat_warm_water').val/100 + 0.006; const Kitchen_cold = getState('modbus.0.holdingRegisters.12324_Kitchen_stat_cold_water').val/100; const Kitchen_warm = getState('modbus.0.holdingRegisters.12326_Kitchen_stat_warm_water').val/100; //log('Bath_cold='+Bath_cold+', Bath_warm='+Bath_warm); //log('Kitchen_cold='+Kitchen_cold+', Kitchen_warm='+Kitchen_warm); //Проверка на допустимый диапазон данных if (Bath_cold >1 && Bath_cold <100000 && Bath_warm>1 && Bath_warm<100000 && Kitchen_cold >1 && Kitchen_cold <100000 && Kitchen_warm>1 && Kitchen_warm<100000) { const browser = await puppeteer.launch({ //указываем путь к chromium браузеру. executablePath: '/usr/bin/chromium', //разрешение с которым будет работать chromium. Должно быть достаточным для отображения всей страницы, с которыми будет работать chromium. Увеличить при необходимости. Не имеет отношение к разрешению экрана. 'defaultViewport' : { 'width' : 1920, 'height' : 1080 } }); const page = await browser.newPage(); //подключаем функцию отображения указателя мыши (может понадобиться в редких случаях) await installMouseHelper(page); //Указываем главную страницу сайта await page.goto('https://lk.rkcgkh.ru/', { waitUntil: 'networkidle0' }); //Смотрим картинку step1_selector.jpg, копипастим селектор. Команда на ожидание появление селектора. await page.waitForSelector('#authorization_form > div:nth-child(2) > div > input'); //Другой вариант - ждём появления элемента с заданными атрибутами. await page.waitFor('input[name=login]'); //Указываем свой login и пароль await page.$eval('input[name=login]', el => el.value = 'MYEAMAIL@yandex.ru'); await page.$eval('input[name=password]', el => el.value = 'MYPASSWORD'); //Селектор для кнопки можно определить несколькими способами: скопипастить - см. step2_selector.jpg //const selector1 = "#authorization_form > div:nth-child(4) > div.col-auto > button"; //или определить так. const selector1_alt = ('button[type=submit]'); //ждём когда селектор кнопки загрузится await page.waitFor(selector1_alt); //Здесь происходит клик по кнопке await page.click(selector1_alt); //Сохраняем картинку чтобы увидеть что происходит. Это момент сразу после клика поэтому страница будет такая же как перед кликом. Чтобы увидеть результат клика надо принудительно задать ожидание(ненадёжно) или ожидать загрузки селектора новой страницы. await page.screenshot({path: '/tmp/screen0.png'}); //Определяем селектор - пункт меню "Показания" . Картинки нет, но подход полностью аналогичен предыдущему клику. const selector2 = "#stack-sidebar > ul > li:nth-child(3) > a"; //Ожидаем загрузки селектора для подтверждения открытия нового содержимого. await page.waitFor(selector2); //Вот здесь мы только увидим новую страницу после клика. await page.screenshot({path: '/tmp/screen1.png'}); //Клик на пункте "Показания" await page.click(selector2); // Определяем селектор - блок в котором содержатся поля ввода данных. Это может быть любой вышестоящий блок с наличием идентификатора (id = "... ") или селекторы полей ввода. const selector3 = '#sendCountersValues'; //Ожидаем загрузки селектора для подтверждения открытия нового содержимого. await page.waitFor(selector3); //Сохраняем скриншот для целей отладки. Здесь будут пустые поля await page.screenshot({path: '/tmp/screen2.png'}); //На картинке step3.png смотрим атрибуты полей ввода по которым будет определяться селектор. // Копипаст селектора в данном случае не сработал, возможно из-за бага в библиотеке при экранировании квадратных кавычек "[", "]" //Вводим данные await page.$eval('input[name="counters[139786_0][value]"]', (el, var_KC) => el.value = var_KC,Kitchen_cold); await page.$eval('input[name="counters[139787_0][value]"]', (el, var_BC) => el.value = var_BC,Bath_cold); await page.$eval('input[name="counters[139788_0][value]"]', (el, var_KW) => el.value = var_KW,Kitchen_warm); await page.$eval('input[name="counters[139789_0][value]"]', (el, var_BW) => el.value = var_BW,Bath_warm); //Здесь картинка уже будет содержать данные, так как функция ввода с подтверждением ввода. await page.screenshot({path: '/tmp/screen3.png'}); //Создаём клик на кнопку "Передать", см step4.png, копипастим селектор. const selector4= '#sendCountersValues > div.row.row-widget-next.row-submit > div > input'; //Ожидаем загрузки селектора await page.waitFor(selector4); //Клик на кнопке "Передать" await page.click(selector4); //Ждём 2 секунды, а лучше сделать ожидание селектора новой страницы. await page.waitForTimeout(2000); //Здесь будет скриншот об успешном приёме данных. await page.screenshot({path: '/tmp/screen4.png'}); await browser.close(); sendTo('telegram.0', '/tmp/screen4.png', function (res) { log('Sent to telegram ' + res + ' users'); }); } } async function installMouseHelper(page) { await page.evaluateOnNewDocument(() => { // Install mouse helper only for top-level frame. if (window !== window.parent) return; window.addEventListener('DOMContentLoaded', () => { const box = document.createElement('puppeteer-mouse-pointer'); const styleElement = document.createElement('style'); styleElement.innerHTML = ` puppeteer-mouse-pointer { pointer-events: none; position: absolute; top: 0; z-index: 10000; left: 0; width: 20px; height: 20px; background: rgba(0,0,0,.4); border: 1px solid white; border-radius: 10px; margin: -10px 0 0 -10px; padding: 0; transition: background .2s, border-radius .2s, border-color .2s; } puppeteer-mouse-pointer.button-1 { transition: none; background: rgba(0,0,0,0.9); } puppeteer-mouse-pointer.button-2 { transition: none; border-color: rgba(0,0,255,0.9); } puppeteer-mouse-pointer.button-3 { transition: none; border-radius: 4px; } puppeteer-mouse-pointer.button-4 { transition: none; border-color: rgba(255,0,0,0.9); } puppeteer-mouse-pointer.button-5 { transition: none; border-color: rgba(0,255,0,0.9); } `; document.head.appendChild(styleElement); document.body.appendChild(box); document.addEventListener('mousemove', event => { box.style.left = event.pageX + 'px'; box.style.top = event.pageY + 'px'; updateButtons(event.buttons); }, true); document.addEventListener('mousedown', event => { updateButtons(event.buttons); box.classList.add('button-' + event.which); }, true); document.addEventListener('mouseup', event => { updateButtons(event.buttons); box.classList.remove('button-' + event.which); }, true); function updateButtons(buttons) { for (let i = 0; i < 5; i++) box.classList.toggle('button-' + i, buttons & (1 << i)); } }, false); }); };
Рассмотрим вариант когда элемент не уникален.
В моём случае это была таблица с полями ввода, которая через селекторы никак не хотела работать.
Пример на картинке step5.png.
Решение следующее:const inputs = await page.$$('input[type=text]'); await inputs[0].type(String(T1)); await inputs[1].type(String(T2));
Так как у всех элементов input есть общий атрибут "type=text" - загоняем их в массив, и обращаемся к элементам массива.
Другой проблемный вариант кнопка на картинке step6.png , селектор через копипаст не работает.
Решение - использовать class элемента :const button = await page.$('.btn-lg'); await button.click();
И самый плохой вариант, когда через селекторы не получается. Здесь уже нужна функция installMouseHelper, которую я нашёл на просторах интернета.
Просто позиционируем курсор (небольшой серый кружок) на нужный элемент и кликаем - картинка step7.png:await page.mouse.move(600,820); await page.mouse.click(600,820);
Кроме того, можно с помощью TAB-ов выбирать нужное поле. Просто вызываете нажатие TAB, сохраняете картинку и смотрите, какое поле подсвечено.
await page.keyboard.press("Tab"); await page.keyboard.press("Tab"); await page.keyboard.press("Tab"); await page.keyboard.type(String(T2));
API библиотеки : puppeteer
step1_selector.png
step2_selector.png
step3.png
step4.png
step5.png
step6.png
step7.png