Herramientas de usuario

Herramientas del sitio


captador_de_datos_a_sqlite

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Próxima revisión
Revisión previa
captador_de_datos_a_sqlite [2026/09/07 18:52] – creado hispacaptador_de_datos_a_sqlite [2026/09/09 07:19] (actual) hispa
Línea 1: Línea 1:
 +<WRAP round box>
 +====== Captador de datos a SQLite ======
 +</WRAP>
 +<WRAP tabs>
 +  * [[Estación meteorológica ESP8266 (D1 Mini), con captación de datos y elaboración de gráficos]]
 +  * [[Informática y cacharreo]]
 +</WRAP>
  
 +<WRAP collarge>
 +=== Descripción General del Sistema ===
 +Programa escrito en Free Pascal orientado a la captura automatizada de datos meteorológicos publicados por un bot en una instancia de Mastodon. Su función principal es consultar la red social mediante su API REST, extraer el contenido HTML del último estado publicado, limpiar las etiquetas, procesar los valores numéricos de temperatura, presión y humedad utilizando expresiones regulares, convertir la marca temporal UTC a hora local y grabar la información en una base de datos local SQLite si se trata de un nuevo registro.
 +
 +=== Requisitos y Dependencias ===
 +
 +  * Compilador: Free Pascal Compiler (FPC) configurado en modo Object Pascal ({$mode objfpc}{$H+}).
 +  * Librerías del Sistema / FPC:
 +      *Red y Protocolos: fphttpclient, opensslsockets
 +      *Procesamiento JSON: fpjson, jsonparser
 +      *Expresiones Regulares: RegExpr
 +      *Base de Datos: sqlite3conn, sqlite3dyn, sqldb
 +      *Utilidades Generales: SysUtils, Classes, DateUtils
 +  * Librería Externa: libsqlite3.so.0 (en entornos UNIX/Linux) o sqlite3.dll (en entornos Windows).
 +  * En entornos Linux, ejecución mediante un programador de tareas como Cron. También es posible crear un disparador como servicio en systemd que ejecute el programa a intervalos definidos.
 +</WRAP>
 +
 +<code pascal>
 +program captadormedidasmastodon;
 +
 +{$mode objfpc}{$H+}
 +
 +uses
 +  // Soporte para UTF-8
 +  {$IFDEF UNIX}
 +    cwstring,
 +  {$ENDIF}
 +  // Soporte para Windows
 +  {$IFDEF WINDOWS}
 +    Windows,
 +  {$ENDIF}
 +  // SysUtils Siempre detrás de cwstring para evitar errores
 +  SysUtils,
 +  // Soporte de OOP
 +  Classes,
 +  // Soporte HTTP, SSL y JSON
 +  fphttpclient,
 +  opensslsockets,
 +  fpjson,
 +  jsonparser,
 +  // Procesado de expresiones regulares
 +  RegExpr,
 +  // Soporte SQL y SQLite
 +  sqlite3conn,
 +  sqlite3dyn,
 +  sqldb,
 +  // Soporte de fecha/hora
 +  DateUtils;
 +
 +const
 +  // Constantes para leer el contenido del usuario bot (@meteo) en Mastodon
 +  INSTANCIA_MASTODON = 'social.hispa.net';
 +  USUARIO_MASTODON = 'meteo';
 +  USER_AGENT = 'MastodonWeatherReader/1.0 (FreePascal)';
 +
 +  // Directorio para su uso en el sobremesa (testing)
 +  // DB_FILE = '/home/hispa/Nextcloud/Data_1/Documentos/Jorge/Programas/Pascal/captador_medidas_mastodon/clima.db';
 +  // Directorio para su uso en el servidor (definitivo)
 +  DB_FILE = '/media/Data_1/Documentos/Jorge/Programas/Pascal/captador_medidas_mastodon/clima.db';
 +
 +type
 +  TMastodonPost = record
 +    Content: string;
 +    CreatedAt: string; // ISO 8601 UTC
 +  end;
 +
 +  TDatosClima = record
 +    Temperatura: Double;
 +    Presion: Double;
 +    Humedad: Double;
 +  end;
 +
 +{ ============================================================================ }
 +{ MÓDULO DE RED Y API MASTODON                                                 }
 +{ ============================================================================ }
 +
 +// Realiza una petición GET HTTPS configurando el User-Agent
 +function HttpGet(const URL: string): string;
 +var
 +  Client: TFPHTTPClient = nil;
 +begin
 +  Result := '';
 +  try
 +    Client := TFPHTTPClient.Create(nil);
 +    Client.AddHeader('User-Agent', USER_AGENT);
 +    Result := Client.Get(URL);
 +  finally
 +    Client.Free;
 +  end;
 +end;
 +
 +// Obtiene el ID numérico interno de un usuario de Mastodon
 +function ObtenerAccountId(const Instancia, Usuario: string): string;
 +var
 +  URL, Response: string;
 +  JsonData: TJSONData = nil;
 +  IdNode: TJSONData = nil;
 +begin
 +  Result := '';
 +  URL := Format('https://%s/api/v1/accounts/lookup?acct=%s', [Instancia, Usuario]);
 +  try
 +    Response := HttpGet(URL);
 +    JsonData := GetJSON(Response);
 +    if Assigned(JsonData) then
 +    begin
 +      IdNode := JsonData.FindPath('id');
 +      if Assigned(IdNode) then
 +        Result := IdNode.AsString;
 +    end;
 +  except
 +    on E: Exception do
 +      WriteLn('[Error HTTP/JSON] Al buscar usuario: ', E.Message);
 +  end;
 +  JsonData.Free;
 +end;
 +
 +// Obtiene el contenido y fecha del último estado publicado por la cuenta
 +function ObtenerUltimoPost(const Instancia, AccountId: string; out Post: TMastodonPost): Boolean;
 +var
 +  URL, Response: string;
 +  JsonData: TJSONData = nil;
 +  JsonArray: TJSONArray;
 +  PostObj: TJSONObject;
 +begin
 +  Result := False;
 +  Post.Content := '';
 +  Post.CreatedAt := '';
 +
 +  URL := Format('https://%s/api/v1/accounts/%s/statuses?limit=1', [Instancia, AccountId]);
 +  try
 +    Response := HttpGet(URL);
 +    JsonData := GetJSON(Response);
 +    if (JsonData is TJSONArray) and (TJSONArray(JsonData).Count > 0) then
 +    begin
 +      JsonArray := TJSONArray(JsonData);
 +      PostObj   := JsonArray.Objects[0];
 +
 +      Post.Content   := PostObj.Get('content', '');
 +      Post.CreatedAt := PostObj.Get('created_at', '');
 +      Result := True;
 +    end;
 +  except
 +    on E: Exception do
 +      WriteLn('[Error HTTP/JSON] Al obtener publicaciones: ', E.Message);
 +  end;
 +  JsonData.Free;
 +end;
 +
 +{ ============================================================================ }
 +{ MÓDULO DE TRATAMIENTO DE TEXTO Y VALORES METEOROLÓGICOS                      }
 +{ ============================================================================ }
 +
 +// Remueve etiquetas HTML y espacios múltiples sobrantes
 +function LimpiarTextoHTML(const TextoHTML: string): string;
 +var
 +  Regex: TRegExpr = nil;
 +begin
 +  try
 +    Regex := TRegExpr.Create;
 +    Regex.Expression := '<[^>]+>';
 +    Result := Regex.Replace(TextoHTML, ' ', False);
 +
 +    // Normalizar espacios múltiples a uno solo
 +    Regex.Expression := '\s+';
 +    Result := Trim(Regex.Replace(Result, ' ', False));
 +  finally
 +    Regex.Free;
 +  end;
 +end;
 +
 +// Convierte fecha ISO 8601 UTC a hora local (formato YYYY-MM-DD HH:NN:SS)
 +function UTCaFechaLocal(const FechaUTC: string): string;
 +var
 +  DT_UTC, DT_Local: TDateTime;
 +begin
 +  try
 +    DT_UTC := ISO8601ToDate(FechaUTC, True);
 +    DT_Local := UniversalTimeToLocal(DT_UTC);
 +    Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', DT_Local);
 +  except
 +    Result := FechaUTC;
 +  end;
 +end;
 +
 +// Extrae valor numérico mediante Regex independientemente del separador (coma/punto)
 +function ExtraerValor(const Texto, Patron: string): Double;
 +var
 +  Regex: TRegExpr = nil;
 +  ValStr: string;
 +  FS: TFormatSettings;
 +begin
 +  Result := 0.0;
 +
 +  // Configuración de formato independiente del locale
 +  FS := DefaultFormatSettings;
 +  FS.DecimalSeparator := '.';
 +
 +  try
 +    Regex := TRegExpr.Create;
 +    Regex.ModifierI := True; // Insensible a mayúsculas
 +    Regex.Expression := Patron;
 +
 +    if Regex.Exec(Texto) then
 +    begin
 +      ValStr := StringReplace(Regex.Match[1], ',', '.', [rfReplaceAll]);
 +      Result := StrToFloatDef(ValStr, 0.0, FS);
 +    end;
 +  finally
 +    Regex.Free;
 +  end;
 +end;
 +
 +// Parsea las tres magnitudes principales del post
 +function ParsearDatosClima(const Texto: string): TDatosClima;
 +begin
 +  Result.Temperatura := ExtraerValor(Texto, '(?:temp|temperatura)[^\d]*(-?\d+(?:[\.,]\d+)?)');
 +  Result.Presion     := ExtraerValor(Texto, '(?:presi|barom)[^\d]*(\d+(?:[\.,]\d+)?)');
 +  Result.Humedad     := ExtraerValor(Texto, '(?:hum|humedad)[^\d]*(\d+(?:[\.,]\d+)?)');
 +end;
 +
 +{ ============================================================================ }
 +{ MÓDULO DE BASE DE DATOS (SQLITE)                                             }
 +{ ============================================================================ }
 +
 +// Prepara la conexión SQL asegurando el estado abierto
 +procedure AbrirConexion(Conn: TSQLite3Connection);
 +begin
 +  if Conn.DatabaseName = '' then
 +    Conn.DatabaseName := DB_FILE;
 +  if not Conn.Connected then
 +    Conn.Open;
 +end;
 +
 +// Crea la estructura de tablas inicial
 +procedure InicializarBaseDatos(Conn: TSQLite3Connection; Trans: TSQLTransaction);
 +var
 +  Query: TSQLQuery = nil;
 +begin
 +  AbrirConexion(Conn);
 +  Trans.StartTransaction;
 +
 +  try
 +    Query := TSQLQuery.Create(nil);
 +    Query.DataBase := Conn;
 +    Query.Transaction := Trans;
 +    Query.SQL.Text :=
 +      'CREATE TABLE IF NOT EXISTS mediciones (' +
 +      '  id INTEGER PRIMARY KEY AUTOINCREMENT,' +
 +      '  fecha TEXT NOT NULL,' +
 +      '  temperatura REAL,' +
 +      '  presion REAL,' +
 +      '  humedad REAL' +
 +      ');';
 +    Query.ExecSQL;
 +    Trans.Commit;
 +  except
 +    if Trans.Active then
 +      Trans.Rollback;
 +    Query.Free;
 +    raise;
 +  end;
 +  Query.Free;
 +end;
 +
 +// Devuelve la última fecha registrada en SQLite o cadena vacía si no hay registros
 +function ObtenerUltimaFechaBD(Conn: TSQLite3Connection; Trans: TSQLTransaction): string;
 +var
 +  Query: TSQLQuery = nil;
 +begin
 +  Result := '';
 +  AbrirConexion(Conn);
 +
 +  try
 +    Query := TSQLQuery.Create(nil);
 +    Query.DataBase := Conn;
 +    Query.Transaction := Trans;
 +    Query.SQL.Text := 'SELECT MAX(fecha) FROM mediciones;';
 +    Query.Open;
 +
 +    if not Query.EOF and not Query.Fields[0].IsNull then
 +      Result := Query.Fields[0].AsString;
 +  finally
 +    Query.Free;
 +  end;
 +end;
 +
 +// Guarda los valores parseados en la base de datos
 +procedure GuardarLectura(Conn: TSQLite3Connection; Trans: TSQLTransaction;
 +  const Fecha: string; const Datos: TDatosClima);
 +var
 +  Query: TSQLQuery = nil;
 +begin
 +  AbrirConexion(Conn);
 +  Trans.StartTransaction;
 +
 +  try
 +    Query := TSQLQuery.Create(nil);
 +    Query.DataBase := Conn;
 +    Query.Transaction := Trans;
 +    Query.SQL.Text :=
 +      'INSERT INTO mediciones (fecha, temperatura, presion, humedad) ' +
 +      'VALUES (:fecha, :temp, :pres, :hum);';
 +
 +    Query.Params.ParamByName('fecha').AsString := Fecha;
 +    Query.Params.ParamByName('temp').AsFloat   := Datos.Temperatura;
 +    Query.Params.ParamByName('pres').AsFloat   := Datos.Presion;
 +    Query.Params.ParamByName('hum').AsFloat    := Datos.Humedad;
 +
 +    Query.ExecSQL;
 +    Trans.Commit;
 +    WriteLn('-> Nueva medida guardada con éxito.');
 +  except
 +    if Trans.Active then
 +      Trans.Rollback;
 +    Query.Free;
 +    raise;
 +  end;
 +  Query.Free;
 +end;
 +
 +{ ============================================================================ }
 +{ BLOQUE PRINCIPAL                                                             }
 +{ ============================================================================ }
 +
 +var
 +  Connection: TSQLite3Connection = nil;
 +  Transaction: TSQLTransaction = nil;
 +  AccountId, UltimaFechaBD, FechaLocal, TextoLimpio: string;
 +  Post: TMastodonPost;
 +  Clima: TDatosClima;
 +
 +begin
 +  {$IFDEF UNIX}
 +    SQLiteDefaultLibrary := 'libsqlite3.so.0';
 +  {$ENDIF}
 +  {$IFDEF WINDOWS}
 +    SQLiteDefaultLibrary := 'sqlite3.dll';
 +  {$ENDIF}
 +
 +  Connection := TSQLite3Connection.Create(nil);
 +  Transaction := TSQLTransaction.Create(nil);
 +  Connection.Transaction := Transaction;
 +
 +  try
 +    WriteLn('Base de datos: ', DB_FILE);
 +    InicializarBaseDatos(Connection, Transaction);
 +
 +    WriteLn(Format('Buscando cuenta "@%s@%s"...', [USUARIO_MASTODON, INSTANCIA_MASTODON]));
 +    AccountId := ObtenerAccountId(INSTANCIA_MASTODON, USUARIO_MASTODON);
 +
 +    if AccountId = '' then
 +    begin
 +      WriteLn('Error: No se pudo resolver la cuenta del usuario.');
 +      Exit;
 +    end;
 +
 +    WriteLn('Consultando último post...');
 +    if not ObtenerUltimoPost(INSTANCIA_MASTODON, AccountId, Post) then
 +    begin
 +      WriteLn('No se encontraron publicaciones.');
 +      Exit;
 +    end;
 +
 +    TextoLimpio := LimpiarTextoHTML(Post.Content);
 +    FechaLocal  := UTCaFechaLocal(Post.CreatedAt);
 +
 +    WriteLn('Fecha UTC: ', Post.CreatedAt);
 +    WriteLn('Fecha Local: ', FechaLocal);
 +    WriteLn('Contenido: ', TextoLimpio);
 +
 +    UltimaFechaBD := ObtenerUltimaFechaBD(Connection, Transaction);
 +
 +    if UltimaFechaBD <> '' then
 +      WriteLn('Última fecha registrada en BD: ', UltimaFechaBD)
 +    else
 +      WriteLn('Última fecha registrada en BD: Ninguna (BD vacía)');
 +
 +    // Verificar si la fecha actual es posterior a la almacenada
 +    if (UltimaFechaBD = '') or (FechaLocal > UltimaFechaBD) then
 +    begin
 +      Clima := ParsearDatosClima(TextoLimpio);
 +
 +      WriteLn(Format('Extraído -> Temp: %.2f °C | Presión: %.2f hPa | Humedad: %.2f%%',
 +        [Clima.Temperatura, Clima.Presion, Clima.Humedad]));
 +
 +      GuardarLectura(Connection, Transaction, FechaLocal, Clima);
 +    end;
 +
 +  finally
 +    Transaction.Free;
 +    Connection.Free;
 +  end;
 +end.
 +</code>
 +<WRAP collarge>
 +
 +=== Estructura de Datos ===
 +
 +  * **TMastodonPost**: Registro que almacena el contenido bruto del estado (Content) y su fecha de creación en formato ISO 8601 UTC (CreatedAt).
 +  * **TDatosClima**: Registro numérico con las tres variables climáticas capturadas: Temperatura, Presion y Humedad (todas de tipo Double).
 +
 +=== Arquitectura y Módulos del Sistema ===
 +
 +  - **Módulo de Red y API de Mastodon**
 +      * **HttpGet(URL: string): string**: Realiza una petición HTTPS GET configurando una cabecera User-Agent personalizada para cumplir con los requisitos de la API.
 +      * **ObtenerAccountId(Instancia, Usuario: string): string**: Consulta el endpoint de búsqueda de cuentas (/api/v1/accounts/lookup?acct=...) para resolver el identificador numérico interno del usuario.
 +      * **ObtenerUltimoPost(Instancia, AccountId: string; out Post: TMastodonPost): Boolean**: Obtiene el listado de estados recientes del usuario (/api/v1/accounts/{id}/statuses?limit=1) y extrae el contenido y la fecha de creación del último mensaje.
 +  - **Módulo de Tratamiento de Texto y Valores Meteorológicos**
 +      * **LimpiarTextoHTML(TextoHTML: string): string**: Elimina todas las etiquetas HTML mediante expresiones regulares y normaliza los espacios múltiples a un solo carácter.
 +      * **UTCaFechaLocal(FechaUTC: string): string**: Transforma la marca temporal ISO 8601 UTC a la hora local del sistema aplicando el huso horario correspondiente y formateándola como YYYY-MM-DD HH:NN:SS.
 +      * **ExtraerValor(Texto, Patron: string): Double**: Aplica un patrón de expresión regular independiente de mayúsculas/minúsculas para extraer un valor numérico, normalizando separadores decimales (comas y puntos) mediante un objeto TFormatSettings.
 +      * **ParsearDatosClima(Texto: string): TDatosClima**: Ejecuta de manera secuencial la extracción de temperatura, presión y humedad sobre el texto limpio del post.
 +   - **Módulo de Base de Datos (SQLite)**
 +      * **AbrirConexion(Conn: TSQLite3Connection)**: Verifica y asigna la ruta del archivo de base de datos (DB_FILE) y asegura que la conexión se encuentre abierta.
 +      * **InicializarBaseDatos(Conn: TSQLite3Connection, Trans: TSQLTransaction)**: Crea la tabla relacional mediciones si esta no existe previamente en el archivo SQLite.
 +      * **ObtenerUltimaFechaBD(Conn: TSQLite3Connection, Trans: TSQLTransaction): string**: Consulta la marca temporal máxima registrada (SELECT MAX(fecha)) para determinar el estado actual de los registros.
 +      * **GuardarLectura(Conn: TSQLite3Connection, Trans: TSQLTransaction, Fecha: string, Datos: TDatosClima)**: Inserta una nueva fila en la base de datos utilizando parámetros tipados para prevenir inyecciones SQL, controlando transacciones y aplicando un Rollback automático en caso de fallo.
 +
 +=== Esquema de Base de Datos ===
 +
 +La persistencia se realiza en una base de datos SQLite con la siguiente estructura de tabla:
 +
 +   * Nombre de la tabla: mediciones
 +   * Campos:
 +      * **id: INTEGER PRIMARY KEY AUTOINCREMENT** (Identificador único autoincremental).
 +      * **fecha: TEXT NOT NULL** (Marca temporal local en formato YYYY-MM-DD HH:NN:SS).
 +      * **temperatura: REAL** (Valor numérico de la temperatura).
 +      * **presion: REAL** (Valor numérico de la presión barométrica).
 +      * **humedad: REAL** (Valor numérico del porcentaje de humedad).
 +
 +=== Flujo de Ejecución Principal ===
 +
 +   - **Configuración de Entorno**: Asignación de la librería dinámica de SQLite en función de la plataforma de compilación (UNIX o WINDOWS).
 +   - **Conexión Inicial**: Creación de los componentes de conexión y transacción, seguido de la inicialización automática de la base de datos y su estructura de tablas.
 +   - **Consulta en Red**: Resolución del ID de la cuenta de Mastodon (@meteo@social.hispa.net) y obtención del estado más reciente.
 +   - **Procesamiento**: Limpieza del código HTML del mensaje, conversión de la fecha UTC a hora local y consulta de la última fecha existente en la base de datos local.
 +   - **Evaluación y Persistencia**: Si la fecha del último post es posterior a la almacenada en la base de datos (o si la base de datos está vacía), se realiza el análisis sintáctico de las variables climáticas y se inserta el nuevo registro de manera transaccional.
 +</WRAP>
 +{{tag> informática_y_cacharreo Pascal FreePascal Lazarus}}

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki