One place for hosting & domains

      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

      Información sobre las bases de datos relacionales


      Introducción

      Los sistemas de administración de bases de datos (DBMS) son programas que permiten a los usuarios interactuar con una base de datos. Les permiten controlar el acceso a una base de datos, escribir datos, ejecutar consultas y realizar otras tareas relacionadas con la administración de bases de datos.

      Sin embargo, para realizar cualquiera de estas tareas, los DBMS deben tener algún tipo de modelo subyacente que defina la organización de los datos. El modelo relacional es un enfoque para la organización de datos que se ha utilizado ampliamente en programas de software de bases de datos desde su concepción a fines de la década de 1960; a tal grado que, a la fecha de redacción de este artículo, cuatro de los cinco DBMS más populares son relacionales.

      Este artículo conceptual describe la historia del modelo relacional, la manera en que se organizan los datos en las bases de datos relacionales y cómo se utilizan hoy en día.

      Historia del modelo relacional

      Las bases de datos son conjuntos de información, o datos, modelados de forma lógica. Cualquier recopilación de datos es una base de datos, independientemente de cómo o dónde se almacene. Incluso un gabinete de archivos con información sobre nómina es una base de datos, al igual que una pila de formularios de pacientes hospitalizados o la recopilación de información sobre clientes de una empresa repartida en varias ubicaciones. Antes de que almacenar y administrar datos con computadoras se convirtiera en una práctica común, las bases de datos físicas como estas eran lo único con lo que contaban las organizaciones gubernamentales y empresariales que necesitaban almacenar información.

      A mediados del siglo XX, los desarrollos en las ciencias de la computación dieron lugar a máquinas con mayor capacidad de procesamiento y almacenamiento, tanto local como externo. Estos avances hicieron que los especialistas en ciencias de la computación comenzaran a reconocer el potencial que tenían estas máquinas para almacenar y administrar cantidades de datos cada vez más grandes.

      Sin embargo, no había teorías sobre cómo las computadoras podían organizar datos de manera significativa y lógica. Una cosa es almacenar datos no ordenados en una máquina, pero es mucho más complicado diseñar sistemas que permitan agregar, recuperar, clasificar y administrar esos datos de forma sistemática y práctica. La necesidad de contar con un marco de trabajo lógico para almacenar y organizar datos dio lugar a varias propuestas sobre cómo utilizar las computadoras para administrar datos.

      Uno de los primeros modelos de bases de datos fue el modelo jerárquico, en el que los datos se organizan con una estructura de árbol similar a la de los sistemas de archivos modernos. El siguiente ejemplo muestra el aspecto que podría tener el diseño de una parte de una base de datos jerárquica utilizada para categorizar animales:

      Ejemplo de base de datos jerárquica: categorización de animales

      El modelo jerárquico se implementó ampliamente en los primeros sistemas de administración de bases de datos, pero resultaba poco flexible. En este modelo, a pesar de que los registros individuales pueden tener varios “elementos secundarios”, cada uno puede tener un solo “elemento primario” en la jerarquía. Es por eso que estas primeras bases de datos jerárquicas se limitaban a representar relaciones “uno a uno” y “uno a varios”. Esta carencia de relaciones “varios a varios” podía provocar problemas al trabajar con puntos de datos que se deseaba asociar a más de un elemento primario.

      A fines de los años 60, Edgar F. Codd, un especialista en ciencias de la computación que trabajaba en IBM, diseñó el modelo relacional de administración de bases de datos. El modelo relacional de Codd permitía que los registros individuales se relacionaran con más de una tabla y, de esta manera, posibilitaba las relaciones “varios a varios” entre los puntos de datos además de las relaciones “uno a varios”. Esto proporcionó más flexibilidad que los demás modelos existentes a la hora de diseñar estructuras de base de datos y permitió que los sistemas de gestión de bases de datos relacionales (RDBMS) pudieran satisfacer una gama mucho más amplia de necesidades empresariales.

      Codd propuso un lenguaje para la administración de datos relacionales, conocido como Alfa, que influyó en el desarrollo de los lenguajes de bases de datos posteriores. Dos colegas de Codd en IBM, Donald Chamberlin y Raymond Boyce, crearon un lenguaje similar inspirado en Alpha. Lo llamaron SEQUEL, abreviatura de Structured English Query Language (Lenguaje de consulta estructurado en inglés), pero debido a una marca comercial existente, lo abreviaron a SQL (conocido formalmente como* Lenguaje de consulta estructurado*).

      Debido a las limitaciones de hardware, las primeras bases de datos relacionales eran prohibitivamente lentas y el uso de la tecnología tardó un tiempo en generalizarse. Pero a mediados de los años ochenta, el modelo relacional de Codd se había implementado en varios productos comerciales de administración de bases de datos, tanto de IBM como de sus competidores. Estos proveedores también siguieron el liderazgo de IBM al desarrollar e implementar sus propios dialectos de SQL. Para 1987, tanto el Instituto Nacional Estadounidense de Estándares (American National Standards Institute) como la Organización Internacional de Normalización (International Organization for Standardization) habían ratificado y publicado normas para SQL, lo que consolidó su estado como el lenguaje aceptado para la administración de RDBMS.

      Gracias al uso extendido del modelo relacional en varias industrias, se lo comenzó a reconocer como el modelo estándar para la administración de datos. Incluso con el surgimiento de varias bases de datos NoSQL en los últimos años, las bases de datos relacionales siguen siendo las herramientas predominantes para almacenar y organizar datos.

      Cómo organizan los datos las bases de datos relacionales

      Ahora que tiene una idea general de la historia del modelo relacional, vamos a analizar en detalle cómo organiza los datos.

      Los elementos más fundamentales del modelo relacional son las relaciones, que los usuarios y los RDBMS modernos reconocen como tablas. Una relación es un conjunto de tuplas, o filas de una tabla, en el que cada una de ellas comparte un conjunto de atributos o columnas:

      Diagrama de ejemplo de la asociación entre las relaciones, las tuplas y los atributos

      Una columna es la estructura de organización más pequeña de una base de datos relacional y representa las distintas facetas que definen los registros en la tabla. De ahí proviene su nombre más formal: atributos. Se puede pensar que cada tupla es como una instancia única de cualquier tipo de personas, objetos, eventos o asociaciones que contenga la tabla.  Estas instancias pueden ser desde empleados de una empresa y ventas de un negocio en línea hasta resultados de pruebas de laboratorio. Por ejemplo, en una tabla que contiene registros de los maestros de una escuela, las tuplas pueden tener atributos como name, subjects, start_date, etc.

      Al crear una columna, especificamos un* tipo de datos *que determina el tipo de entradas que se permiten en esa columna. Los sistemas de administración de bases de datos relacionales (RDBMS) suelen implementar sus propios tipos de datos únicos, que pueden no ser directamente intercambiables con tipos de datos similares de otros sistemas. Algunos tipos de datos frecuentes son fechas, cadenas, enteros y booleanos.

      En el modelo relacional, cada tabla contiene, por lo menos, una columna que puede utilizarse para identificar de forma única cada fila, lo que se denomina clave primaria. Esto es importante, dado que significa que los usuarios no necesitan saber dónde se almacenan físicamente los datos en una máquina; en su lugar, sus DBMS pueden realizar un seguimiento de cada registro y devolverlo según corresponda. A su vez, significa que los registros no tienen un orden lógico definido y que los usuarios tienen la capacidad de devolver sus datos en cualquier orden y a través de los filtros que deseen.

      Si tiene dos tablas que desea asociar, una manera de hacerlo es con una clave externa. Una clave externa es, básicamente, una copia de la clave primaria de una tabla (la tabla “primaria”) insertada en una columna de otra tabla (la “secundaria”). El siguiente ejemplo destaca la relación entre dos tablas: una se utiliza para registrar información sobre los empleados de una empresa y la otra, para realizar un seguimiento de las ventas de la empresa. En este ejemplo, la clave primaria de la tabla EMPLOYEES se utiliza como clave externa de la tabla SALES:

      Diagrama de ejemplo de cómo la clave primaria de la tabla EMPLOYEE actúa como la clave externa de la tabla SALES

      Si intenta agregar un registro a la tabla secundaria y el valor ingresado en la columna de la clave externa no existe en la clave primaria de la tabla primaria, la instrucción de inserción no será válida. Esto ayuda a mantener la integridad del nivel de las relaciones, dado que las filas de ambas tablas siempre se relacionarán correctamente.

      Los elementos estructurales del modelo relacional ayudan a mantener los datos almacenados de forma organizada, pero el almacenamiento de datos solo es útil si se pueden recuperar. Para obtener información de un RDBMS, puede emitir una consulta o una solicitud estructurada de un conjunto de información. Como se mencionó anteriormente, la mayoría de las bases de datos relacionales utilizan SQL para administrar y consultar datos. SQL permite filtrar y manipular los resultados de las consultas con una variedad de cláusulas, predicados y expresiones, lo que, a su vez, permite controlar correctamente qué datos se mostrarán en el conjunto de resultados.

      Ventajas y limitaciones de las bases de datos relacionales

      Teniendo en cuenta la estructura organizativa subyacente de las bases de datos relacionales, consideremos algunas de sus ventajas y desventajas.

      Hoy en día, tanto SQL como las bases de datos que lo implementan se desvían del modelo relacional de Codd de diversas maneras. Por ejemplo, el modelo de Codd determina que cada fila de una tabla debe ser única, mientras que, por cuestiones de practicidad, la mayoría de las bases de datos relacionales modernas permiten la duplicación de filas. Algunas personas no consideran que las bases de datos SQL sean verdaderas bases de datos relacionales a menos que cumplan con las especificaciones de Codd del modelo relacional. Sin embargo, en términos prácticos, es probable que cualquier DBMS que utilice SQL y, por lo menos, se adhiera en cierta medida al modelo relacional se denomine “sistema de administración de bases de datos relacionales”.

      A pesar de que las bases de datos relacionales ganaron popularidad rápidamente, algunas de las deficiencias del modelo relacional comenzaron a hacerse evidentes a medida que los datos se volvieron más valiosos y las empresas comenzaron a almacenar más de ellos. Entre otras cosas, puede ser difícil escalar una base de datos relacional de forma horizontal.  Los términos escalado horizontal o escala horizontal se refieren a la práctica de agregar más máquinas a una pila existente para extender la carga y permitir un mayor tráfico y un procesamiento más rápido. Se suele contrastar con el escalado vertical, que implica la actualización del hardware de un servidor existente, generalmente, añadiendo más RAM o CPU.

      El motivo por el cual es difícil escalar una base de datos relacional de forma horizontal está relacionado con el hecho de que el modelo relacional se diseñó para garantizar coherencia, lo que significa que los clientes que realizan consultas en la misma base de datos siempre obtendrán los mismos datos. Al querer escalar una base de datos relacional de forma horizontal en varias máquinas, resulta difícil asegurar la coherencia, dado que los clientes pueden escribir datos en un nodo pero no en otros.  Es probable que haya un retraso entre la escritura inicial y el momento en que se actualicen los demás nodos para reflejar los cambios, lo que daría lugar a inconsistencias entre ellos.

      Otra limitación que presentan los RDBMS es que el modelo relacional se diseñó para administrar datos estructurados o alineados con un tipo de datos predeterminado o, por lo menos, organizado de una manera predeterminada para que se puedan ordenar o buscar de forma más sencilla. Sin embargo, con la difusión de los equipos informáticos personales y el auge de Internet a principios de la década de 1990, los datos no estructurados, como los mensajes de correo electrónico, las fotos, los videos, etc., se volvieron más comunes.

      Nada de esto significa que las bases de datos relacionales no sean útiles. Al contrario, después de más de 40 años, el modelo relacional sigue siendo el marco dominante para la administración de datos. Su prevalencia y longevidad indican que las bases de datos relacionales son una tecnología madura, lo que constituye una de sus principales ventajas. Hay muchas aplicaciones diseñadas para trabajar con el modelo relacional, así como muchos administradores de bases de datos profesionales expertos en bases de datos relacionales. También hay una amplia variedad de recursos disponibles en formato impreso y digital para quienes desean comenzar a utilizar bases de datos relacionales.

      Otra ventaja de las bases de datos relacionales es que casi todos los RDBMS admiten transacciones. Las transacciones constan de una o más instrucciones SQL individuales que se ejecutan en secuencia como una unidad de trabajo única. Las transacciones tienen un enfoque de todo o nada, lo que significa que todas las instrucciones SQL de una transacción deben ser válidas; de lo contrario, falla en su totalidad. Esto es muy útil para garantizar la integridad de los datos al realizar cambios en varias filas o tablas.

      Por último, las bases de datos relacionales son sumamente flexibles. Se han utilizado para crear una amplia variedad de aplicaciones distintas y siguen funcionando de forma eficiente incluso con grandes cantidades de datos. SQL también es sumamente potente y permite agregar y cambiar datos sobre la marcha, así como alterar la estructura de los esquemas y las tablas de las bases de datos sin afectar a los datos existentes.

      Conclusión

      Gracias a su flexibilidad y diseño para la integridad de los datos, más de cincuenta años después de su concepción inicial, las bases de datos relacionales siguen siendo la principal forma de administrar y almacenar datos.  A pesar del surgimiento de diversas bases de datos NoSQL en los últimos años, la comprensión del modelo relacional y la manera de trabajar con RDBMS es fundamental para cualquier persona que desee crear aplicaciones que aprovechen el poder de los datos.

      Para obtener más información sobre RDBMS populares de código abierto, le recomendamos consultar nuestra comparación de diversas bases de datos relacionales SQL de código abierto. Si desea obtener más información sobre las bases de datos en general, le recomendamos consultar nuestra biblioteca completa de contenidos relacionados con bases de datos.



      Source link

      Cómo utilizar el tipo de datos BLOB de MySQL para almacenar imágenes con PHP en Ubuntu 18.04


      El autor seleccionó Girls Who Code para recibir una donación como parte del programa Write for DOnations.

      Introducción

      El tipo de datos de gran objeto binario (BLOB) es un tipo de datos de MySQL que puede almacenar datos binarios como los de archivos de imagen, multimedia y PDF.

      Al crear aplicaciones que requieren una base de datos estrechamente acoplada donde las imágenes deben estar sincronizadas con los datos relacionados (por ejemplo, un portal de empleados, una base de datos de estudiantes o una aplicación financiera), puede resultarle conveniente almacenar imágenes como las de fotos y firmas de pasaportes de estudiantes en una base de datos de MySQL junto con otra información relacionada.

      Aquí es donde entra el tipo de datos BLOB de MySQL. Este enfoque de programación elimina la necesidad de crear un sistema de archivos independiente para almacenar imágenes. El esquema también centraliza la base de datos, haciéndola más portátil y segura porque los datos están aislados del sistema de archivos. Crear copias de seguridad también es más sencillo, ya que que puede crear un solo archivo MySQL dump que contenga todos sus datos.

      La recuperación de datos es más rápida y, al crear registros, podrá estar seguro de que las reglas de validación de datos y la integridad referencial se preserven, en especial al utilizar transacciones en MySQL.

      En este tutorial, utilizará el tipo de datos BLOB de MySQL para almacenar imágenes con PHP en Ubuntu 18.04.

      Requisitos previos

      Para completar esta guía, necesitará lo siguiente:

      Paso 1: Crear una base de datos

      Comenzará creando una base de datos de ejemplo para su proyecto. Para hacer esto, aplique SSH a su servidor y luego ejecute el siguiente comando para iniciar sesión en su servidor MySQL como root:

      Ingrese la contraseña root de su base de datos de MySQL y presione INTRO para continuar.

      Luego, ejecute el siguiente comando para crear una base de datos. En este tutorial, lo llamaremos test_company:

      • CREATE DATABASE test_company;

      Una vez que cree la base de datos, verá el siguiente resultado:

      Output

      Query OK, 1 row affected (0.01 sec)

      Luego, cree una cuenta test_user en el servidor de MySQL y recuerde reemplazar PASSWORD por una contraseña segura:

      • CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'PASSWORD';

      Verá el siguiente resultado:

      Output

      Query OK, 0 rows affected (0.01 sec)

      Para otorgar a test_user privilegios completos respecto de la base de datos test_company, ejecute lo siguiente:

      • GRANT ALL PRIVILEGES ON test_company.* TO 'test_user'@'localhost';

      Asegúrese de obtener el siguiente resultado:

      Output

      Query OK, 0 rows affected (0.01 sec)

      Por último, elimine la tabla de privilegios para que MySQL vuelva a cargar los permisos:

      Asegúrese de ver el siguiente resultado:

      Output

      Query OK, 0 rows affected (0.01 sec)

      Ahora que la base de datos test_company y test_user están listos, continúe creando una tabla products para almacenar productos de ejemplo. Más adelante, utilizará esta tabla para insertar y obtener registros a fin de demostrar cómo funciona BLOB de MySQL.

      Cierre sesión en el servidor de MySQL:

      Luego, vuelva a iniciar sesión con las credenciales de test_user que creó:

      Cuando se le solicite, ingrese la contraseña de test_user y presione ENTER para continuar. Luego, posiciónese en la base de datos test_company escribiendo lo siguiente:

      Una vez que seleccione la base de datos test_company, MySQL mostrará lo siguiente:

      Output

      Database changed

      Luego, cree una tabla products ejecutando lo siguiente:

      • CREATE TABLE `products` (product_id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(50), price DOUBLE, product_image BLOB) ENGINE = InnoDB;

      Con este comando se crea una tabla llamada products. La tabla tiene cuatro columnas:

      • product_id: esta columna utiliza un tipo de datos BIGINT para admitir una gran lista de productos hasta un máximo de 2⁶³-1 artículos. Se marca la columna como PRIMARY KEY para identificar productos de manera exclusiva. Para que MySQL administre la generación de nuevos identificadores para columnas insertadas, utilizó la palabra clave AUTO_INCREMENT.

      • product_name: esta columna contiene los nombres de los productos. Se utiliza el tipo de datos VARCHAR, ya que este campo generalmente administra alfanuméricos de hasta un máximo de 50 caracteres; el límite de 50 es solo un valor hipotético utilizado para de este tutorial.

      • price: para fines demostrativos, su tabla products contiene la columna price que permite almacenar el precio minorista de los productos. Dado que algunos productos pueden tener valores flotantes (por ejemplo 23.69, 45.36, 102.99), se utiliza el tipo de datos DOUBLE.

      • product_image: en esta columna se utiliza el tipo de datos BLOB para almacenar los datos binarios reales de las imágenes de los productos.

      Se utiliza el ENGINE de almacenamiento InnoDB para que la tabla admita una amplia gama de funciones, incluso transacciones de MySQL. Después de ejecutar esto para crear la tabla products, verá el siguiente resultado:

      Output

      Query OK, 0 rows affected (0.03 sec)

      Cierre la sesión de su servidor de MySQL:

      Verá el siguiente resultado:

      Output

      Bye

      La tabla products ahora está lista para almacenar algunos registros, incluidas las imágenes de los productos y, en el siguiente paso, la completará con algunos productos.

      Paso 2: Crear secuencias de comandos PHP para conectar y completar la base de datos

      En este paso, creará una secuencia de comandos PHP que se conectará a la base de datos MySQL que creó en el paso 1. La secuencia de comandos preparará tres productos de ejemplo y los insertará en la tabla products.

      Para crear el código PHP, abra un nuevo archivo con su editor de texto:

      • sudo nano /var/www/html/config.php

      Luego, ingrese la siguiente información en el archivo y reemplace PASSWORD por la contraseña test_user que creó en el paso 1:

      /var/www/html/config.php

      <?php
      
      define('DB_NAME', 'test_company');
      define('DB_USER', 'test_user');
      define('DB_PASSWORD', 'PASSWORD');
      define('DB_HOST', 'localhost');
      
      $pdo = new PDO("mysql:host=" . DB_HOST . "; dbname=" . DB_NAME, DB_USER, DB_PASSWORD);
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
      
      

      Guarde y cierre el archivo.

      En este archivo, utilizó cuatro constantes PHP para conectarse a la base de datos MySQL que creó en el paso 1:

      • DB_NAME : esta constante contiene el nombre de la base de datos test_company.

      • DB_USER : esta variable contiene el nombre de usuario test_user.

      • DB_PASSWORD : esta constante almacena la PASSWORD de MySQL de la cuenta test_user.

      • DB_HOST: esto representa el servidor en el que se ubica la base de datos. En este caso, utilizará el servidor localhost.

      Con la siguiente línea de su archivo se inicia un objeto de datos de PHP (PDO) y se conecta a la base de datos MySQL:

      ...
      $pdo = new PDO("mysql:host=" . DB_HOST . "; dbname=" . DB_NAME, DB_USER, DB_PASSWORD);
      ...
      

      Cerca del final del archivo, configuró algunos atributos PDO:

      • ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION: este atributo indica a PDO que inicie una excepción que se puede registrar para depuración.
      • ATTR_EMULATE_PREPARES, false: esta opción aumenta la seguridad al indicar al motor de la base de datos MySQL que realice la preparación en lugar de PDO.

      Incluirá el archivo /var/www/html/config.php en dos secuencias de comandos PHP que creará luego para insertar y recuperar registros respectivamente.

      Primero, cree la secuencia de comandos PHP /var/www/html/insert_products.php para insertar registros en la tabla de productos:

      • sudo nano /var/www/html/insert_products.php

      Luego, añada la siguiente información al archivo /var/www/html/insert_products.php:

      /var/www/html/insert_products.php

      <?php
      
      require_once 'config.php';
      
      $products = [];
      
      $products[] = [
                    'product_name' => 'VIRTUAL SERVERS',
                    'price' => 5,
                    'product_image' => file_get_contents("https://i.imgur.com/VEIKbp0.png")
                    ];
      
      $products[] = [
                    'product_name' => 'MANAGED KUBERNETES',
                    'price' => 30,
                    'product_image' => file_get_contents("https://i.imgur.com/cCc9Gw9.png")
                    ];
      
      $products[] = [
                    'product_name' => 'MySQL DATABASES',
                    'price' => 15,
                    'product_image' => file_get_contents("https://i.imgur.com/UYcHkKD.png" )
                    ];
      
      $sql = "INSERT INTO products(product_name, price, product_image) VALUES (:product_name, :price, :product_image)";
      
      foreach ($products as $product) {
          $stmt = $pdo->prepare($sql);
          $stmt->execute($product);
      }
      
      echo "Records inserted successfully";
      

      Guarde y cierre el archivo.

      En el archivo, incluyó el archivo config.php en la parte superior. Este es el primer archivo que creó para definir las variables de la base de datos y conectarse a la base de datos. El archivo también inicia un objeto PDO y lo almacena en una variable $pdo.

      Luego, creó una matriz de datos de los productos que se insertarán en la base de datos. Aparte de product_name y price, que se preparan como cadenas y valores numéricos respectivamente, la secuencia de comandos utiliza la función file_get_contents integrada de PHP para leer imágenes de una fuente externa y pasarlas como cadenas a la columna product_image.

      Luego, preparó una instrucción SQL y utilizó la instrucción foreach{...} de PHP para insertar cada producto en la base de datos.

      Para ejecutar el archivo /var/www/html/insert_products.php, realice la ejecución en la ventana de su navegador utilizando la siguiente URL. Recuerde reemplazar your-server-IP por la dirección IP pública de su servidor:

      http://your-server-IP/insert_products.php
      

      Después de ejecutar el archivo, verá un mensaje de éxito en su navegador confirmando que los registros se insertaron en la base de datos.

      Mensaje de éxito que indica que los registros se insertaron en la base de datos

      Insertó con éxito tres registros que contienen imágenes de productos en la tabla products. En el siguiente paso, creará una secuencia de comandos PHP para obtener estos registros y mostrarlos en su navegador.

      Paso 3: Mostrar la información de los productos de la base de datos MySQL

      Con la información e imágenes de los productos en la base de datos, ahora debe codificar otra secuencia de comandos PHP que consulta y muestra la información de los productos en una tabla HTML en su navegador.

      Para crear el archivo, escriba lo siguiente:

      • sudo nano /var/www/html/display_products.php

      Luego, ingrese la siguiente información en el archivo:

      /var/www/html/display_products.php

      <html>
        <title>Using BLOB and MySQL</title>
        <body>
      
        <?php
      
        require_once 'config.php';
      
        $sql = "SELECT * FROM products";
        $stmt = $pdo->prepare($sql);
        $stmt->execute();
        ?>
      
        <table border="1" align = 'center'> <caption>Products Database</caption>
          <tr>
            <th>Product Id</th>
            <th>Product Name</th>
            <th>Price</th>
            <th>Product Image</th>
          </tr>
      
        <?php
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            echo '<tr>';
            echo '<td>' . $row['product_id'] . '</td>';
            echo '<td>' . $row['product_name'] . '</td>';
            echo '<td>' . $row['price'] . '</td>';
            echo '<td>' .
            '<img src = "data:image/png;base64,' . base64_encode($row['product_image']) . '" width = "50px" height = "50px"/>'
            . '</td>';
            echo '</tr>';
        }
        ?>
      
        </table>
        </body>
      </html>
      

      Guarde los cambios del archivo y ciérrelo.

      Aquí, nuevamente incluyó el archivo config.php para establecer conexión con la base de datos. Luego, preparó y ejecutó una instrucción SQL utilizando PDO para obtener todos los elementos de la tabla products utilizando el comando SELECT * FROM products​​​.

      Luego, creó una tabla HTML y la completó con los datos de los productos utilizando la instrucción PHP while() {...}​​​. La línea $row = $stmt->fetch(PDO::FETCH_ASSOC)​​​ consulta la base de datos y almacena el resultado en la variable $row como matriz multidimensional, que luego se mostró en una columna de la tabla HTML utilizando la sintaxis $row['column_name']​​​.

      Las imágenes de la columna product_image se incluyen en el interior de las etiquetas <img src = "">. Se utilizan los atributos width y height para cambiar el tamaño de las imágenes por uno más pequeño que pueda caber en la columna de la tabla HTML.

      Para convertir los datos contenidos en el tipo de datos BLOB de vuelta en imágenes, se utilizan la función base64_encode de PHP integrada y la siguiente sintaxis para el esquema URI de datos:

      data:media_type;base64, base_64_encoded_data
      

      En este caso, imagen/png es media_type y la cadena codificada Base64 de la columna product_image es base_64_encoded_data.

      Luego, ejecute el archivo display_products.php en un navegador web escribiendo la siguiente dirección:

      http://your-server-IP/display_products.php
      

      Después de ejecutar el archivo display_products.php en su navegador, verá una tabla HTML con una lista de productos e imágenes asociados.

      Lista de productos de la base de datos MySQL

      Esto confirma que la secuencia de comandos de PHP para obtener imágenes de MySQL funciona según lo previsto.

      Conclusión

      A través de esta guía, utilizó el tipo de datos BLOB de MySQL para almacenar y mostrar imágenes con PHP en Ubuntu 18.04. También vio las ventajas básicas de almacenar imágenes en una base de datos respecto de hacerlo en un sistema de archivos. Entre ellas, se incluyen la portabilidad, la seguridad y la facilidad de respaldo. Si compila una aplicación, como un portal de estudiantes o una base de datos de empleados para los cuales se deban almacenar juntas la información y las imágenes relacionadas, esta tecnología puede resultarle muy útil.

      Para obtener más información sobre los tipos de datos compatibles en MySQL, consulte la guía de tipos de datos de MySQL. Si está interesado en más contenido relacionado con MySQL y PHP, consulte los siguientes tutoriales:



      Source link