Estamos escribiendo una aplicación de servidor que generará mosaicos de trama PNG basados en mapas vectoriales en línea. Utilice el raspado web con Puppeteer para obtener datos del mapa.
Contenido:
1 - Introducción. Mapas ráster estándar
2 - Continuación. Escribir un rasterizador simple para mapas vectoriales
3 - Un caso especial. Conectamos la tarjeta OverpassTurbo
Continuación
Y así llegamos al tema más interesante. Imagine que encontramos un sitio con un mapa que realmente queremos agregar a nuestro navegador. Hacemos todo de acuerdo con las instrucciones de la parte anterior . Abrimos la visualización de los contenidos del sitio, ¡y no hay imágenes! Absolutamente Bueno, un par de íconos y eso es todo. Y algún otro archivo de texto con una lista de coordenadas.
Felicidades, encontramos un mapa vectorial. En términos generales, su navegador lo procesa en tiempo real. Entonces ella no necesita ningún azulejo preparado en absoluto. Por un lado, no hay tantos mapas vectoriales hasta ahora. Pero esta tecnología es muy prometedora y con el tiempo pueden convertirse en muchas más. Bueno, lo descubrimos. Y sin embargo, ¿qué hacemos ahora?
En primer lugar, puede intentar descargar un navegador de una versión muy, muy antigua. Uno que no admite las funciones requeridas para representar el mapa. Es posible que se le muestre una versión diferente del sitio. Con mapa ráster. Bueno, lo que necesitas hacer con eso ya lo sabes.
Sin embargo, si este truco no funcionó, pero todavía desea obtener esta tarjeta y, además, no en el navegador del teléfono inteligente, es decir, en su navegador, entonces hay una manera.
Idea principal
Procederemos del hecho de que queremos obtener un mapa que se pueda abrir en cualquiera de los navegadores. Luego necesitamos un adaptador, un tipo de intermediario que generará mosaicos para nosotros en formato PNG.
Resulta que necesitas inventar una bicicleta desarrollar otro motor para visualizar datos vectoriales. Bueno, o puedes escribir un script que irá al sitio, permitiéndole dibujar su propio mapa vectorial por su cuenta. Y luego esperará la descarga, tomará una captura de pantalla, recortará y volverá al usuario. Quizás elegiré la segunda opción.
Para tomar capturas de pantalla, usaré un "navegador de control remoto": Chrome sin cabeza. Puede controlarlo utilizando el nodo Puppeteer de la biblioteca js. Puede aprender sobre los conceptos básicos para trabajar con esta biblioteca en este artículo .
Hola mundo O crea y personaliza un proyecto
Si aún no ha instalado Node.js, vaya a esta o esta página, seleccione su sistema operativo y complete la instalación de acuerdo con las instrucciones.
Cree una nueva carpeta para el proyecto y ábrala en la terminal.
$ cd /Mapshoter_habr
Comenzamos el gerente de crear un nuevo proyecto
$ npm init
Aquí puede especificar el nombre del proyecto ( nombre del paquete ), el nombre del archivo para ingresar a la aplicación ( punto de entrada ) y el nombre del autor ( autor ). Para todas las demás solicitudes, aceptamos los parámetros predeterminados: no ingresamos nada y solo presionamos Enter . Al final, presione yy Enter .
A continuación, instale los marcos necesarios para el trabajo. Express para crear un servidor y Puppeteer para trabajar con un navegador.
$ npm install express $ npm i puppeteer
Como resultado, el archivo de configuración del proyecto package.json aparece en la carpeta del proyecto. En mi caso, esto:
{ "name": "mapshoter_habr", "version": "1.0.0", "description": "", "main": "router.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "nnngrach", "license": "ISC", "dependencies": { "express": "^4.17.1", "puppeteer": "^1.18.1" } }
Agregaré la línea de inicio a la sección de scripts para iniciar nuestra aplicación de manera más conveniente.
"scripts": { "start": "node router.js", "test": "echo \"Error: no test specified\" && exit 1" },
Ahora cree dos archivos con la implementación de la funcionalidad básica. El primer archivo es el punto de entrada a la aplicación. En mi caso, router.js . Creará un servidor y hará el enrutamiento.
Ahora crea un segundo archivo. Él controlará el navegador y tomará capturas de pantalla. Lo tengo llamado mapshoter.js .
const puppeteer = require( 'puppeteer' ) async function makeTile( x, y, z ) {
Ejecute nuestro script y verifique su rendimiento. Para hacer esto, escriba la consola:
$ npm start
Aparece un mensaje que dice "El servidor se crea en el puerto 5000". Ahora abra un navegador en su computadora y vaya a la dirección local de nuestro servidor. En lugar de las coordenadas x, y, z, puede ingresar cualquier número. Entré en 1, 2, 3.
http://localhost:5000/1/2/3
Si todo se hace correctamente, aparecerá una captura de pantalla del sitio de Google.

Presione Ctrl + C en la consola para detener nuestro script.
¡Felicitaciones, la base de nuestra aplicación está lista! Creamos un servidor que acepta nuestras solicitudes html, toma una captura de pantalla y nos devuelve una imagen. Ahora es el momento de pasar a la implementación de los detalles.
Calcular las coordenadas
La idea es que el navegador abra un sitio con un mapa e ingrese las coordenadas del lugar que necesitamos en la barra de búsqueda. Después de hacer clic en el botón "Buscar", este lugar estará exactamente en el centro de la pantalla. Por lo tanto, será fácil cortar el área que necesitamos.
Pero primero, debe calcular las coordenadas del centro del mosaico en función de su número de serie. Haré esto basado en la fórmula para encontrar la esquina superior izquierda. Lo puse en la función getCoordinates () .
Y dado que para algunos sitios, además del centro del mosaico, también debe especificar sus bordes, entonces también los buscaré. Bueno, creemos un módulo separado para estos cálculos con el nombre geoTools.js . Aquí está su código:
Ahora estamos listos para comenzar a implementar el script para trabajar con el navegador. Veamos algunos escenarios de cómo se puede hacer esto.
Escenario 1 - Búsqueda de API
Comencemos con el caso más simple, cuando simplemente puede ingresar las coordenadas en la URL de la página del mapa. Por ejemplo, así:
https://nakarte.me/#m=5/50.28144/89.30666&l=O/Wp
Miremos el guión. Simplemente reemplace, elimine todo el contenido del archivo mapshoter.js y pegue el código a continuación.
En esta versión, al iniciar el navegador, especificamos parámetros adicionales que le permitirán iniciarse y funcionar en servidores Linux, como Heroku. Además, ahora reduciremos el tamaño de la ventana para que quepan en la pantalla la menor cantidad posible de mosaicos de mapas. Por lo tanto, aumentamos la velocidad de carga de la página.
A continuación, calculamos las coordenadas del centro del mosaico deseado. Los pegamos en la URL y hacemos clic en él. El mosaico aparece exactamente en el centro de la pantalla. Corta una pieza de 256x256 píxeles. Este será el mosaico que necesitamos. Solo queda devolverlo al usuario.
Antes de pasar al código, noto que para mayor claridad, todo el manejo de errores se ha eliminado del script.
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) {
Ahora ejecute nuestro script y vea el mapa para esta sección.
http://localhost:5000/24/10/5
Si todo se hace correctamente, el servidor debería devolver ese mosaico:

Para asegurarse de que no mezclemos nada al recortar, compare nuestro mosaico con el original de OpenStreetMaps.org

Escenario 2 - Buscar usando la interfaz del sitio
Sin embargo, no siempre es posible controlar una tarjeta a través de una línea de navegador. Bueno, en tales casos, nuestro script se comportará como un usuario real. Imprimirá las coordenadas en el cuadro de búsqueda y hará clic en el botón Buscar. Después de eso, eliminará el marcador del punto encontrado, que generalmente aparece en el centro de la pantalla. Y luego hará clic en los botones para aumentar o disminuir la escala hasta que alcance la deseada. Luego tomará una captura de pantalla y la devolverá al usuario.
Observo que generalmente después de la búsqueda se establece la misma escala. 15, por ejemplo. En nuestro ejemplo, esto no siempre sucede. Por lo tanto, reconoceremos el nivel de zoom de los parámetros de los elementos html en la página.
También en este ejemplo, buscaremos elementos de interfaz utilizando selectores XPath. ¿Pero cómo los reconoces?
Para hacer esto, abra la página requerida en el navegador y abra la barra de herramientas del desarrollador ( Ctll + Alt + I para Google Chrome). Presione el botón para seleccionar elementos. Hacemos clic en el elemento que le interesa (hice clic en el campo de búsqueda).

La lista de elementos se desplaza al que hizo clic y se resalta en azul. Haga clic en el botón con tres puntos a la izquierda del nombre.
En el menú emergente, seleccione Copiar. A continuación, si necesita un selector normal, haga clic en Copiar selector . Pero para el mismo ejemplo, utilizaremos el elemento Copiar XPath .

Ahora reemplace el contenido del archivo mapshoter.js con este código. En él, ya he recopilado selectores para todos los elementos de interfaz necesarios.
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z ) {
Ejecute nuestro script y siga el enlace. Si todo se hace correctamente, el script nos devolverá algo como este mosaico.
http://localhost:5000/1237/640/11

Optimización
En principio, los dos métodos descritos anteriormente son suficientes para conectarse a muchos sitios con mapas vectoriales. Pero si de repente necesita acceso a algún mapa nuevo, solo tendrá que modificar ligeramente la secuencia de comandos en el archivo mapshoter.js. Es decir, este método hace que sea muy fácil agregar nuevas tarjetas. Esto es de sus ventajas.
Pero también hay desventajas. Y el principal es la velocidad del trabajo. Solo compara. En promedio, se tarda aproximadamente 0,5 segundos en descargar un mosaico ráster normal. Si bien recibir una ficha de nuestro script en este momento, toma alrededor de 8 segundos.
¡Pero eso no es todo! Usamos el nodo js de un solo subproceso y nuestras solicitudes largas eventualmente bloquearán el subproceso principal, que desde el exterior se verá como una cola sincrónica regular. Y cuando intentamos descargar el mapa para toda la pantalla (en la que, por ejemplo, se colocan 24 mosaicos), es decir, existe el riesgo de encontrar un problema.
Y una cosa más. Algunos navegadores tienen un tiempo de espera: dejarán de cargar después de 30 segundos. Y esto significa que con la implementación actual solo 3-4 mosaicos tendrán tiempo para cargar. Bueno, veamos qué podemos hacer al respecto.
Probablemente la forma más obvia es simplemente aumentar el número de servidores en los que se ejecutará nuestro script. Por ejemplo, si tenemos 10 servidores, tendrán tiempo para procesar los mosaicos para toda la pantalla en 30 segundos. (Si no desea pagar mucho dinero, puede obtenerlo registrando varias cuentas gratuitas en Heroku)
En segundo lugar, todavía es posible implementar subprocesos múltiples en el nodo js utilizando el módulo worker_threads . Según mis observaciones, en un servidor con un procesador de un solo núcleo en una cuenta gratuita de Heroku, logro iniciar tres subprocesos. Tres transmisiones con un navegador separado en cada una, que pueden funcionar simultáneamente sin bloquearse entre sí. Para ser justos, noto que como resultado del aumento de la carga en el procesador, la velocidad de descarga de un mosaico incluso aumentó ligeramente. Sin embargo, si intenta descargar un mapa para toda la pantalla, luego de 30 segundos, más de la mitad del mapa tendrá tiempo para cargarse. Más de 12 azulejos. Ya mejor.
En tercer lugar. En la implementación actual de la secuencia de comandos, con cada solicitud, pasamos tiempo descargando el navegador Chrome y luego completándolo. Ahora crearemos un navegador por adelantado y transferiremos un enlace a él en mapshoter.js. Como resultado, la velocidad no cambiará para la primera solicitud. Pero para toda la descarga posterior, la velocidad de un mosaico se reduce a 4 segundos. Y después de 30 segundos, todo el mapa tiene tiempo de cargarse: las 24 fichas que se colocan en mi pantalla.
Bueno, si implementa todo esto, entonces el script puede volverse bastante viable. Entonces comencemos. Para un trabajo más simple con subprocesos múltiples, utilizaré el módulo de grupo de subprocesos de nodo de trabajo , una especie de envoltorio sobre subprocesos de trabajo. Vamos a instalarlo
$ npm install node-worker-threads-pool --save
Corrija el archivo router.js. Agregue la creación de un grupo de subprocesos. Los hilos serán de 3 piezas. Su código se describirá en el archivo worker.js , lo veremos más adelante. Mientras tanto, elimine el inicio del módulo de captura de pantalla directamente. En su lugar, agregaremos una nueva tarea al grupo de subprocesos. Comenzarán a procesarlo cuando alguno de los hilos se libere.
const express = require( 'express' ) const PORT = process.env.PORT || 5000 const app = express() app.listen( PORT, () => { console.log( ' ', PORT ) })
Ahora eche un vistazo al archivo worker.js . Cada vez que llega una nueva tarea , se iniciará el método parentPort.on () . Desafortunadamente, no puede manejar funciones asíncronas / en espera. Entonces usaremos la función del adaptador en forma del método doMyAsyncCode () .
En él, en un formato legible conveniente, pondremos la lógica del trabajador. Es decir, inicie el navegador (si aún no se está ejecutando) y active el método para tomar una captura de pantalla. Al inicio, pasaremos a este método un enlace al navegador en ejecución.
const { parentPort, workerData } = require( 'worker_threads' ); const puppeteer = require( 'puppeteer' ) const mapshoter = require( './mapshoter' )
Para mayor claridad, volvamos a la primera versión de mapshoter.js . No va a cambiar mucho. Ahora, en los parámetros de entrada, aceptará un enlace al navegador, y cuando finalice el script, no apagará el navegador, sino que simplemente cerrará la pestaña creada.
const puppeteer = require( 'puppeteer' ) const geoTools = require( './geoTools' ) async function makeTile( x, y, z, browserLink ) {
En principio, eso es todo. Ahora puede cargar el resultado al servidor de la forma que le resulte más conveniente. Por ejemplo, a través de docker. Si desea ver el resultado final, puede hacer clic en este enlace . También puede encontrar el código completo del proyecto en mi GitHub .
Conclusión
Ahora vamos a evaluar el resultado. Por un lado, incluso a pesar de todos los trucos realizados, la velocidad de descarga sigue siendo muy baja. Además, debido a los frenos, dicha tarjeta es simplemente desagradable para desplazarse.
Por otro lado, este script, sin embargo, hace frente a las tarjetas que antes de eso era generalmente imposible conectarse al navegador en el teléfono inteligente. Es poco probable que esta solución se aplique alguna vez como el método principal para obtener datos cartográficos. Pero aquí como uno adicional, con la ayuda de la cual, si es necesario, será posible abrir una tarjeta exótica, es bastante posible.
Además, las ventajas de este script incluyen el hecho de que es fácil trabajar con él. Es facil de escribir. Y, lo más importante, se puede rehacer extremadamente fácilmente para conectar cualquier otra tarjeta en línea.
Bueno, en el próximo artículo me ocuparé de eso. Transformaré el script en una especie de API para trabajar con el mapa interactivo OverpassTurbo.