Extracción de Contenido Multimedia de Aplicaciones Móviles con Scrapy

La recolección de datos de aplicaciones móviles puede ser un proceso complejo que a menudo implica la intercepción de tráfico de red para identificar puntos finales de API. Este artículo describe un enfoque para extraer imágenes y metadatos asociados, como nombres de anfitriones, identificadores de canal y ubicaciones, de una aplicación móvil utilizando el framework Scrapy, después de analizar su tráfico con herramientas como Fiddler.

Antes de iniciar la extracción, es fundamental configurar una herramienta de proxy de intercecpión, como Fiddler o Charles Proxy, en conjunto con un dispositivo móvil. Esta configuración permite capturar las solicitudes y respuestas de la aplicación, revelando las URL de las API internas y el formato de los datos, que comúnmente se presentan en JSON.

Una vez que se ha configurado la intercepción del tráfico, al interactuar con la aplicación móvil (por ejemplo, navegando por secciones específicas), se pueden observar las solicitudes HTTP/HTTPS. Es crucial identificar la URL exacta que devuelve la información deseada. Para este ejemplo, buscamos una API que responda con datos JSON que contengan detalles de los anfitriones y sus imágenes. Las URL de estas APIs suelen incluir parámetros para paginación o filtrado, como un parámetro de "offset". Por motivos de privacdiad y seguridad, no se revelan aquí URLs específicas; cada usuario debe identificar las suyas mediante la inspección de su propio tráfico.

Con la API identificada, se procede a configurar el proyecto Scrapy para la extracción de datos.

Configuración Inicial de Scrapy

Primero, cree un nuevo proyecto de Scrapy y un spider:

scrapy startproject mobile_image_scraper
cd mobile_image_scraper
scrapy genspider streamer_images douyucdn.cn

Definición del Item

El archivo mobile_image_scraper/items.py define la estructura de los datos que se van a extraer. Cada campo representa un atributo de la información del anfitrión y la imagen.

import scrapy

class StreamerDataItem(scrapy.Item):
    # Nombre del anfitrión (streamer)
    host_name = scrapy.Field()
    # Identificador único del canal
    channel_identifier = scrapy.Field()
    # Ubicación geográfica del anfitrión
    host_location = scrapy.Field()
    # URL de la imagen principal del anfitrión
    image_url = scrapy.Field()
    # Ruta local donde se almacenará la imagen
    local_image_path = scrapy.Field()
    # Fuente de adquisición del dato
    acquisition_source = scrapy.Field()
    # Marca de tiempo UTC de la adquisición
    timestamp_utc = scrapy.Field()

Implementación del Spider

El spider (mobile_image_scraper/spiders/streamer_images.py) es el encargado de realizar las solicitudes HTTP, analizar las respuestas y extraer los datos. Este spider manejará la paginación para obtener múltiples conjuntos de resultados.

import scrapy
import json
from mobile_image_scraper.items import StreamerDataItem

class StreamerImageSpider(scrapy.Spider):
    name = 'streamer_images'
    allowed_domains = ['douyucdn.cn'] # Ajustar según el dominio de la API
    current_page_offset = 0
    
    # URL base de la API (reemplazar ***** con la parte real de la URL)
    api_base_url = 'https://capi.douyucdn.cn/api/v1/live/list?type=yz&limit=20&offset='
    
    start_urls = [api_base_url + str(current_page_offset)]

    def parse(self, response):
        json_response = json.loads(response.body.decode('utf-8'))
        data_entries = json_response.get("data", [])

        if not data_entries:
            self.logger.info("No se encontraron más entradas. Finalizando raspado.")
            return

        for entry in data_entries:
            scraped_item = StreamerDataItem()
            scraped_item["host_name"] = entry.get("nickname")
            scraped_item["channel_identifier"] = entry.get("room_id")
            scraped_item["host_location"] = entry.get("anchor_city")
            scraped_item["image_url"] = entry.get("vertical_src")
            yield scraped_item
        
        # Incrementar el offset para la siguiente página
        self.current_page_offset += 20
        next_page_url = self.api_base_url + str(self.current_page_offset)
        self.logger.info(f"Solicitando la siguiente página: {next_page_url}")
        yield scrapy.Request(url=next_page_url, callback=self.parse)

Configuración de Pipelines para Procesamiento y Almacenameinto

Los pipelines (mobile_image_scraper/pipelines.py) se utilizan para procesar los ítems extraídos después de que el spider los ha generado. Aquí se definen dos pipelines: uno para inyectar metadatos y otro para descargar y renombrar las imágenes.

from scrapy.pipelines.images import ImagesPipeline
from mobile_image_scraper.settings import IMAGES_STORAGE_DIR
from datetime import datetime
import scrapy
import os
import logging

logger = logging.getLogger(__name__)

class MetadataInjectorPipeline(object):
    """
    Pipeline para inyectar metadatos adicionales al item.
    """
    def process_item(self, item, spider):
        item["acquisition_source"] = spider.name
        item["timestamp_utc"] = str(datetime.utcnow())
        logger.debug(f"Metadatos agregados al item: {item['channel_identifier']}")
        return item

class ImageDownloadAndRenamePipeline(ImagesPipeline):
    """
    Pipeline personalizado para descargar imágenes y renombrar los archivos.
    """
    # Envía la solicitud para descargar la imagen
    def get_media_requests(self, item, info):
        image_link_to_download = item.get("image_url")
        if image_link_to_download:
            logger.debug(f"Solicitando descarga de imagen: {image_link_to_download}")
            yield scrapy.Request(url=image_link_to_download)

    # Se llama después de que una imagen ha sido descargada
    def item_completed(self, results, item, info):
        image_download_results = [x for ok, x in results if ok]
        if not image_download_results:
            logger.warning(f"No se descargó ninguna imagen para el item: {item['channel_identifier']}")
            return item

        # La ImagesPipeline guarda las imágenes en un subdirectorio 'full' por defecto
        downloaded_file_path = image_download_results[0]["path"]
        
        # Construir rutas completas para renombrar
        old_full_path = os.path.join(IMAGES_STORAGE_DIR, downloaded_file_path)
        
        # Generar un nuevo nombre de archivo usando el nombre del anfitrión
        host_name_safe = item.get("host_name", "unknown_host").replace(" ", "_").replace("/", "_")
        new_file_name = f"{host_name_safe}_{item.get('channel_identifier')}.jpg"
        new_full_path = os.path.join(IMAGES_STORAGE_DIR, new_file_name)

        item["local_image_path"] = new_full_path # Almacenar la nueva ruta en el item

        try:
            if os.path.exists(old_full_path):
                os.rename(old_full_path, new_full_path)
                logger.info(f"Imagen renombrada de {old_full_path} a {new_full_path}")
            else:
                logger.warning(f"Ruta de imagen original no encontrada: {old_full_path}")
        except OSError as e:
            logger.error(f"Error al renombrar imagen {old_full_path} a {new_full_path}: {e}")
        
        return item

Middleware de Rotación de User-Agent

Para evitar ser bloqueado por el servidor, es una buena práctica rotar los User-Agents en las solicitudes HTTP. El archivo mobile_image_scraper/middlewares.py implementa un middleware para esto.

import random
from mobile_image_scraper.settings import CUSTOM_USER_AGENTS

class RandomUserAgentMiddleware(object):
    """
    Asigna un User-Agent aleatorio a cada solicitud saliente.
    """
    def process_request(self, request, spider):
        selected_agent = random.choice(CUSTOM_USER_AGENTS)
        request.headers['User-Agent'] = selected_agent
        spider.logger.debug(f"Asignando User-Agent: {selected_agent}")

Configuración del Proyecto (settings.py)

El archivo mobile_image_scraper/settings.py contiene la configuración global del proyecto, incluyendo las rutas de almacenamiento, la lista de User-Agents y la activación de los middlewares y pipelines.

# Ruta donde se almacenarán las imágenes descargadas
IMAGES_STORAGE_DIR = '/tmp/scraped_app_images' # Cambiar a una ruta deseada

# Lista de User-Agents para rotación
CUSTOM_USER_AGENTS = [
    "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.79 Mobile Safari/537.36",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Safari/605.1.15",
    "Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/99.0.4844.73 Mobile/15E148 Safari/604.1",
    "Mozilla/5.0 (Android 12; Mobile; rv:98.0) Gecko/98.0 Firefox/98.0"
]

# Habilitar y ordenar los downloader middlewares
DOWNLOADER_MIDDLEWARES = {
   'mobile_image_scraper.middlewares.RandomUserAgentMiddleware': 543,
}

# Habilitar y ordenar los item pipelines
ITEM_PIPELINES = {
    'mobile_image_scraper.pipelines.MetadataInjectorPipeline': 100,
    'mobile_image_scraper.pipelines.ImageDownloadAndRenamePipeline': 200,
}

# Configuración de la pipeline de imágenes
IMAGES_STORE = IMAGES_STORAGE_DIR

Script de Ejecución y Limpieza

Para facilitar la ejecución del spider y realizar una limpieza post-raspado, se puede crear un script simple (por ejemplo, run_scraper.py) en la raíz del proyecto.

import os
import shutil

# Definir la ruta de almacenamiento de imágenes (debe coincidir con settings.py)
IMAGE_BASE_PATH = '/tmp/scraped_app_images'
FULL_DIR_PATH = os.path.join(IMAGE_BASE_PATH, "full")

print("Iniciando el proceso de extracción de imágenes...")
# Ejecutar el spider de Scrapy
os.system("scrapy crawl streamer_images")

print("Proceso de extracción finalizado.")

# Limpiar el directorio 'full' creado por Scrapy ImagesPipeline
if os.path.exists(FULL_DIR_PATH):
    try:
        shutil.rmtree(FULL_DIR_PATH) # Usar rmtree para eliminar directorios no vacíos
        print(f"Directorio temporal '{FULL_DIR_PATH}' eliminado.")
    except OSError as e:
        print(f"Error al eliminar el directorio '{FULL_DIR_PATH}': {e}")
else:
    print(f"El directorio '{FULL_DIR_PATH}' no existe, no es necesario eliminar.")

Después de ejecutar python run_scraper.py, las imágenes se descargarán y renombrarán según la lógica definida en los pipelines, y se almacenarán en la ruta especificada en IMAGES_STORAGE_DIR.

Etiquetas: scrapy Python web-scraping api-scraping images-pipeline

Publicado el 6-28 23:52