Сегодня я делюсь, пожалуй, самой сложной вещью, которую я построил с OpenWhisk. Хотя я горжусь этим, я напомню людям, что я все еще новичок в этом мире, так что имейте это в виду, когда я буду объяснять, что я сделал.
Много лет назад, около семи (черт возьми), я создал демо-версию ColdFusion, которая анализировала локальные данные 911 и локально сохраняла их в базе данных: Proof of Concept 911 Viewer.
Я использовал Yahoo Pipe, чтобы получить HTML-данные, предоставленные веб-сайтом местной полиции, и преобразовать их во что-то, что я мог хранить. Это не обязательно была ракетостроение, но строить было весело. Было еще веселее, когда я забыл, что автоматизировал его, и вернулся через несколько месяцев, чтобы просмотреть все собранные данные: Обновите мой 911 Viewer.
Эта демонстрация недавно пришла мне на ум, и я подумал, что было бы неплохо попробовать построить ее с помощью OpenWhisk. Имея это в виду, я построил следующее:
- Во-первых, действие, которое анализирует данные.
- Во-вторых, действие, которое принимает входные данные, проверяет, существуют ли данные в хранилище данных Cloudant, а затем, если нет, добавляет их.
- Последовательность для соединения двух.
- Расписание на основе Cron для периодической проверки данных.
Это звучит как много и довольно сложно, но разбивка на составные части/функции упростила работу и позволила мне попробовать некоторые части OpenWhisk, с которыми я еще не играл, в частности аспект триггера Cron. Давайте рассмотрим это шаг за шагом.
Разбор необработанных данных HTML
Данные, которые я анализирую, находятся на http://lafayette911.org. Как видите, это таблица отчетов об инцидентах:
Я начал с быстрого просмотра исходного кода, чтобы увидеть, как был создан HTML. Оказалось, что таблица управлялась iframe, указывающим на http://apps.lafayettela.gov/L911/default.aspx. Глядя на исходный код, я увидел, что данные были переданы Ajax-вызовом http://apps.lafayettela.gov/L911/Service2.svc/getTrafficIncidents. Я был взволнован, потому что на мгновение подумал, что мне не придется ничего анализировать. Оказывается, JSON на самом деле был отформатирован HTML (я немного уменьшил его):
{"d":" <center><a href=\"#KEY\">KEY<\/a><table border=0 bgcolor=\"white\"><tr bgcolor=\"#99FF99\"><td><b> <a href='http:\/\/maps.google.com\/maps?q=2909+NW+EVANGELINE+THROUGHWAY+,LAFAYETTE+LA' target='_new'>2909 NW EVANGELINE TW<\/a> <BR> LAFAYETTE,LA <\/b><\/td><td><b>Vehicle Accident w\/ Injuries<\/b><\/td><td><b>02\/14\/2017 - 11:59 AM<\/b><\/td><td><b>P F M <\/b><\/td><\/tr><tr bgcolor=\"#FFFF99\"><td><b> <a href='http:\/\/maps.google.com\/maps?q=1100+SE+EVANGELINE+THROUGHWAY+,LAFAYETTE+LA' target='_new'>1100 SE EVANGELINE TW<\/a> <BR> LAFAYETTE,LA <\/b><\/td><td><b>Vehicle Accident w\/ Injuries<\/b><\/td><td><b>02\/14\/2017 - 11:40 AM<\/b><\/td><td><b>P F M <\/b><\/td><\/tr><\/table><small>Data Updated at 02\/14\/2017 - 1:12:38 PM <\/small><\/center><script>$('dateline').innerHTML = '02\/14\/2017 - 1:12:38 PM'; <\/script>"}
Вздох
Хорошо, к счастью, я знал, как с этим работать. В прошлом году я сделал демонстрацию, включающую парсинг веб-страниц с помощью Cheerio (Парсинг веб-страницы в Node с помощью Cheerio), и я знал, что это работает хорошо, поэтому мои действия были сосредоточены на работе с этим. Помните, что для включения случайного npm, упакованного с помощью OpenWhisk, вы должны использовать заархивированное действие, которое включает каталог package.json и node_modules. Это немного больше работы, но незначительно.
Другой немного сложный аспект заключался в том, что я хотел геокодировать адреса. Для этого я использовал превосходный Google Geocode API, который является частью Maps SDK. Вот и вся акция.
let cheerio = require('cheerio'); let request = require('request');
function main(args) {
return new Promise((resolve, reject) => {
request('http://apps.lafayettela.gov/L911/Service2.svc/getTrafficIncidents', {method:'post'}, function(err, response, body) {
if(err) reject(err);
let results = []; // body is a json packet, html is in d let $ = cheerio.load(JSON.parse(body).d); let channels = $('tr'); //channel 0 is the header for(let i=1;i<channels.length;i++) { let channelRow = channels.get(i); let cells = $(channelRow).children(); //console.log(channelRow); let loc = $(cells.get(0)).text().trim(); let reason = $(cells.get(1)).text().trim(); let timestamp = $(cells.get(2)).text().trim(); let [daypart,timepart] = timestamp.split(' - '); let incidentDate = new Date(daypart + ' '+timepart); let assisting = $(cells.get(3)).text().trim().split(' '); //package it up results.push({location:loc, reason:reason, timestamp:incidentDate, assisting: assisting}); }
/* New logic - for each, geocode */ let promises = []; results.forEach(function(res) {
console.log('need to work on '+res.location); promises.push(new Promise( (resolve, reject) => { let geourl = 'https://maps.googleapis.com/maps/api/geocode/json?address='+encodeURIComponent(res.location); console.log(geourl); request(geourl, function(err, response, body) { if(err) reject(err); let geoResult = {}; let geodata = JSON.parse(body); if(geodata.status === 'OK') { geoResult.geostatus = true; geoResult.geo = geodata.results[0].geometry.location; } else { geoResult.geostatus = false; } resolve(geoResult); //console.log(body); });
}));
}); Promise.all(promises).then(function(geodata) { console.log('done with all promises'); //brittle code here, geodata len != results for(var i=0;i<geodata.length;i++) { results[i].geo = geodata[i]; } resolve({ traffic:results }); });
});
});
}
exports.main = main;
Итак, сверху — мы начинаем с общего запроса данных. Получив это, мы можем попросить Cheerio превратиться в DOM, как HTML в браузере. Затем я беру все строки таблицы, а затем извлекаю ячейки внутри каждой строки. Я немного манипулирую временем, чтобы превратить его в данные JavaScript и преобразовать «вспомогательную» ячейку в массив.
Следующая часть немного сложнее. Мне нужно геокодировать все адреса, и это включает в себя N асинхронных процессов. Поэтому я использую массив обещаний, чтобы получить все результаты, а затем обновить исходные данные. К сожалению, похоже, что у сервиса есть проблема с пересечениями. Так, например, авария в «Джонстон и Камелия» неправильно геокодирована, хотя ссылки на карту с сайта работают нормально. Это может быть моя вина. Иногда это срабатывало, иногда нет.
В итоге я получаю хороший набор данных:
{
"traffic": [
{
"location": "LEE AV & E CYPRESS ST LAFAYETTE,LA",
"reason": "Vehicle Accident",
"timestamp": "2017-02-14T19:08:00.000Z",
"assisting": [
"P",
"M"
],
"geo": {
"geostatus": true,
"geo": {
"lat": 30.2256757,
"lng": -92.0149277
}
}
},
{
"location": "E UNIVERSITY AV & W PINHOOK RD LAFAYETTE,LA",
"reason": "Vehicle Accident",
"timestamp": "2017-02-14T19:02:00.000Z",
"assisting": [
"S",
"P"
],
"geo": {
"geostatus": true,
"geo": {
"lat": 30.21055,
"lng": -92.0097742
}
}
},
{
"location": "6801 JOHNSTON ST LAFAYETTE,LA",
"reason": "Vehicle Accident",
"timestamp": "2017-02-14T19:00:00.000Z",
"assisting": [
"P"
],
"geo": {
"geostatus": true,
"geo": {
"lat": 30.150066,
"lng": -92.0934762
}
}
},
{
"location": "W PINHOOK RD & BENDEL RD LAFAYETTE,LA",
"reason": "Vehicle Accident",
"timestamp": "2017-02-14T18:44:00.000Z",
"assisting": [
"P"
],
"geo": {
"geostatus": true,
"geo": {
"lat": 30.1990935,
"lng": -92.0163944
}
}
}
]
}
Неплохо! Хорошо, переходим ко второму шагу — сохранению данных.
Сохранение данных с помощью Cloudant
Для хранения данных я предоставил новый сервис Cloudant с Bluemix. OpenWhisk может автоматически подбирать новые сервисы Cloudant и добавлять в вашу учетную запись пакет с действиями/триггерами для взаимодействия с этим сервисом. Чтобы работать с этими действиями, я создал свое собственное действие, которому поручено обрабатывать ввод данных, проверять, являются ли они новыми, а затем добавлять их. Вот это действие.
var openWhisk = require('openwhisk'); var ow = openWhisk({ apihost:'openwhisk.ng.bluemix.net', api_key:'my secret is so secret it doesnt know it is a secret' });
var actionBase = '/[email protected]_My Space/Bluemix_Cloudant Traffic_Credentials-1';
function main(args) {
/* hard coded for now args.traffic = [ { location:"W CONGRESS ST & CAJUNDOME BL LAFAYETTE,LA", reason:"Flood", timestamp:"2017-02-08T20:59:00.000Z" }, { location:"ssss W CONGRESS ST & CAJUNDOME BL LAFAYETTE,LA", reason:"Vehicle Accident", timestamp:"2017-02-08T20:59:00.000Z" }, { location:"W CONGRESS ST & CAJUNDOME BL LAFAYETTE,LA", reason:"Monster", timestamp:"2017-02-08T20:59:00.000Z" },
]; */
if(!args.traffic) args.traffic = []; return new Promise((resolve, reject) => {
let promises = []; args.traffic.forEach(function(d) { promises.push(addIfNew(d)); }); Promise.all(promises).then((results) => { console.log('all done like a boss'); resolve({results:results}); });
});
}
function addIfNew(d) {
return new Promise((resolve, reject) => { ow.actions.invoke({ actionName:actionBase+'/exec-query-find', blocking:true, params:{ "dbname":"traffic", "query": { "selector": { "location": { "$eq": d.location }, "reason":{ "$eq":d.reason }, "timestamp":{ "$eq":d.timestamp } }, "fields": [ "_id" ] } } }).then(function(res) { let numMatches = res.response.result.docs.length; if(numMatches === 0) { console.log('data is new, so add it'); ow.actions.invoke({ actionName:actionBase+'/write', blocking:true, params:{ "dbname":"traffic", "doc":d } }).then(function(res) { resolve({result:1}); }); } else { resolve({result:0}); } }); }); }
exports.main = main;
Сверху — обратите внимание, что я использую пакет OpenWhisk. Это в основном позволяет мне использовать OpenWhisk из моего действия так же, как я использую его из CLI. Это все еще кажется мне… немного неправильным, но я, честно говоря, не знаю другого способа сделать это. Теоретически я мог бы просто совершать REST-вызовы прямо в свой сервис Cloudant, но пока я собираюсь использовать пакет. Я определенно думаю, что в будущем я буду делать здесь что-то по-другому.
Обратите внимание, что в основном разделе у меня есть некоторые жестко закодированные данные, закомментированные. Во время тестирования именно так я справился с вводом демонстрационных данных в действие. В конце концов, все сводится к блоку addIfNew
. Мои навыки Cloudant несколько слабы, но моя логика работала хорошо. Я запрашиваю местоположение, причину и отметку времени, но не вспомогательные данные, поскольку я не был уверен, смогу ли я запросить такие значения массива. На случай, если две аварии произойдут одновременно в одном и том же месте, но с разными спасателями, я просто предположу, что вся мультивселенная рушится, и жизнь, какой мы ее знаем, почти закончилась. (Эй, мне не придется писать модульные тесты!)
Если совпадений не возвращается, я просто передаю данные в действие записи и — все!
Соединение точек
Давайте резюмируем. У меня есть действие, которое может сосать строку HTML и превращать ее в данные. У меня есть второе действие, которое может принять этот ввод и сохранить его, если он новый. Теперь нам нужно собрать это вместе, запланировать и запустить с этим.
Во-первых, соединить их тривиально — просто используйте последовательность! Я назвал свой handleTraffic и просто передал ему имя двух моих действий — getTraffic и addTraffic. Команда выглядит так:
wsk action create handleTraffic --sequence getTraffic,addTrafic
Затем я отправил вызов последовательности, чтобы убедиться, что она работает. Помните, детские шаги.
Хорошо — здесь все становится немного сложнее. Я начал с создания триггера Тревожный. Это триггер, доступный на платформе Bluemix OpenWhisk, который позволяет вам определить время срабатывания на основе Cron. Я создал свой так:
wsk trigger create checkTraffic --feed /whisk.system/alarms/alarm --param cron "5 * * * *"
Я всегда находил Cron непонятным синтаксисом, поэтому я использовал http://crontab-generator.org, чтобы сгенерировать для меня строку.
Все это делает будильник — даже через 5 минут сработает триггер. Но само по себе это ничего не дает. Чтобы заставить его что-то делать, я сделал правило. Правило просто гласило: когда срабатывает checkTraffic, запускать мою последовательность. Я назвал свое правило newTrafficRule, потому что у меня нет воображения.
Пользовательский интерфейс OpenWhisk в Bluemix отлично справляется с этой задачей, хотя, хоть убей, я не могу понять, как настроить исходный Cron. Я думаю, это то, что вы хотите убедиться, что вы не забыли.
Я планирую более подробно рассказать о пользовательском интерфейсе OpenWhisk позже, но хочу отметить, что монитор был чрезвычайно полезным, когда я работал над этой демонстрацией. Это позволило мне видеть, как мои действия срабатывают в режиме реального времени, и наблюдать за их результатами.
Заворачивать
В целом, это работает довольно хорошо. Я обнаружил, что существует ограничение на количество запросов, которые я могу выполнять в секунду на бесплатном уровне Cloudant, но в платной учетной записи, конечно, такой проблемы не будет. У меня это работает уже несколько дней (хотя изначально у меня не было геокодирования), и я приближаюсь к 400 точкам данных. Я планирую оставить это на некоторое время и вернуться, как только у меня будет добрая тысяча или около того записей, и немного повеселиться, нарисовав диаграммы / проанализировав данные.
Эта статья первоначально была опубликована в моем блоге по адресу: https://www.raymondcamden.com/2017/02/14/collecting-911-data-openwhisk-cron-triggers