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.