One place for hosting & domains

      utilizando

      Cómo extraer datos de un sitio web utilizando Node.js y Puppeteer


      El autor seleccionó Free and Open Source Fund para recibir una donación como parte del programa Write for DOnations.

      Introducción

      La extracción de datos de la web (web scraping) es un proceso que automatiza la recopilación de datos de Internet. En general, conlleva la implementación de un rastreador (crawler) que navega por la web de forma automática y extrae datos de páginas seleccionadas. Hay muchos motivos por los cuales se puede querer utilizar este proceso. El principal es que agiliza la recopilación en gran medida al eliminar el proceso manual de obtención de datos. La extracción de datos también es una buena solución cuando se quieren o deben obtener datos de un sitio web, pero este no proporciona una API.

      En este tutorial, creará una aplicación de extracción de datos web utilizando Node.js y Puppeteer. La complejidad de su aplicación irá aumentando a medida que avance. Primero, programará su aplicación para que abra Chromium y cargue un sitio web especial diseñado como espacio aislado para la extracción de datos web: books.toscrape.com. En los dos pasos siguientes, extraerá todos los libros de una sola página de books.toscrape y, luego, de varias. En los pasos restantes, filtrará la extracción por categoría de libros y, a continuación, guardará sus datos como archivo JSON.

      Advertencia: Los aspectos éticos y legales de la extracción de datos de la web son muy complejos y están en constante evolución. También difieren según la ubicación desde la que se realice, la ubicación de los datos y el sitio web en cuestión. En este tutorial, se extraen datos de un sitio web especial, books.toscrape.com, que está diseñado específicamente para probar aplicaciones de extracción de datos. La extracción de cualquier otro dominio queda fuera del alcance de este tutorial.

      Requisitos previos

      Con Node.js instalado, puede comenzar a configurar su extractor de datos web. Primero, creará un directorio root de un proyecto y, a continuación, instalará las dependencias requeridas. Este tutorial requiere una sola dependencia que instalará utilizando el administrador de paquetes predeterminado de Node.js npm. npm viene previamente instalado con Node.js, por lo tanto, no es necesario instalarlo.

      Cree una carpeta para este proyecto y posiciónese en ella:

      • mkdir book-scraper
      • cd book-scraper

      Ejecutará todos los comandos subsiguientes desde este directorio.

      Debemos instalar un paquete utilizando npm o el administrador de paquetes de Node. Primero, inicialice npm para crear un archivo packages.json que gestionará las dependencias y los metadatos de su proyecto.

      Inicialice npm para su proyecto:

      npm presentará una secuencia de solicitudes. Puede presionar ENTER en cada una de ellas o añadir descripciones personalizadas. Asegúrese de presionar ENTER y dejar los valores predeterminados en las solicitudes de entrypoint: y test command:. Alternativamente, puede pasar el indicador y a npm, npm init -y, para que complete todos los valores predeterminados de forma automática.

      El resultado tendrá un aspecto similar a este:

      Output

      { "name": "sammy_scraper", "version": "1.0.0", "description": "a web scraper", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "sammy the shark", "license": "ISC" } Is this OK? (yes) yes

      Escriba yes y presione ENTER. npm guardará este resultado como su archivo package.json.

      Ahora, utilice npm para instalar Puppeteer:

      • npm install --save puppeteer

      Este comando instala Puppeteer y una versión de Chromium que el equipo de Puppeteer sabe que funciona con su API.

      En equipos basados en Linux, Puppeteer podría requerir algunas dependencias adicionales.

      Si está utilizando Ubuntu 18.04, consulte el menú desplegable ‘Debian Dependencies’ (Dependencias de Debian) de la sección ‘Chrome headless doesn’t launch on UNIX’ (Chrome desatendido no se inicia en UNIX) en los documentos de resolución de problemas de Puppeteer. Puede utilizar el siguiente comando como ayuda para encontrar dependencias faltantes:

      Ahora que tiene npm, Puppeteer y todas las dependencias adicionales instaladas, su archivo package.json requiere un último ajuste para que pueda comenzar a programar. En este tutorial, iniciará su aplicación desde la línea de comandos con npm run start. Debe añadir cierta información sobre esta secuencia de comandos start en el archivo package.json. Específicamente, debe añadir una línea en la directiva scripts relativa a su comando start.

      Abra el archivo en su editor de texto preferido:

      Busque la sección scripts: y añada las siguientes configuraciones. Recuerde colocar una coma al final de la línea test de la secuencia de comandos; de lo contrario, su archivo no se analizará correctamente.

      Output

      { . . . "scripts": { "test": "echo "Error: no test specified" && exit 1", "start": "node index.js" }, . . . "dependencies": { "puppeteer": "^5.2.1" } }

      También notará que, ahora, aparece puppeteer debajo de dependencies casi al final del archivo. Su archivo package.json no requerirá más ajustes. Guarde sus cambios y cierre el editor.

      Con esto, está listo para comenzar a programar su extractor de datos. En el siguiente paso, configurará una instancia de navegador y probará la funcionalidad básica de su extractor de datos.

      Paso 2: Configurar una instancia de navegador

      Al abrir un navegador tradicional, entre otras cosas, puede hacer clic en botones, navegar con el mouse, escribir y abrir herramientas de desarrollo. Los navegadores desatendidos como Chromium permiten realizar estas mismas tareas, pero mediante programación y sin interfaz de usuario. En este paso, configurará la instancia de navegador de su extractor de datos. Cuando inicie su aplicación, esta abrirá Chromium y se dirigirá a books.toscrape.com de forma automática. Estas acciones iniciales constituirán la base de su programa.

      Su extractor de datos web requerirá cuatro archivos .js: browser.js, index.js, pageController.js y pageScraper.js. En este paso, creará estos cuatro archivos y los actualizará de forma continua a medida que su programa se vuelva más sofisticado. Comience con browser.js; este archivo contendrá la secuencia de comandos que inicia su navegador.

      Desde el directorio root de su proyecto, cree y abra browser.js en un editor de texto:

      Primero, solicitará Puppeteer con require y creará una función async denominada startBrowser(). Esta función iniciará el navegador y devolverá una instancia de él. Añada el siguiente código:

      ./book-scraper/browser.js

      const puppeteer = require('puppeteer');
      
      async function startBrowser(){
          let browser;
          try {
              console.log("Opening the browser......");
              browser = await puppeteer.launch({
                  headless: false,
                  args: ["--disable-setuid-sandbox"],
                  'ignoreHTTPSErrors': true
              });
          } catch (err) {
              console.log("Could not create a browser instance => : ", err);
          }
          return browser;
      }
      
      module.exports = {
          startBrowser
      };
      

      Puppeteer cuenta con un método .launch() que inicia una instancia de un navegador. Este método devuelve una promesa que debe asegurarse de que se resuelva utilizando un bloque .then o await.

      Utiliza await para asegurarse de que la promesa se resuelva, envuelve esta instancia en un bloque de código try-catch y, luego, devuelve una instancia del navegador.

      Tenga en cuenta que el método .launch() toma un parámetro JSON con varios valores:

      • headless: con el valor false, el navegador se ejecuta con una interfaz para que pueda ver la ejecución de su secuencia de comandos y, con true, se ejecuta en modo desatendido. Tenga en cuenta que si desea implementar su extractor de datos en la nube, debe establecer headless en true. La mayoría de los equipos virtuales se ejecutan en modo desatendido y no incluyen interfaz de usuario, por tanto, solo pueden ejecutar el navegador en ese modo. Puppeteer también incluye un modo headful, pero debe utilizarse únicamente para fines de prueba.
      • ignoreHTTPSErrors: con el valor true, le permite visitar sitios web que no están alojados con un protocolo HTTPS seguro e ignorar cualquier error relacionado con HTTPS.

      Guarde y cierre el archivo.

      Ahora, cree su segundo archivo .js, index.js:

      Aquí, utilizará require para solicitar browser.js y pageController.js. Luego, invocará la función startBrowser() y pasará la instancia de navegador creada a nuestro controlador de página, que dirigirá sus acciones. Añada el siguiente código:

      ./book-scraper/index.js

      const browserObject = require('./browser');
      const scraperController = require('./pageController');
      
      //Start the browser and create a browser instance
      let browserInstance = browserObject.startBrowser();
      
      // Pass the browser instance to the scraper controller
      scraperController(browserInstance)
      

      Guarde y cierre el archivo.

      Cree su tercer archivo .js, pageController.js:

      pageController.js controla el proceso de extracción de datos. Utiliza la instancia de navegador para controlar el archivo pageScraper.js, que es donde se ejecutan todas las secuencias de comandos de extracción de datos. Más adelante, lo utilizará para especificar de qué categoría de libros desea extraer datos. Sin embargo, por ahora, solo desea asegurarse de que puede abrir Chromium y navegar a una página web:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              await pageScraper.scraper(browser); 
      
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      Este código exporta una función que toma la instancia de navegador y la pasa a otra función denominada scrapeAll(). Esta función, a su vez, pasa la instancia como argumento a pageScraper.scraper(), que la utiliza para extraer datos de las páginas.

      Guarde y cierre el archivo.

      Por último, cree su cuarto archivo .js, pageScraper.js:

      Aquí, creará un literal de objeto con una propiedad url y un método scraper(). La propiedad url es la URL de la página web de la que desea extraer datos y el método scraper() contiene el código que realizará la extracción en sí, aunque, en este punto, simplemente navega a una URL. Añada el siguiente código:

      ./book-scraper/pageScraper.js

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              await page.goto(this.url);
      
          }
      }
      
      module.exports = scraperObject;
      

      Puppeteer cuenta con un método newPage() que crea una instancia de página nueva en el navegador. Se pueden hacer varias cosas con las instancias de página. En nuestro método scraper(), creó una instancia de página y, luego, utilizó el método page.goto() para navegar a la página de inicio de books.toscrape.com.

      Guarde y cierre el archivo.

      Con esto, completó la estructura de archivos de su programa. El primer nivel del árbol de directorios de su proyecto tendrá este aspecto:

      Output

      . ├── browser.js ├── index.js ├── node_modules ├── package-lock.json ├── package.json ├── pageController.js └── pageScraper.js

      Ahora, ejecute el comando npm run start y observe la ejecución de su aplicación de extracción de datos:

      Abrirá una instancia de Chromium y una página nueva en el navegador, y se dirigirá a books.toscrape.com de forma automática.

      En este paso, creó una aplicación de Puppeteer que abre Chromium y carga la página de inicio de una librería en línea ficticia: books.toscrape.com. En el siguiente paso, extraerá datos de todos los libros de esa página de inicio.

      Antes de añadir más funcionalidades a su aplicación de extracción de datos, abra su navegador web preferido y diríjase de forma manual a la página de inicio de Books to Scrape. Navegue por el sitio para comprender cómo están estructurados los datos.

      Imagen del sitio web de Books to Scrape

      Verá una sección de categorías a la izquierda y los libros exhibidos a la derecha. Al hacer clic en un libro, el navegador se dirige a una nueva URL que muestra información pertinente sobre ese libro en particular.

      En este paso, replicará este comportamiento, pero mediante programación: automatizará la tarea de navegar por el sitio web y consumir sus datos.

      Primero, si analiza el código fuente de la página de inicio utilizando las herramienta de desarrollo de su navegador, notará que la página enumera los datos de cada libro debajo de una etiqueta section. Dentro de la etiqueta section, cada libro está debajo de una etiqueta list (li), y es aquí donde se encuentra el enlace a la página separada del libro, su precio y su disponibilidad.

      Vista del código fuente de books.to.scrape con herramientas de desarrollo

      Extraerá datos de estas URL de los libros, filtrará los libros que están disponibles y navegará a la página separada de cada libro para extraer sus datos.

      Vuelva a abrir su archivo pageScraper.js:

      Añada el siguiente contenido resaltado: Anidará otro bloque de await dentro de await page.goto(this.url);:

      ./book-scraper/pageScraper.js

      
      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              // Wait for the required DOM to be rendered
              await page.waitForSelector('.page_inner');
              // Get the link to all the required books
              let urls = await page.$$eval('section ol > li', links => {
                  // Make sure the book to be scraped is in stock
                  links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                  // Extract the links from the data
                  links = links.map(el => el.querySelector('h3 > a').href)
                  return links;
              });
              console.log(urls);
          }
      }
      
      module.exports = scraperObject;
      
      

      En este bloque de código, invocó el método page.waitForSelector(). Con esto, esperó el div que contiene toda la información relacionada con el libro que se debe reproducir en el DOM y, luego, invocó el método page.$$eval(). Este método obtiene el elemento de URL con el selector section ol li (asegúrese de que siempre se devuelva solo una cadena o un número con los métodos page.$eval() y page.$$eval()).

      Cada libro puede tener dos estados: disponible, In Stock, o no disponible, Out of stock. Solo desea extraer datos de los libros In Stock. Como page.$$eval() devuelve una matriz de todos los elementos coincidentes, la filtró para asegurarse de estar trabajando únicamente con los libros que están disponibles. Para hacerlo, buscó y evaluó la clase .instock.availability. Luego, identificó la propiedad href de los enlaces del libro y la devolvió del método.

      Guarde y cierre el archivo.

      Vuelva a ejecutar su aplicación:

      El navegador se abrirá, se dirigirá a la página web y se cerrará cuando la tarea se complete. Ahora, observe su consola; contendrá todas las URL extraídas:

      Output

      > book-scraper@1.0.0 start /Users/sammy/book-scraper > node index.js Opening the browser...... Navigating to http://books.toscrape.com... [ 'http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html', 'http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html', 'http://books.toscrape.com/catalogue/soumission_998/index.html', 'http://books.toscrape.com/catalogue/sharp-objects_997/index.html', 'http://books.toscrape.com/catalogue/sapiens-a-brief-history-of-humankind_996/index.html', 'http://books.toscrape.com/catalogue/the-requiem-red_995/index.html', 'http://books.toscrape.com/catalogue/the-dirty-little-secrets-of-getting-your-dream-job_994/index.html', 'http://books.toscrape.com/catalogue/the-coming-woman-a-novel-based-on-the-life-of-the-infamous-feminist-victoria-woodhull_993/index.html', 'http://books.toscrape.com/catalogue/the-boys-in-the-boat-nine-americans-and-their-epic-quest-for-gold-at-the-1936-berlin-olympics_992/index.html', 'http://books.toscrape.com/catalogue/the-black-maria_991/index.html', 'http://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html', 'http://books.toscrape.com/catalogue/shakespeares-sonnets_989/index.html', 'http://books.toscrape.com/catalogue/set-me-free_988/index.html', 'http://books.toscrape.com/catalogue/scott-pilgrims-precious-little-life-scott-pilgrim-1_987/index.html', 'http://books.toscrape.com/catalogue/rip-it-up-and-start-again_986/index.html', 'http://books.toscrape.com/catalogue/our-band-could-be-your-life-scenes-from-the-american-indie-underground-1981-1991_985/index.html', 'http://books.toscrape.com/catalogue/olio_984/index.html', 'http://books.toscrape.com/catalogue/mesaerion-the-best-science-fiction-stories-1800-1849_983/index.html', 'http://books.toscrape.com/catalogue/libertarianism-for-beginners_982/index.html', 'http://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html' ]

      Es un gran comienzo, pero desea extraer todos los datos pertinentes de un libro determinado, no solo su URL. Ahora, utilizará estas URL para abrir cada página y extraer el título, el autor, el precio, la disponibilidad, el UPC, la descripción y la URL de la imagen de cada libro.

      Vuelva a abrir pageScraper.js:

      Añada el siguiente código, que recorrerá en bucle cada uno de los enlaces extraídos, abrirá una instancia de página nueva y, luego, obtendrá los datos pertinentes:

      ./book-scraper/pageScraper.js

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              // Wait for the required DOM to be rendered
              await page.waitForSelector('.page_inner');
              // Get the link to all the required books
              let urls = await page.$$eval('section ol > li', links => {
                  // Make sure the book to be scraped is in stock
                  links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                  // Extract the links from the data
                  links = links.map(el => el.querySelector('h3 > a').href)
                  return links;
              });
      
      
              // Loop through each of those links, open a new page instance and get the relevant data from them
              let pagePromise = (link) => new Promise(async(resolve, reject) => {
                  let dataObj = {};
                  let newPage = await browser.newPage();
                  await newPage.goto(link);
                  dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                  dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                  dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                      // Strip new line and tab spaces
                      text = text.textContent.replace(/(rnt|n|r|t)/gm, "");
                      // Get the number of stock available
                      let regexp = /^.*((.*)).*$/i;
                      let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                      return stockAvailable;
                  });
                  dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                  dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                  dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                  resolve(dataObj);
                  await newPage.close();
              });
      
              for(link in urls){
                  let currentPageData = await pagePromise(urls);
                  // scrapedData.push(currentPageData);
                  console.log(currentPageData);
              }
      
          }
      }
      
      module.exports = scraperObject;
      

      Tiene una matriz de todas las URL. Lo que desea hacer es recorrer en bucle esta matriz, abrir una URL en una página nueva, extraer los datos de esa página, cerrarla y abrir una nueva para la siguiente URL de la matriz. Tenga en cuenta que envolvió este código en una promesa. Esto se debe a que desea esperar a que se complete cada acción de su bucle. Por lo tanto, cada promesa abre una URL nueva y no se resuelve hasta que el programa haya extraído todos sus datos y la instancia de esa página se haya cerrado.

      Advertencia: Tenga en cuenta que esperó la promesa utilizando un bucle for-in. Puede utilizar cualquier otro bucle, pero evite recorrer en iteración sus matrices de URL con métodos de iteración de matrices, como forEach, o cualquier otro método que utilice una función de devolución de llamada. Esto se debe a que la función de devolución de llamada debe pasar, primero, por la cola de devolución de llamadas y el bucle de evento, por lo tanto, se abrirán varias instancias de la página a la vez. Esto consumirá mucha más memoria.

      Observe con mayor detenimiento su función pagePromise. Primero, su extractor creó una página nueva para cada URL y, luego, utilizó la función page.$eval() para apuntar los selectores a los detalles pertinentes que deseaba extraer de la página nueva. Algunos de los textos contienen espacios en blanco, pestañas, líneas nuevas y otros caracteres no alfanuméricos, que eliminó utilizando una expresión regular. Luego, anexó el valor de cada dato extraído de esta página a un objeto y resolvió ese objeto.

      Guarde y cierre el archivo.

      Vuelva a ejecutar la secuencia de comandos:

      El navegador abre la página de inicio y, luego, abre la página de cada libro y registra los datos extraídos de cada una de ellas. Se imprimirá este resultado en su consola:

      Output

      Opening the browser...... Navigating to http://books.toscrape.com... { bookTitle: 'A Light in the Attic', bookPrice: '£51.77', noAvailable: '22', imageUrl: 'http://books.toscrape.com/media/cache/fe/72/fe72f0532301ec28892ae79a629a293c.jpg', bookDescription: "It's hard to imagine a world without A Light in the Attic. [...]', upc: 'a897fe39b1053632' } { bookTitle: 'Tipping the Velvet', bookPrice: '£53.74', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/08/e9/08e94f3731d7d6b760dfbfbc02ca5c62.jpg', bookDescription: `"Erotic and absorbing...Written with starling power."--"The New York Times Book Review " Nan King, an oyster girl, is captivated by the music hall phenomenon Kitty Butler [...]`, upc: '90fa61229261140a' } { bookTitle: 'Soumission', bookPrice: '£50.10', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/ee/cf/eecfe998905e455df12064dba399c075.jpg', bookDescription: 'Dans une France assez proche de la nôtre, [...]', upc: '6957f44c3847a760' } ...

      En este paso, extrajo datos pertinentes de cada libro de la página de inicio de books.toscrape.com, pero podría añadir muchas funcionalidades más. Por ejemplo, todas las páginas de libros están paginadas; ¿cómo puede obtener libros de estas otras páginas? Además, observó que hay categorías de libros en el lado izquierdo del sitio web; ¿y si no desea extraer todos los libros, sino solo los libros de un género en particular? Ahora, añadirá estas funciones.

      Las páginas de books.toscrape.com que están paginadas tienen un botón next debajo de su contenido; las páginas que no lo están, no.

      Utilizará la presencia de este botón para determinar si una página está paginada o no. Como los datos de todas las páginas tienen la misma estructura y marcación, no escribirá un extractor para cada página posible. En su lugar, utilizará la técnica de recursión.

      Primero, debe realizar algunos cambios en la estructura de su código para admitir la navegación recursiva a varias páginas.

      Vuelva a abrir pagescraper.js:

      Agregará una nueva función denominada scrapeCurrentPage() a su método scraper(). Esta función contendrá todo el código que extrae datos de una página en particular y, luego, hará clic en el botón “siguiente”, si está presente. Añada el siguiente código resaltado:

      ./book-scraper/pageScraper.js scraper()

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              let scrapedData = [];
              // Wait for the required DOM to be rendered
              async function scrapeCurrentPage(){
                  await page.waitForSelector('.page_inner');
                  // Get the link to all the required books
                  let urls = await page.$$eval('section ol > li', links => {
                      // Make sure the book to be scraped is in stock
                      links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                      // Extract the links from the data
                      links = links.map(el => el.querySelector('h3 > a').href)
                      return links;
                  });
                  // Loop through each of those links, open a new page instance and get the relevant data from them
                  let pagePromise = (link) => new Promise(async(resolve, reject) => {
                      let dataObj = {};
                      let newPage = await browser.newPage();
                      await newPage.goto(link);
                      dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                      dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                      dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                          // Strip new line and tab spaces
                          text = text.textContent.replace(/(rnt|n|r|t)/gm, "");
                          // Get the number of stock available
                          let regexp = /^.*((.*)).*$/i;
                          let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                          return stockAvailable;
                      });
                      dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                      dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                      dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                      resolve(dataObj);
                      await newPage.close();
                  });
      
                  for(link in urls){
                      let currentPageData = await pagePromise(urls);
                      scrapedData.push(currentPageData);
                      // console.log(currentPageData);
                  }
                  // When all the data on this page is done, click the next button and start the scraping of the next page
                  // You are going to check if this button exist first, so you know if there really is a next page.
                  let nextButtonExist = false;
                  try{
                      const nextButton = await page.$eval('.next > a', a => a.textContent);
                      nextButtonExist = true;
                  }
                  catch(err){
                      nextButtonExist = false;
                  }
                  if(nextButtonExist){
                      await page.click('.next > a');   
                      return scrapeCurrentPage(); // Call this function recursively
                  }
                  await page.close();
                  return scrapedData;
              }
              let data = await scrapeCurrentPage();
              console.log(data);
              return data;
          }
      }
      
      module.exports = scraperObject;
      
      

      Primero, configure la variable nextButtonExist en false y, luego, compruebe si el botón aparece. Si el botón next está presente, configure nextButtonExists en true y proceda a hacer clic en él“; luego, se invoca esta función de forma recursiva.

      Cuando nextButtonExists tiene un valor false, devuelve la matriz de scrapedData como de costumbre.

      Guarde y cierre el archivo.

      Vuelva a ejecutar su secuencia de comandos:

      Esto puede tomar un tiempo; después de todo, su aplicación está extrayendo datos de más de 800 libros. Puede cerrar el navegador o presionar CTRL + C para cancelar el proceso.

      Ha maximizado las capacidades de su extractor de datos, pero, en el proceso, generó un problema. Ahora, el problema no es tener pocos datos, sino demasiados. En el siguiente paso, ajustará su aplicación para filtrar la extracción de datos por categoría de libros.

      Para extraer datos por categoría, deberá modificar sus archivos pageScraper.js y pageController.js.

      Abra pageController.js en un editor de texto:

      nano pageController.js
      

      Invoque el extractor de modo que solo extraiga libros de viajes. Añada el siguiente código:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              let scrapedData = {};
              // Call the scraper for different set of books to be scraped
              scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
              await browser.close();
              console.log(scrapedData)
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      Ahora, está pasando dos parámetros a su método pageScraper.scraper(); el segundo es la categoría de libros de la que desea extraer datos, que, en este ejemplo, es Travel. Pero su archivo pageScraper.js todavía no reconoce este parámetro. También deberá ajustar este archivo.

      Guarde y cierre el archivo.

      Abra pageScraper.js:

      Agregue el siguiente código, que añadirá su parámetro de categoría, navegará a esa página de categoría y, luego, comenzará a extraer datos de los resultados paginados:

      ./book-scraper/pageScraper.js

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser, category){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              // Select the category of book to be displayed
              let selectedCategory = await page.$$eval('.side_categories > ul > li > ul > li > a', (links, _category) => {
      
                  // Search for the element that has the matching text
                  links = links.map(a => a.textContent.replace(/(rnt|n|r|t|^s|s$|Bs|sB)/gm, "") === _category ? a : null);
                  let link = links.filter(tx => tx !== null)[0];
                  return link.href;
              }, category);
              // Navigate to the selected category
              await page.goto(selectedCategory);
              let scrapedData = [];
              // Wait for the required DOM to be rendered
              async function scrapeCurrentPage(){
                  await page.waitForSelector('.page_inner');
                  // Get the link to all the required books
                  let urls = await page.$$eval('section ol > li', links => {
                      // Make sure the book to be scraped is in stock
                      links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                      // Extract the links from the data
                      links = links.map(el => el.querySelector('h3 > a').href)
                      return links;
                  });
                  // Loop through each of those links, open a new page instance and get the relevant data from them
                  let pagePromise = (link) => new Promise(async(resolve, reject) => {
                      let dataObj = {};
                      let newPage = await browser.newPage();
                      await newPage.goto(link);
                      dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                      dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                      dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                          // Strip new line and tab spaces
                          text = text.textContent.replace(/(rnt|n|r|t)/gm, "");
                          // Get the number of stock available
                          let regexp = /^.*((.*)).*$/i;
                          let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                          return stockAvailable;
                      });
                      dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                      dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                      dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                      resolve(dataObj);
                      await newPage.close();
                  });
      
                  for(link in urls){
                      let currentPageData = await pagePromise(urls);
                      scrapedData.push(currentPageData);
                      // console.log(currentPageData);
                  }
                  // When all the data on this page is done, click the next button and start the scraping of the next page
                  // You are going to check if this button exist first, so you know if there really is a next page.
                  let nextButtonExist = false;
                  try{
                      const nextButton = await page.$eval('.next > a', a => a.textContent);
                      nextButtonExist = true;
                  }
                  catch(err){
                      nextButtonExist = false;
                  }
                  if(nextButtonExist){
                      await page.click('.next > a');   
                      return scrapeCurrentPage(); // Call this function recursively
                  }
                  await page.close();
                  return scrapedData;
              }
              let data = await scrapeCurrentPage();
              console.log(data);
              return data;
          }
      }
      
      module.exports = scraperObject;
      

      Este bloque de código utiliza la categoría que pasó para obtener la URL en la que se encuentran los libros de esa categoría.

      El método page.$$eval() puede tomar argumentos al pasarlos como tercer parámetro al método $$eval() y definirlos como tales en la devolución de llamada de la siguiente manera:

      example page.$$eval() function

      page.$$eval('selector', function(elem, args){
          // .......
      }, args)
      

      Esto es lo que hizo en su código: pasó la categoría de libros de la que quería extraer datos, marcó todas las categorías para determinar cuál coincidía y, luego, devolvió la URL de esa categoría.

      Luego, utilizó esa URL para navegar a la página que muestra la categoría de libros de la que desea extraer datos utilizando el método page.goto(selectedCategory).

      Guarde y cierre el archivo.

      Vuelva a ejecutar su aplicación. Observará que navega a la categoría Travel, abre los libros de esa categoría de forma recursiva, página por página, y registra los resultados:

      En este paso, extrajo datos de varias páginas y, luego, de varias páginas de una categoría en particular. En el paso final, modificará su secuencia de comandos para extraer datos de varias categorías y, luego, guardará los datos extraídos en un archivo JSON convertido a cadena JSON.

      En este paso final, modificará su secuencia de comandos para extraer datos de todas las categorías que desee y, luego, cambiará la forma de su resultado. En lugar de registrar los resultados, los guardará en un archivo estructurado denominado data.json.

      Puede añadir más categorías para extraer de datos de forma rápida. Para hacerlo, solo se requiere una línea adicional por género.

      Abra pageController.js:

      Ajuste su código para incluir categorías adicionales. En el ejemplo a continuación, se suman HistoricalFiction y Mystery a nuestra categoría Travel existente:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              let scrapedData = {};
              // Call the scraper for different set of books to be scraped
              scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
              scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');
              scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');
              await browser.close();
              console.log(scrapedData)
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      Guarde y cierre el archivo.

      Vuelva a ejecutar la secuencia de comandos y observe cómo extrae datos de las tres categorías:

      Ahora que el extractor está totalmente operativo, su paso final es guardar los datos en un formato más útil. Ahora, los almacenará en un archivo JSON utilizando el módulo fs de Node.js.

      Primero, vuelva a abrir pageController.js:

      Añada el siguiente código resaltado:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      const fs = require('fs');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              let scrapedData = {};
              // Call the scraper for different set of books to be scraped
              scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
              scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');
              scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');
              await browser.close();
              fs.writeFile("data.json", JSON.stringify(scrapedData), 'utf8', function(err) {
                  if(err) {
                      return console.log(err);
                  }
                  console.log("The data has been scraped and saved successfully! View it at './data.json'");
              });
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      Primero, solicita el módulo fs de Node.js en pageController.js. Esto garantiza que pueda guardar sus datos como archivo JSON. Luego, añade código para que, cuando se complete la extracción de datos y se cierre el navegador, el programa cree un archivo nuevo denominado data.json. Tenga en cuenta que el contenido de data.json está convertido a cadena JSON. Por tanto, al leer el contenido de data.json, siempre se lo debe analizar como JSON antes de volver a utilizar los datos.

      Guarde y cierre el archivo.

      Con esto, ha creado una aplicación de extracción de datos web que extrae libros de varias categorías y, luego, almacena los datos extraídos en un archivo JSON. A medida que su aplicación se vaya volviendo más compleja, puede resultarle conveniente almacenar los datos extraídos en una base de datos o proporcionarlos a través de una API. La manera en la que se consumen estos datos es una decisión personal.

      Conclusión

      En este tutorial, creó un rastreador web que extrajo datos de varias páginas de forma recursiva y los guardó en un archivo JSON. En resumen, aprendió una nueva manera de automatizar la recopilación de datos de sitios web.

      Puppeteer cuenta con muchas características que no se abordaron en este tutorial. Para obtener más información, consulte Cómo usar Puppeteer para controlar Chrome desatendido de forma sencilla. También puede consultar la documentación oficial de Puppeteer.



      Source link

      Cómo monitorear los anuncios y las rutas de BGP utilizando BGPalerter en Ubuntu 18.04


      El autor seleccionó el COVID-19 Relief Fund para que reciba una donación como parte del programa Write for DOnations.

      Introducción

      BGP (Protocolo de puerta de enlace de borde) es uno de los protocolos principales responsable de redirigir paquetes a través de Internet; por lo tanto, si presenta errores, se pueden producir interrupciones importantes. Por ejemplo, en 2019, un pequeño proveedor de servicios de Internet hizo una mala configuración de BGP que, lamentablemente, se propagó de manera ascendente y dejó importantes partes de Cloudflare y AWS sin conexión durante más de una hora.  Además, hace un año, se realizó un ataque a BGP para interceptar el tráfico a un proveedor de monederos de criptomonedas y robar los fondos de clientes desprevenidos.

      BGPalerter es una herramienta de monitoreo de la red de BGP de código abierto que puede proporcionar alertas en tiempo real sobre la actividad de BGP, incluso la visibilidad de rutas y los anuncios de nuevas rutas, así como actividades potencialmente nefastas, como intercepciones o fugas en las rutas. BGPalerter ingiere automáticamente la información de redireccionamiento de la red disponible públicamente, lo que significa que no necesita tener ningún nivel de acceso con privilegios ni integración en las redes que quiere controlar.

      Nota: BGPalerter ingiere automáticamente la información de redireccionamiento de la red disponible públicamente, lo que significa que no necesita tener ningún nivel de acceso con privilegios ni integración en las redes que quiere controlar. Todo el monitoreo cumple plenamente con la Ley de Uso Indebido de Computadoras (Computer Misuse Act), la Ley de Fraude y Abuso de Computadoras (Computer Fraud and Abuse Act), y otras leyes similares.  Sin embargo, se recomienda revelar de forma responsable cualquier hallazgo relevante al operador de la red afectado.

      En este tutorial, instalará y configurará BGPalerter para monitorear sus redes importantes, a fin de detectar actividades potencialmente sospechosas.

      Requisitos previos

      Para completar este tutorial, necesitará lo siguiente:

      Para cada dispositivo o red, necesitará identificar la dirección IP individual, el intervalo de la dirección IP o el número de sistema autónomo del que es parte. Esto se abarca en el paso 1.

      Una vez que tenga todo esto listo, inicie sesión en su servidor como non-root user.

      Paso 1: Identificar las redes que se quieren monitorear

      En este paso, identificará los detalles pertinentes de las redes que quiere monitorear.

      BGPalerter puede monitorear sobre la base de direcciones IP individuales o prefijos de red. También puede monitorear redes enteras sobre la base de su número de sistema autónomo (AS), que es un identificador global único de una red propiedad de una entidad administrativa en particular.

      Para encontrar esta información, puede utilizar el servicio de búsqueda IP-to-ASN de WHOIS, proporcionado por el servicio de inteligencia de amenazas Team Cymru. Se trata de un servidor de WHOIS personalizado diseñado para buscar información de dirección IP y enrutamiento de red.

      Si no tiene whois instalado, puede instalarlo usando el siguiente comando:

      • sudo apt update
      • sudo apt install whois

      Una vez que haya confirmado que whois está instalado, comience por realizar una búsqueda de la dirección IP de su propio servidor, utilizando el argumento -h para especificar un servidor personalizado:

      • whois -h whois.cymru.com your-ip-address

      Esto generará un resultado similar al siguiente, que muestra el nombre y el número de AS del que su servidor es parte. Normalmente, será el AS de su proveedor de alojamiento del servidor, por ejemplo, DigitalOcean.

      Output

      AS | IP | AS Name 14061 | your-ip-address | DIGITALOCEAN-ASN, US

      A continuación, puede realizar una búsqueda para identificar el prefijo de la red o el intervalo del que su servidor es parte. Para hacerlo, agregue el argumento -p a su solicitud:

      • whois -h whois.cymru.com " -p your-ip-address"

      El resultado será muy similar al comando anterior, pero, ahora, mostrará el prefijo de dirección IP al que pertenece la dirección IP de su servidor:

      Output

      AS | IP | BGP Prefix | AS Name 14061 | your-ip-address | 157.230.80.0/20 | DIGITALOCEAN-ASN, US

      Por último, puede buscar más detalles del AS del que forma parte su servidor, incluyendo la región geográfica y la fecha de asignación.

      Sustituya el número de AS que identificó usando los comandos anteriores. Utiliza el argumento -v para habilitar el resultado detallado, lo que garantiza que se muestren todos los detalles relevantes:

      • whois -h whois.cymru.com " -v as14061"

      El resultado mostrará más información sobre el AS:

      Output

      AS | CC | Registry | Allocated | AS Name 14061 | US | arin | 2012-09-25 | DIGITALOCEAN-ASN, US

      identificó detalles clave sobre las redes que quiere monitorear. Tome nota de estos detalles en algún lugar, ya que los necesitará más adelante. A continuación, comenzará a configurar BGPalerter.

      Paso 2: Crear un usuario sin privilegios para BGPalerter

      En este paso, creará una nueva cuenta de usuario sin privilegios para BGPalerter, dado que el programa no necesita ejecutarse con privilegios sudo/root.

      Primero, cree un usuario nuevo con contraseña deshabilitada:

      • sudo adduser --disabled-password bgpalerter

      No necesita configurar una contraseña o una clave de SSH, dado que solo utilizará este usuario como una cuenta de servicio para ejecutar/mantener BGPalerter.

      Inicie sesión con el usuario nuevo utilizando su:

      Ahora, está conectado con su usuario nuevo:

      bgpalerter@droplet:/home/user$
      

      Utilice el comando cd para dirigirse al directorio de inicio de su usuario nuevo:

      bgpalerter@droplet:/home/user$ cd
      bgpalerter@droplet:~$
      

      Creó un usuario sin privilegios nuevo para BGPalerter. A continuación, instalará y configurará BGPalerter en su sistema.

      Paso 3: Instalar y configurar BGPalerter

      En este paso, instalará y configurará BGPalerter. Asegúrese de seguir conectado con su usuario sin privilegios nuevo.

      Primero, debe identificar la última versión de BGPalerter, a fin de asegurarse de descargar la más reciente. Diríjase a la página de Lanzamientos de BGPalerter y copie el enlace de descarga de la versión de Linux x64 más reciente.

      Ahora, puede descargar una copia de BGPalerter usando wget, asegurándose de sustituir el enlace de descarga correcto:

      • wget https://github.com/nttgin/BGPalerter/releases/download/v1.24.0/bgpalerter-linux-x64

      Una vez que el archivo haya terminado de descargarse, márquelo como ejecutable:

      • chmod +x bgpalerter-linux-x64

      A continuación, compruebe que BGPalerter se haya descargado e instalado correctamente comprobando el número de la versión:

      • ./bgpalerter-linux-x64 --version

      Esto dará como resultado el número de la versión actual:

      Output

      1.24.0

      Para poder ejecutar BGPalerter adecuadamente, deberá definir las redes que desea monitorear en un archivo de configuración. Cree y abra el archivo prefixes.yml en su editor de texto favorito:

      En este archivo de configuración, especificará cada una de las direcciones IP individuales, los intervalos de dirección IP y los números de AS que quiere monitorear.

      Añada el siguiente ejemplo y ajuste los valores de configuración según sea necesario usando la información de la red que identificó en el paso 1:

      ~/prefixes.yml

      your-ip-address/32:
        description: My Server
        asn:
          - 14061
        ignoreMorespecifics: false
      
      157.230.80.0/20:
        description: IP range for my Server
        asn:
          - 14061
        ignoreMorespecifics: false
      
      options:
        monitorASns:
          '14061':
            group: default
      

      Puede monitorear todos los intervalos de dirección IP o números de AS que quiera. Para monitorear direcciones IP individuales, represéntelas utilizando /32 para IPv4 y /128 para IPv6.

      El valor ignoreMorespecifics se utiliza para determinar si BGPalerter debe ignorar la actividad de las rutas más específicas (pequeñas) que la que está monitoreando. Por ejemplo, si está monitoreando /20 y se detecta un cambio de enrutamiento para /24 en su interior, se considera que es más específica. En la mayoría de los casos, no es recomendable ignorarlas si está monitoreando una red grande con varios prefijos de cliente delegados, sin embargo, puede ayudar a reducir interferencias de fondo.

      Ahora, puede ejecutar BGPalerter por primera vez para comenzar a monitorear sus redes:

      Si BGPalerter se inicia correctamente, verá un resultado similar al siguiente. Tenga en cuenta que, a veces, el monitoreo puede tardar unos minutos en iniciarse:

      Output

      Impossible to load config.yml. A default configuration file has been generated. BGPalerter, version: 1.24.0 environment: production Loaded config: /home/bgpalerter/config.yml Monitoring 157.230.80.0/20 Monitoring your-ip-address/32 Monitoring AS 14061

      BGPalerter se seguirá ejecutando hasta que lo detenga usando Ctrl+C.

      En el siguiente paso, interpretará algunas de las alertas que BGPalerter puede generar.

      Paso 4: Interpretar alertas de BGPalerter

      En este paso, revisará algunas alertas de BGPalerter de ejemplo. BGPalerter emitirá alertas en la fuente de salida principal, y también, de forma opcional, en cualquier otro extremo de información que pueda configurarse dentro de config.yml, como se describe en la documentación de BGPalerter.

      De manera predeterminada, BGPalerter monitorea y alerta sobre lo siguiente:

      • Intercepciones de ruta: se produce cuando un AS anuncia un prefijo que no está permitido, lo que provoca que el tráfico se enrute de forma errónea. Puede ser un ataque deliberado o un error de configuración accidental.

      • Pérdida de visibilidad de la ruta: una ruta se considera visible cuando la mayoría de los enrutadores de BGP de Internet pueden redirigir de forma fiable hacia ella. La pérdida de visibilidad significa que su red no está disponible, por ejemplo, si su emparejamiento de BGP ha dejado de funcionar.

      • Nuevos anuncios de subprefijos: sucede cuando un AS comienza a anunciar un prefijo que es más pequeño de lo que se espera. Esto puede indicar un cambio de configuración previsto, un error de configuración accidental o, en algunos casos, un ataque.

      • Actividad en su AS: normalmente, se refiere a anuncios de rutas nuevas. Una ruta se considera “nueva” si BGPalerter todavía no la conoce.

      A continuación, se presentan algunas alertas de ejemplo, junto con una descripción breve de su significado:

      Alert #1

      The prefix 203.0.113.0/24 is announced by AS64496 instead of AS65540
      

      Esta alerta muestra pruebas de una intercepción de la ruta, donde AS64496 anunció 203.0.113.0/24 cuando se esperaba que se anuncie AS65540. Esto es un claro indicio de un error de configuración que conduce a una fuga de la ruta o a una intercepción deliberada de un atacante.

      Alert #2

      The prefix 203.0.113.0/24 has been withdrawn. It is no longer visible from 6 peers
      

      Esta alerta indica que la red 203.0.113.0/24 ya no está visible. Esto puede deberse a un problema de enrutamiento previo o a un fallo de energía en un enrutador.

      Alert #3

      A new prefix 203.0.113.0/25 is announced by AS64496. It should be instead 203.0.113.0/24 announced by AS64496
      

      Esta alerta indica que se anunció un prefijo más específico en un caso en el que no estaba previsto, por ejemplo, si se anunció /25 cuando se esperaba /24. Es muy probable que esto sea un error de configuración, sin embargo, en algunos casos, puede indicar la intercepción de la ruta.

      Alert #4

      AS64496 is announcing 192.0.2.0/24 but this prefix is not in the configured list of announced prefixes
      

      Por último, esta alerta indica que AS64496 anunció un prefijo que BGPalerter todavía no conoce. Esto podría deberse a que usted está anunciando legítimamente un nuevo prefijo o podría ser un indicio de un error de configuración que haya provocado que anunciara accidentalmente un prefijo propiedad de otra persona.

      En este paso, revisó algunas alertas de BGPalerter de ejemplo. A continuación, configurará BGPalerter para que se ejecute de forma automática en el arranque.

      Paso 5: Iniciar BGPalerter en el arranque

      En este último paso, configurará BGPalerter para que se ejecute en el arranque.

      Asegúrese de seguir conectado con su usuario sin privilegios nuevo y, luego, abra el editor de crontab:

      Luego, añada la siguiente línea a la parte inferior del archivo de crontab:

      crontab

      @reboot sleep 10; screen -dmS bgpalerter "./bgpalerter-linux-x64"
      

      Cada vez que su sistema se arranque, esto creará una sesión screen separada denominada ‘bgpalerter’ en la que se iniciará BGPalerter.

      Guarde y salga del editor de crontab. Ahora, es conveniente que reinicie su sistema para asegurarse de que BGPalerter se inicie correctamente en el arranque.

      Primero, cierre la sesión de su usuario de BGPalerter:

      Luego, proceda con un reinicio normal del sistema:

      Una vez que su sistema se haya reiniciado, vuelva a iniciar sesión en su servidor y utilice su para volver a acceder a su usuario de BGPalerter:

      Luego, puede unirse a la sesión en cualquier momento para ver el resultado de BGPalerter:

      En este último paso, configuró BGPalerter para que se ejecute en el arranque.

      Conclusión

      En este artículo, configuró BGPalerter y lo utilizó para monitorear cambios de enrutamiento de BGP en las redes.

      Si quiere hacer que BGPalerter sea más fácil de usar, puede configurarlo para que envíe alertas a un canal de slack a través de un webhook:

      Si quiere obtener más información sobre BGP, pero no tiene acceso a un entorno de producción de BGP, puede utilizar DN42 para realizar pruebas con BGP en un entorno seguro y aislado:



      Source link

      Cómo usar Go con MongoDB utilizando el controlador de Go de MongoDB


      El autor seleccionó la Free Software Foundation para recibir una donación como parte del programa Write for DOnations.

      Introducción

      Después de muchos años de basarse en soluciones desarrolladas por la comunidad, MongoDB anunció que estaban trabajando en un controlador oficial para Go.  Este nuevo controlador estuvo listo para la producción en marzo de 2019 con el lanzamiento de la versión v1.0.0 y, desde entonces, se ha estado actualizando de forma continua.

      Al igual que los demás controladores oficiales de MongoDB, el controlador de Go es característico del lenguaje de programación Go y ofrece una manera sencilla de utilizar MongoDB como solución de base de datos para programas de Go. Está completamente integrado con la API de MongoDB y presenta todas las funciones de consulta, indexación y agregación de la API, así como otras funciones avanzadas. A diferencia de las bibliotecas de terceros, los ingenieros de MongoDB lo respaldarán por completo, por lo que puede tener la seguridad de que se seguirá desarrollando y manteniendo.

      En este tutorial, empezará a utilizar el controlador de oficial de Go de MongoDB. Instalará el controlador, establecerá conexión con una base de datos de MongoDB y realizará varias operaciones CRUD. En el proceso, creará un programa de administración de tareas para gestionar tareas a través de la línea de comandos.

      Requisitos previos

      Para este tutorial, necesitará lo siguiente:

      • Go instalado en su máquina y un espacio de trabajo de Go configurado conforme a Cómo instalar Go y configurar un entorno de programación local. En este tutorial, el proyecto se denominará tasker. Necesitará tener instalada la versión 1.11 o superior de Go en su máquina y los módulos de Go habilitados.
      • MongoDB instalada para su sistema operativo conforme a Cómo instalar MongoDB. MongoDB 2.6 o superior, que es la versión mínima que admite el controlador de Go de MongoDB.

      Si utiliza Go v1.11 o 1.12, asegúrese de que los módulos de Go estén habilitados fijando la variable de entorno GO111MODULE en on como se indica a continuación:

      Para obtener más información sobre la implementación de variables de entorno, consulte el tutorial Cómo leer y establecer variables de entorno y shell.

      Los comandos y el código que se muestran en esta guía se probaron con Go v1.14.1 y MongoDB v3.6.3.

      Paso 1: Instalar el controlador de Go de MongoDB

      En este paso, instalará el paquete del controlador de Go para MongoDB y lo importará en su proyecto. También establecerá conexión con su base de datos de MongoDB y verificará el estado de la conexión.

      Proceda a crear un directorio nuevo para este tutorial en su sistema de archivos:

      Una vez que haya establecido el directorio de su proyecto, posiciónese en él con el siguiente comando:

      A continuación, inicie el proyecto de Go con un archivo go.mod. Este archivo define los requisitos del proyecto y bloquea las dependencias en sus versiones correctas:

      Si el directorio de su proyecto está fuera de $GOPATH, debe especificar la ruta de importación de su módulo de la siguiente manera:

      • go mod init github.com/<your_username>/tasker

      En este punto, su archivo go.mod tendrá el siguiente aspecto:

      go.mod

      module github.com/<your_username>/tasker
      
      go 1.14
      

      Agregue el controlador de Go de MongoDB como dependencia de su proyecto utilizando el siguiente comando:

      • go get go.mongodb.org/mongo-driver

      Verá un resultado como el siguiente:

      Output

      go: downloading go.mongodb.org/mongo-driver v1.3.2 go: go.mongodb.org/mongo-driver upgrade => v1.3.2

      En este punto, su archivo go.mod tendrá el siguiente aspecto:

      go.mod

      module github.com/<your_username>/tasker
      
      go 1.14
      
      require go.mongodb.org/mongo-driver v1.3.1 // indirect
      

      A continuación, cree un archivo main.go en el root de su proyecto y ábralo en su editor de texto:

      Para comenzar a usar el controlador, importe los siguientes paquetes en su archivo main.go:

      main.go

      package main
      
      import (
          "context"
          "log"
      
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      

      Aquí, añade los paquetes mongo y options, que proporciona el controlador de Go de MongoDB.

      A continuación, después de realizar sus importaciones, cree un cliente de MongoDB nuevo y establezca conexión con su servidor de MongoDB en ejecución:

      main.go

      . . .
      var collection *mongo.Collection
      var ctx = context.TODO()
      
      func init() {
          clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
          client, err := mongo.Connect(ctx, clientOptions)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      mongo.Connect() acepta los objetos Context y options.ClientOptions, que se utiliza para establecer la cadena de conexión y otros ajustes del controlador. Puede consultar las opciones de configuración disponibles en la documentación del paquete de opciones.

      El contexto es como un tiempo de espera o un plazo que indica cuándo una operación debe dejar de funcionar y reanudarse. Ayuda a prevenir la degradación del rendimiento en los sistemas de producción cuando determinadas operaciones se ejecutan con lentitud. En este código, está pasando context.TODO() para indicar que no está seguro de qué contexto usar en este momento, pero que planea añadir uno en el futuro.

      A continuación, vamos a asegurarnos de que su servidor de MongoDB se haya encontrado y conectado con éxito utilizando el método Ping.  Añada el siguiente código en la función init:

      main.go

      . . .
          log.Fatal(err)
        }
      
        err = client.Ping(ctx, nil)
        if err != nil {
          log.Fatal(err)
        }
      }
      

      Si hay algún error al establecer conexión con la base de datos, el programa debe bloquearse mientras intenta solucionar el problema, dado que no tiene sentido mantenerlo en ejecución sin una conexión activa con la base de datos.

      Añada el siguiente código para crear una base de datos:

      main.go

      . . .
        err = client.Ping(ctx, nil)
        if err != nil {
          log.Fatal(err)
        }
      
        collection = client.Database("tasker").Collection("tasks")
      }
      

      Creó una base de datos tasker y una colección task para almacenar las tareas que creará. También estableció collection como variable de nivel de paquete para poder reutilizar la conexión a la base de datos en todo el paquete.

      Guarde el archivo y ciérrelo.

      En este punto, todo main.go tiene la siguiente estructura:

      main.go

      package main
      
      import (
          "context"
          "log"
      
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      
      var collection *mongo.Collection
      var ctx = context.TODO()
      
      func init() {
          clientOptions := options.Client().ApplyURI("mongodb://localhost:27017/")
          client, err := mongo.Connect(ctx, clientOptions)
          if err != nil {
              log.Fatal(err)
          }
      
          err = client.Ping(ctx, nil)
          if err != nil {
              log.Fatal(err)
          }
      
          collection = client.Database("tasker").Collection("tasks")
      }
      

      Configuró su programa para establecer conexión con su servidor de MongoDB usando el controlador de Go. En el siguiente paso, procederá a crear su programa de administración de tareas.

      Paso 2: Crear un programa de CLI

      En este paso, instalará el famoso paquete cli para ayudar a desarrollar su programa de administración de tareas. Ofrece una interfaz que puede aprovechar para crear rápidamente herramientas de línea de comandos modernas. Por ejemplo, este paquete proporciona la capacidad de definir subcomandos para su programa con el fin de obtener una experiencia de línea de comandos más similar a git.

      Ejecute el siguiente comando para añadir el paquete como dependencia:

      • go get github.com/urfave/cli/v2

      Luego, vuelva a abrir su archivo main.go:

      Añada el siguiente código resaltado a su archivo main.go:

      main.go

      package main
      
      import (
          "context"
          "log"
          "os"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Importó el paquete cli como se indicó. También importó el paquete os, que usará para pasar argumentos de línea de comandos a su programa:

      Añada el siguiente código después de la función init para crear su programa de CLI y hacer que su código se compile:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:     "tasker",
              Usage:    "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{},
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      Este fragmento crea un programa de CLI denominado tasker y añade una descripción de uso breve que se imprimirá al ejecutar el programa. El segmento de Commands es donde agregará comandos para su programa. El comando Run redistribuye el segmento de argumentos al comando correspondiente.

      Guarde y cierre su archivo.

      Este es el comando que necesita para compilar y ejecutar el programa:

      Verá el siguiente resultado:

      Output

      NAME: tasker - A simple CLI program to manage your tasks USAGE: main [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)

      El programa ejecuta y muestra texto de ayuda, que es útil para aprender sobre lo que puede hacer el programa y cómo usarlo.

      En los siguientes pasos, mejorará la utilidad de su programa añadiendo subcomandos para ayudar a gestionar sus tareas en MongoDB.

      Paso 3: Crear una tarea

      En este paso, agregará un subcomando a su programa de CLI utilizando el paquete cli. Al final de esta sección, podrá añadir una tarea nueva a la base de datos de MongoDB utilizando un comando add nuevo en su programa de CLI.

      Comience por abrir su archivo main.go:

      Luego, importe los paquetes go.mongodb.org/mongo-driver/bson/primitive, time y errors:

      main.go

      package main
      
      import (
          "context"
          "errors"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Luego, cree una nueva estructura para representar una sola tarea en la base de datos e insértela inmediatamente antes de la función main:

      main.go

      . . .
      type Task struct {
          ID        primitive.ObjectID `bson:"_id"`
          CreatedAt time.Time          `bson:"created_at"`
          UpdatedAt time.Time          `bson:"updated_at"`
          Text      string             `bson:"text"`
          Completed bool               `bson:"completed"`
      }
      . . .
      

      Utilizó el paquete primitive para establecer el tipo de ID de cada tarea, dado que MongoDB utiliza ObjectID para el campo _id por defecto. Otro comportamiento predeterminado de MongoDB es que el nombre de campo en minúsculas se utiliza como la clave de cada campo exportado cuando se está serializado, pero esto se puede modificar utilizando las etiquetas de estructura bson.

      A continuación, cree una función que reciba una instancia de Task y la guarde en la base de datos. Añada este fragmento después de la función main:

      main.go

      . . .
      func createTask(task *Task) error {
          _, err := collection.InsertOne(ctx, task)
        return err
      }
      . . .
      

      El método collection.InsertOne() inserta la tarea proporcionada en la colección de la base de datos y devuelve la ID del documento que se insertó. Como no necesita esta ID, la descarta al asignar al operador de guion bajo.

      El siguiente paso es añadir un comando nuevo a su programa de administración de tareas para crear tareas nuevas. Lo llamaremos add:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      

      Todos los comandos nuevos que se añaden a su programa de CLI se colocan dentro del segmento de Commands. Cada uno consta de un nombre, una descripción de uso y una acción. Este es el código que se ejecutará al ejecutar el comando.

      Con este código, se recoge el primer argumento en add y se utiliza para establecer la propiedad Text de una nueva instancia de Task a la vez que se asignan los valores predeterminados correspondientes de las demás propiedades. La tarea nueva se pasa posteriormente a createTask, que inserta la tarea en la base de datos y devuelve nil si todo está bien, lo que provoca que el comando se cierre.

      Guarde y cierre su archivo.

      Pruébelo al añadir algunas tareas utilizando el comando add. Si todo es correcto, no verá errores en la pantalla:

      • go run main.go add "Learn Go"
      • go run main.go add "Read a book"

      Ahora que puede añadir tareas correctamente, implementaremos una manera de mostrar todas las tareas que añadió a la base de datos.

      Paso 4: Enumerar todas las tareas

      Los documentos de una colección se pueden enumerar utilizando el método collection.Find(), que espera un filtro y un indicador a un valor en el que se pueda decodificar el resultado.  El valor que devuelve es un cursor, que proporciona un flujo de documentos que se pueden iterar y decodificar de a uno a la vez. El cursor se cierra una vez que se agota.

      Abra su archivo main.go:

      Asegúrese de importar el paquete bson:

      main.go

      package main
      
      import (
          "context"
          "errors"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
      )
      . . .
      

      Luego, cree las siguientes funciones inmediatamente después de createTask:

      main.go

      . . .
      func getAll() ([]*Task, error) {
        // passing bson.D{{}} matches all documents in the collection
          filter := bson.D{{}}
          return filterTasks(filter)
      }
      
      func filterTasks(filter interface{}) ([]*Task, error) {
          // A slice of tasks for storing the decoded documents
          var tasks []*Task
      
          cur, err := collection.Find(ctx, filter)
          if err != nil {
              return tasks, err
          }
      
          for cur.Next(ctx) {
              var t Task
              err := cur.Decode(&t)
              if err != nil {
                  return tasks, err
              }
      
              tasks = append(tasks, &t)
          }
      
          if err := cur.Err(); err != nil {
              return tasks, err
          }
      
        // once exhausted, close the cursor
          cur.Close(ctx)
      
          if len(tasks) == 0 {
              return tasks, mongo.ErrNoDocuments
          }
      
          return tasks, nil
      }
      

      BSON (JSON con codificado binario) es la forma en que los documentos se representan en una base de datos de MongoDB, y el paquete bson es lo que nos ayuda a trabajar con los objetos de BSON en Go. El tipo bson.D que se utiliza en la función getAll() representa un documento de BSON y se utiliza cuando el orden de las propiedades es importante. Al pasar bson.D{{}} como su filtro a filterTasks(), indica que desea hacer coincidir todos los documentos de la colección.

      En la función filterTasks(), itera sobre el cursor que devuelve el método collection.Find() y decodifica cada documento en una instancia de Task. Luego, cada Task se anexa al segmento de tareas creadas al principio de la función. Una vez que el cursor se agota, se cierra y se devuelve el segmento de tasks.

      Antes de crear un comando para enumerar todas las tareas, crearemos una función de ayuda que tome un segmento de tasks e imprima en la salida estándar. Utilizará el paquete color para dar color al resultado.

      Para poder utilizar este paquete, instálelo con lo siguiente:

      • go get gopkg.in/gookit/color.v1

      Verá el siguiente resultado:

      Output

      go: downloading gopkg.in/gookit/color.v1 v1.1.6 go: gopkg.in/gookit/color.v1 upgrade => v1.1.6

      Impórtelo en su archivo main.go junto con el paquete fmt:

      main.go

      package main
      
      import (
          "context"
          "errors"
        "fmt"
          "log"
          "os"
          "time"
      
          "github.com/urfave/cli/v2"
          "go.mongodb.org/mongo-driver/bson"
          "go.mongodb.org/mongo-driver/bson/primitive"
          "go.mongodb.org/mongo-driver/mongo"
          "go.mongodb.org/mongo-driver/mongo/options"
          "gopkg.in/gookit/color.v1"
      )
      . . .
      

      Luego, cree una función printTasks nueva después de su función main:

      main.go

      . . .
      func printTasks(tasks []*Task) {
          for i, v := range tasks {
              if v.Completed {
                  color.Green.Printf("%d: %sn", i+1, v.Text)
              } else {
                  color.Yellow.Printf("%d: %sn", i+1, v.Text)
              }
          }
      }
      . . .
      

      Esta función printTasks toma un segmento de tasks, itera sobre cada una de ellas y las imprime en la salida estándar utilizando el color verde para indicar las tareas completadas y el amarillo para las tareas incompletas.

      Proceda a agregar las siguientes líneas resaltadas para crear un comando all nuevo en el segmento de Commands. Este comando imprimirá todas las tareas añadidas a la salida estándar:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      
      . . .
      

      El comando all obtiene todas las tareas presentes en la base de datos y las imprime en la salida estándar. Si no hay tareas presentes, en su lugar, se imprime una línea de comandos para añadir una tarea nueva.

      Guarde y cierre su archivo.

      Compile y ejecute su programa con el comando all:

      Enumerará todas las tareas que añadió hasta ahora:

      Output

      1: Learn Go 2: Read a book

      Ahora que puede ver todas las tareas de la base de datos, añadiremos la capacidad de marcar una tarea como completa en el siguiente paso.

      Paso 5: Completar una tarea

      En este paso, creará un subcomando nuevo denominado done que le permitirá marcar una tarea existente en la base de datos como completada. Para marcar una tarea como completada, puede utilizar el método collection.FindOneAndUpdate(). Le permite localizar un documento en una colección y actualizar todas sus propiedades o algunas de ellas. Este método requiere un filtro para localizar el documento y un documento de actualización para describir la operación. Los dos se crean utilizando tipos bson.D.

      Comience por abrir su archivo main.go:

      A continuación, inserte el siguiente fragmento después de la función de filterTasks:

      main.go

      . . .
      func completeTask(text string) error {
          filter := bson.D{primitive.E{Key: "text", Value: text}}
      
          update := bson.D{primitive.E{Key: "$set", Value: bson.D{
              primitive.E{Key: "completed", Value: true},
          }}}
      
          t := &Task{}
          return collection.FindOneAndUpdate(ctx, filter, update).Decode(t)
      }
      . . .
      

      La función coincide con el primer documento en el que la propiedad de texto es igual al parámetro text. El documento update especifica que la propiedad completed se establezca en true. Si hay un error en la operación FindOneAndUpdate(), se devolverá mediante completeTask(). De lo contrario, se devuelve nil.

      A continuación, añadiremos un comando done a su programa de CLI que marca una tarea como completada:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
              },
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      
      . . .
      

      Utiliza el argumento que se pasó al comando done para encontrar el primer documento cuya propiedad text coincida. Si se encuentra, la propiedad completed del documento se establece en true.

      Guarde y cierre su archivo.

      Luego, ejecute su programa con el comando done:

      • go run main.go done "Learn Go"

      Si vuelve a utilizar el comando all, observará que la tarea que se marcó como completada, ahora, se imprime en color verde.

      Captura de pantalla del resultado de la terminal después de completar una tarea

      A veces, solo desea ver las tareas que todavía no se completaron. Añadiremos esa función a continuación.

      Paso 6: Mostrar únicamente tareas pendientes

      En este paso, incorporará código para obtener las tareas pendientes de la base de datos utilizando el controlador de MongoDB. Las tareas pendientes son las que tienen la propiedad completed establecida en false.

      Vamos a añadir una función nueva para obtener las tareas que todavía no se completaron. Abra su archivo main.go:

      Luego, añada este fragmento después de la función completeTask:

      main.go

      . . .
      func getPending() ([]*Task, error) {
          filter := bson.D{
              primitive.E{Key: "completed", Value: false},
          }
      
          return filterTasks(filter)
      }
      . . .
      

      Creó un filtro utilizando los paquetes bson y primitive del controlador de MongoDB, que devolverá los documentos que tengan la propiedad completed establecida en false. Luego, el segmento de tareas pendientes se devuelve al autor de la llamada.

      En lugar de crear un comando nuevo para enumerar las tareas pendientes, vamos a hacer que sea la acción predeterminada cuando se ejecute el programa sin comandos. Para hacerlo, vamos a añadir una propiedad Action al programa de la siguiente manera:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
      . . .
      

      La propiedad Action realiza una acción predeterminada cuando el programa se ejecuta sin ningún subcomando. Aquí es donde se aplica la lógica para enumerar las tareas pendientes. Se invoca la función get getPending() y las tareas resultantes se imprimen en la salida estándar utilizando printTasks(). Si no hay tareas pendientes, se muestra un aviso en su lugar, que anima al usuario a añadir una nueva tarea usando el comando add.

      Guarde y cierre su archivo.

      Ahora, al ejecutar el programa sin añadir ningún comando, se enumerarán todas las tareas pendientes en la base de datos:

      Verá el siguiente resultado:

      Output

      1: Read a book

      Ahora que puede enumerar tareas incompletas, vamos a añadir otro comando que permite ver únicamente las tareas completadas.

      Paso 7: Mostrar tareas completadas

      En este paso, agregará un subcomando finished nuevo que obtiene las tareas completadas de la base de datos y las muestra en la pantalla. Esto incluye filtrar y devolver las tareas que tengan la propiedad completed establecida en true.

      Abra su archivo main.go:

      Luego, añada el siguiente código al final de su archivo:

      main.go

      . . .
      func getFinished() ([]*Task, error) {
          filter := bson.D{
              primitive.E{Key: "completed", Value: true},
          }
      
          return filterTasks(filter)
      }
      . . .
      

      De forma similar a lo que hizo con la función getPending(), añadió una función getFinished() que devuelve un segmento de tareas completadas. En este caso, el filtro tiene la propiedad completed establecida en true, por lo tanto, solo se devolverán los documentos que coincidan con esta condición.

      A continuación, cree un comando finished que imprima todas las tareas completadas:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
                  {
                      Name:    "finished",
                      Aliases: []string{"f"},
                      Usage:   "list completed tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getFinished()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `done 'task'` to complete a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
              }
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      . . .
      

      El comando finished obtiene las tareas que tienen la propiedad completed establecida en true a través de la función getFinished() creada aquí. Luego, las pasa a la función printTasks para que se impriman en la salida estándar.

      Guarde y cierre su archivo.

      Ejecute el siguiente comando:

      Verá el siguiente resultado:

      Output

      1: Learn Go

      En el paso final, proporcionará a los usuarios la opción de eliminar tareas de la base de datos.

      Paso 8: Eliminar una tarea

      En este paso, agregará un subcomando delete para permitir que los usuarios puedan eliminar tareas de la base de datos. Para eliminar una tarea, utilizará el método collection.DeleteOne() del controlador de MongoDB. También utiliza un filtro para buscar el documento que se eliminará.

      Vuelva a abrir su archivo main.go una vez más:

      Añada esta función deleteTask para eliminar tareas de la base de datos directamente después de su función getFinished:

      main.go

      . . .
      func deleteTask(text string) error {
          filter := bson.D{primitive.E{Key: "text", Value: text}}
      
          res, err := collection.DeleteOne(ctx, filter)
          if err != nil {
              return err
          }
      
          if res.DeletedCount == 0 {
              return errors.New("No tasks were deleted")
          }
      
          return nil
      }
      . . .
      

      Este método deleteTask toma un argumento de cadena que representa el elemento de tarea que se va a eliminar. Se crea un filtro para buscar el elemento de tarea que tenga la propiedad text establecida en el argumento de cadena. Pasa el filtro al método DeleteOne() que coincide con el elemento de la colección y lo elimina.

      Puede verificar la propiedad DeletedCount en el resultado del método DeleteOne para verificar si se eliminaron documentos. Si el filtro no encuentra un documento para eliminar, DeletedCount será cero y, en ese caso, puede devolver un error.

      Ahora, añada un comando rm nuevo como se indica en la sección resaltada:

      main.go

      . . .
      func main() {
          app := &cli.App{
              Name:  "tasker",
              Usage: "A simple CLI program to manage your tasks",
              Action: func(c *cli.Context) error {
                  tasks, err := getPending()
                  if err != nil {
                      if err == mongo.ErrNoDocuments {
                          fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                          return nil
                      }
      
                      return err
                  }
      
                  printTasks(tasks)
                  return nil
              },
              Commands: []*cli.Command{
                  {
                      Name:    "add",
                      Aliases: []string{"a"},
                      Usage:   "add a task to the list",
                      Action: func(c *cli.Context) error {
                          str := c.Args().First()
                          if str == "" {
                              return errors.New("Cannot add an empty task")
                          }
      
                          task := &Task{
                              ID:        primitive.NewObjectID(),
                              CreatedAt: time.Now(),
                              UpdatedAt: time.Now(),
                              Text:      str,
                              Completed: false,
                          }
      
                          return createTask(task)
                      },
                  },
                  {
                      Name:    "all",
                      Aliases: []string{"l"},
                      Usage:   "list all tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getAll()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `add 'task'` to add a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:    "done",
                      Aliases: []string{"d"},
                      Usage:   "complete a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          return completeTask(text)
                      },
                  },
                  {
                      Name:    "finished",
                      Aliases: []string{"f"},
                      Usage:   "list completed tasks",
                      Action: func(c *cli.Context) error {
                          tasks, err := getFinished()
                          if err != nil {
                              if err == mongo.ErrNoDocuments {
                                  fmt.Print("Nothing to see here.nRun `done 'task'` to complete a task")
                                  return nil
                              }
      
                              return err
                          }
      
                          printTasks(tasks)
                          return nil
                      },
                  },
                  {
                      Name:  "rm",
                      Usage: "deletes a task on the list",
                      Action: func(c *cli.Context) error {
                          text := c.Args().First()
                          err := deleteTask(text)
                          if err != nil {
                              return err
                          }
      
                          return nil
                      },
                  },
              }
          }
      
          err := app.Run(os.Args)
          if err != nil {
              log.Fatal(err)
          }
      }
      . . .
      

      Al igual que con todos los demás subcomandos añadidos anteriormente, el comando rm utiliza su primer argumento para buscar una tarea en la base de datos y la elimina.

      Guarde y cierre su archivo.

      Puede enumerar tareas pendientes ejecutando su programa sin pasar ningún subcomando de la siguiente manera:

      Output

      1: Read a book

      Al ejecutar el subcomando rm en la tarea de "Read a book", se eliminará de la base de datos:

      • go run main.go rm "Read a book"

      Si vuelve a enumerar todas las tareas pendientes, observará que la tarea "Read a book" ya no aparece y, en su lugar, se muestra un aviso para añadir una nueva tarea:

      Output

      Nothing to see here Run `add 'task'` to add a task

      En este paso, añadió una función para eliminar tareas de la base de datos.

      Conclusión

      Creó correctamente un programa de línea de comandos de administración de tareas y aprendió los conceptos básicos del uso del controlador de Go de MongoDB en el proceso.

      Asegúrese de consultar la documentación completa del controlador de Go de MongoDB en GoDoc para obtener más información sobre las funciones que proporciona su utilización. La documentación que describe el uso de agregaciones o transacciones puede resultarle particularmente interesante.

      Puede ver el código final de este tutorial en este repositorio de GitHub.



      Source link