Vai al contenuto

Un software che cerca su google al posto mio

Scritto da:

carlopeluso

Oggi voglio mostrarti come ho realizzato un’idea che da tempo mi frugava in testa: realizzare un software che cerca al posto mio le informazioni su google.

Il mio obiettivo nasceva dall’esigenza di fare ripetutamente alcune ricerche su google, magari a distanza di giorni, per controllare se una determinata ricerca avesse dei nuovi risultati.

Ad esempio, se sono interessato a partecipare ad un concorso di fotografia, cerco settimanalmente “concorso fotografia” con il filtro “ultima settimana” per visualizzare gli ultimi concorsi pubblicati.

Oppure se voglio sapere se a torino organizzano una nuova mostra d’arte, scrivo semplicemente “mostra torino”:

L’idea del bot telegram

Ho scelto di realizzare un Bot Telegram che cerca queste informazioni al posto mio, e l’ho fatto per poter essere notificato quando una di queste ricerche ha qualcosa di nuovo. Il bot telegram è una soluzione efficace per realizzare dei software che lavorano in background, quindi senza computer o smartphone acceso. Inoltre il sistema di notifiche è stabile e gratuito.

La realizzazione

Ho scelto di listare tutte le mie parole chiave preferite e farne un database, ecco un esempio:

  • “concorso fotografia torino”
  • “mostra torino”
  • “GTT torino” (per essere aggiornato su scioperi dei bus)
  • “Leverano news” (per essere aggiornato sulle news del mio paese in Puglia)
  • “progressive web apps” (per essere aggiornato sulle novità riguardo le PWA, ovvero il futuro dello sviluppo app, ecco a te un link)
  • “volontariato torino”
  • “massimo recalcati” (per essere aggiornato sul mio divulgatore e psicologo preferito)

Un json database

Per questo tipo di liste ho optato per l’uso di un database Json, in modo da non scomodare server DB pesanti. Tutto il database verrà scritto in un unico file contenente un oggetto con le seguenti proprietà:

"chiave":{ //root del json per l'indicizzazione
      "query":"query", //keyword da cercare su google
      "last":[  //array di due link,l'ultimo trovato e quello trovato nella ricerca precedente
          { 
            "titolo":"titolo link",
            "href":"url",
            "descrizione":"descrizione google",
            "data":"data google"
          },
          {
            "titolo":"titolo link",
            "href":"url",
            "descrizione":"descrizione google",
            "data":"data google"
          },
       ],
      "date": "timestamp"
},

dove “chiave” è chiaramente una key univoca che identifica la singola ricerca.

Un hosting gratuito per il file json DB

Sostanzialmente serve un server fisico dove hostare il file ed accedervi da internet. Io ho scelto S3 di Amazon, che permette di fare storage di file gratuitamente entro una certa soglia di dimensione ed accessi, attraverso un sistema di Buckets.

Uno script NodeJs

NodeJS è in grado di soddisfare tutti i miei requisiti per quest’app:

  • leggere e scrivere sul database json attraverso il modulo node-json-db
  • connettersi all’hosting di Amazon S3 dove è caricato il file del database, attraverso il modulo aws-sdk
  • comunicare con telegram attraverso il modulo node-telegram-bot-api
  • eseguire un browser internet attraverso il modulo puppeteer

Ecco il codice per intero:

var puppeteer = require('puppeteer');
var _ = require('underscore');
var TelegramBot = require('node-telegram-bot-api');
var JsonDB = require('node-json-db');
var c = require('chalk');
var schedule = require('node-schedule');
var child_process = require('child_process');
var AWS = require('aws-sdk');
var fs = require('fs');


var bucketName = 'S3-BUCKET-NAME';
var token = 'TELEGRAM-TOCKEN';
var bot = new TelegramBot(token, {
    polling: true
});


var s3 = new AWS.S3({
    apiVersion: '2006-03-01'
});
var params = {
    Bucket: 'BUCKET-NAME',
    Key: 'DB-JSON-FILE.JSON'
};
var db;


//sezione della rimozione della parola chiave tramite telegram
bot.on('message', (msg) => {
    var chatId = msg.chat.id + "";
    if (msg.reply_to_message) {
        try {
            var text = msg.reply_to_message.text.split("\n");
            text = text[0];
            text = text.toLowerCase().replace(/[^0-9a-z-]/g, "");
            db.delete("/" + chatId + "/google/" + text);
            bot.sendMessage(chatId, "Ho rimosso " + text + "!", {
                parse_mode: 'HTML'
            });
            saveToS3();
        } catch (e) {
            console.log(c.red("\n Delete " + text + " not successful! \n"));
        }

    } else {

        //se invio a telegram "Cerca" lui cerchera manualmente
        if (msg.text == "Cerca" || msg.text == "cerca") {
            console.log(c.black.bgYellow("\nManual search."));
            startChat(chatId);
        } else if (msg.text == "/start") {
            console.log(c.black.bgYellow("\New User!"));
            console.log(msg);
            bot.sendMessage(chatId, "Specifica una ricerca di tuo interesse (ad esempio 'Risultati partita', 'Bicicletta usata torino', 'Trekking piemonte'..). Monitorerò i risultati su Google e ti avviserò non appena ci sono novità.", {
                parse_mode: 'HTML'
            });
        } else {

        //altrimenti se invio a Telegram una parola chiave, questa verrà aggiunta e cercata.
            try {
                var q = msg.text;
                q = msg.text.toLowerCase().replace(/[^0-9a-z-]/g, "");
                console.log("Aggiunto topic: " + chatId);
                db.push("/" + chatId + "/google/" + q, {
                    query: msg.text,
                    date: new Date(0).getTime() //data vecchia in modo che ordinando per data sia vecchio
                });
                bot.sendMessage(chatId, "Sarai aggiornato su " + msg.text + ".", {
                    parse_mode: 'HTML'
                });
                console.log(c.black.bgYellow("\n Added " + msg.text));
                saveToS3();
            } catch (e) {
                console.log(c.red(e));
            }
        }
    }
});

//funzione che salva il database ad ogni modifica
function saveToS3() {
    fs.readFile("db2.json", (err, data) => {
        if (err) console.error(err)
        var base64data = new Buffer(data, 'binary')
        var params = {
            Bucket: bucketName,
            Key: "DB-JSON-FILE",
            Body: base64data
        }
        s3.upload(params, (err, data) => {
            if (err) console.error(`Upload Error ${err}`)
            console.log(c.black.bgYellow("\n Upload Completed to S3"));
        })
    })
}

bot.on('polling_error', (error) => {
  console.log(c.black.bgYellow("\n Error from S3"));
});

//funzione che formatta una data
function data() {
    var currentdate = new Date();
    return currentdate.getDate() + "/" +
        (currentdate.getMonth() + 1) + "/" +
        currentdate.getFullYear() + " " +
        currentdate.getHours() + ":" +
        currentdate.getMinutes() + ":" +
        currentdate.getSeconds();
}

//funzione che esegue la ricerca su google 
var processNow = function(chatId) {
    console.log(c.blue.bgWhite('\n' + data() + ' Start\n'));
    var chain = [];
    bot.sendChatAction(chatId, "typing");
    var chiavi = db.getData("/" + chatId + "/google");
    var conteggio = _.countBy(chiavi, function(e) {
        return e.last && e.last.length ? 'withNews' : 'without';
    });
    console.log(conteggio);
    var sublist = _.sortBy(chiavi, 'date');
    var d = _.first(sublist, 2);
    var queries = [];
    for (i in d) {
        queries.push(d[i].query);
    }

    //apertura browser e chiamata di google
    puppeteer.launch({
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox']
    }).then(function(browser) {

        for (i in queries) {
            chain.push(openPage(browser, queries[i], chatId));
        }

        Promise.all(chain).then(function() {
            browser.disconnect();
            browser.close();
            saveToS3();
        });

    });
}


//funzione che schedula la ricerca ogni ora
var j = schedule.scheduleJob('0 10,11,12,13,14,15,16,17,18,19 * * *', function(date) {
    startAllChats();
    console.log("\nNext start: " + c.yellow(j.nextInvocation()));
});

//funzione che data una lista di utenti telegram, ne fa partire le ricerche.
var startAllChats = function() {
    s3.getObject(params, function(err, data) {
        console.log("presa ultima versione del db");
        fs.writeFileSync("db2.json", data.Body.toString());
        db = new JsonDB("db2", true, false);
        var chats = _.keys(db.getData("/"));
        for (i in chats) {
            processNow(chats[i]);
        }
    });
}

var startChat = function(chat) {
    s3.getObject(params, function(err, data) {
        console.log("presa ultima versione del db");
        fs.writeFileSync("db2.json", data.Body.toString());
        db = new JsonDB("db2", true, false);
        processNow(chat);
    });
}


startAllChats();

//apertura pagina google
var openPage = function(browser, query, chatId) {
    var url = 'https://www.google.it/search?q=' + query + '&safe=off&source=lnt&tbs=qdr:w&sa=X';
    return browser.newPage().then(function(page) {
        return page.goto(url, {
            waitUntil: 'networkidle2'
        }).then(function() {
            return page.addScriptTag({
                url: 'https://code.jquery.com/jquery-3.2.1.min.js'
            }).then(function() {
                return page.addScriptTag({
                    url: 'https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js'
                }).then(function() {
                    return page.evaluate(function() {
                        var risultati = document.querySelectorAll('.med[role=main] .g .rc');
                        var d = [];
                        for (i = 0; i < 2; ++i) { var link = risultati[i].querySelector("a"); var descrizione = risultati[i].querySelector(".s .st") || null; var data = risultati[i].querySelector(".s .st .f") || null; if (data && link.href && descrizione.innerText && data.innerHTML) { d.push({ titolo: link.querySelector("h3").innerText, href: link.href, descrizione: descrizione.innerText, data: data.innerHTML }); } } return d; }).then(function(newData) { var q = query.toLowerCase().replace(/[^0-9a-z-]/g, ""); var oldData = []; try { var oldData = db.getData("/" + chatId + "/google/" + q + "/last"); } catch (e) { //non c'è il record console.log("\n" + query + ": added."); } newData = _.sortBy(newData, 'titolo'); oldData = _.sortBy(oldData, 'titolo'); var dif = _.filter(newData, function(obj) { return !_.findWhere(oldData, { href: obj.href }); }); var m; if (dif.length > 0 && oldData.length >= 0) {
                            console.log(c.green("\n" + query + ": " + dif.length + " news."));
                            m = "" + query + "";
                            for (d in dif) {
                                console.log(c.grey("- " + dif[d].titolo));
                                m += "\n" + dif[d].titolo + "\n" + dif[d].descrizione + "\n";
                            }
                        } else {
                            console.log(c.grey("\n" + query + ": no news."));
                            bot.sendMessage(chatId, "" + query + "\nNo news.", {
                                parse_mode: 'HTML'
                            });
                        }

                        db.push("/" + chatId + "/google/" + q, {
                            query: query,
                            last: newData,
                            date: new Date().getTime()
                        });
                        if (chatId && m) {

                            bot.sendMessage(chatId, m, {
                                parse_mode: 'HTML',
                                disable_web_page_preview: true
                            });
                        }

                        return;
                    })

                });

            });

        })
    })
}

Elenco funzionalità dello script

  • Attraverso telegram, una volta creato il bot attraverso il Bot-Father, seguire la procedura ed avviare il bot.
  • Se scrivo un messaggio al bot, verrà considerato come una nuova parola chiave da cercare su Google.
  • se inceve scrivo “cerca” lui mi cerchera manualmente due parole chiave del mio database.
  • Se invece rispondo ad una nuova notifica inviata dal bot, verrà considerata come una rimozione della parola chiave.
  • Se non faccio nulla di manuale, il bot mi notificherà solo se tra le parole chiave esiste un nuovo risultato google.

Un hosting gratuito per lo script Node

Ho utilizzato il servizio di Heroku per eseguire lo script ed avere un ottimo strumento di deploy delle versioni dello script.

Disclaimer

Questo è solo una parte dello script, poiche ci sto lavorando ed è in continua evoluzione. Essendo puramente a scopo didattico, non concedo l’autorizzazione a commercializzare questa soluzione, poiche non intendo in nessun modo sostituire o automatizzare i processi di Google in maniera impropria. Se vuoi saperne di piu non esitare a contattarmi.

 

 

Articolo precedente

Come ho realizzato un tool per dividere un file audio

Articolo successivo

Un software per irrigare le piante mentre sono in vacanza