captador_de_datos_a_sqlite
Diferencias
Muestra las diferencias entre dos versiones de la página.
| Próxima revisión | Revisión previa | ||
| captador_de_datos_a_sqlite [2026/09/07 18:52] – creado hispa | captador_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 tabs> | ||
| + | * [[Estación meteorológica ESP8266 (D1 Mini), con captación de datos y elaboración de gráficos]] | ||
| + | * [[Informática y cacharreo]] | ||
| + | </ | ||
| + | <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, | ||
| + | |||
| + | === 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, | ||
| + | *Procesamiento JSON: fpjson, jsonparser | ||
| + | *Expresiones Regulares: RegExpr | ||
| + | *Base de Datos: sqlite3conn, | ||
| + | *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. | ||
| + | </ | ||
| + | |||
| + | <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 = ' | ||
| + | USUARIO_MASTODON = ' | ||
| + | USER_AGENT = ' | ||
| + | |||
| + | // Directorio para su uso en el sobremesa (testing) | ||
| + | // DB_FILE = '/ | ||
| + | // Directorio para su uso en el servidor (definitivo) | ||
| + | DB_FILE = '/ | ||
| + | |||
| + | type | ||
| + | TMastodonPost = record | ||
| + | Content: string; | ||
| + | CreatedAt: string; // ISO 8601 UTC | ||
| + | end; | ||
| + | |||
| + | TDatosClima = record | ||
| + | Temperatura: | ||
| + | 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(' | ||
| + | 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(' | ||
| + | try | ||
| + | Response := HttpGet(URL); | ||
| + | JsonData := GetJSON(Response); | ||
| + | if Assigned(JsonData) then | ||
| + | begin | ||
| + | IdNode := JsonData.FindPath(' | ||
| + | if Assigned(IdNode) then | ||
| + | Result := IdNode.AsString; | ||
| + | end; | ||
| + | except | ||
| + | on E: Exception do | ||
| + | WriteLn(' | ||
| + | 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): | ||
| + | var | ||
| + | URL, Response: string; | ||
| + | JsonData: TJSONData = nil; | ||
| + | JsonArray: TJSONArray; | ||
| + | PostObj: TJSONObject; | ||
| + | begin | ||
| + | Result := False; | ||
| + | Post.Content := ''; | ||
| + | Post.CreatedAt := ''; | ||
| + | |||
| + | URL := Format(' | ||
| + | try | ||
| + | Response := HttpGet(URL); | ||
| + | JsonData := GetJSON(Response); | ||
| + | if (JsonData is TJSONArray) and (TJSONArray(JsonData).Count > 0) then | ||
| + | begin | ||
| + | JsonArray := TJSONArray(JsonData); | ||
| + | PostObj | ||
| + | |||
| + | Post.Content | ||
| + | Post.CreatedAt := PostObj.Get(' | ||
| + | Result := True; | ||
| + | end; | ||
| + | except | ||
| + | on E: Exception do | ||
| + | WriteLn(' | ||
| + | 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, | ||
| + | |||
| + | // Normalizar espacios múltiples a uno solo | ||
| + | Regex.Expression := ' | ||
| + | Result := Trim(Regex.Replace(Result, | ||
| + | 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, | ||
| + | DT_Local := UniversalTimeToLocal(DT_UTC); | ||
| + | Result := FormatDateTime(' | ||
| + | except | ||
| + | Result := FechaUTC; | ||
| + | end; | ||
| + | end; | ||
| + | |||
| + | // Extrae valor numérico mediante Regex independientemente del separador (coma/ | ||
| + | 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], | ||
| + | Result := StrToFloatDef(ValStr, | ||
| + | end; | ||
| + | finally | ||
| + | Regex.Free; | ||
| + | end; | ||
| + | end; | ||
| + | |||
| + | // Parsea las tres magnitudes principales del post | ||
| + | function ParsearDatosClima(const Texto: string): TDatosClima; | ||
| + | begin | ||
| + | Result.Temperatura := ExtraerValor(Texto, | ||
| + | Result.Presion | ||
| + | Result.Humedad | ||
| + | end; | ||
| + | |||
| + | { ============================================================================ } | ||
| + | { MÓDULO DE BASE DE DATOS (SQLITE) | ||
| + | { ============================================================================ } | ||
| + | |||
| + | // Prepara la conexión SQL asegurando el estado abierto | ||
| + | procedure AbrirConexion(Conn: | ||
| + | begin | ||
| + | if Conn.DatabaseName = '' | ||
| + | Conn.DatabaseName := DB_FILE; | ||
| + | if not Conn.Connected then | ||
| + | Conn.Open; | ||
| + | end; | ||
| + | |||
| + | // Crea la estructura de tablas inicial | ||
| + | procedure InicializarBaseDatos(Conn: | ||
| + | var | ||
| + | Query: TSQLQuery = nil; | ||
| + | begin | ||
| + | AbrirConexion(Conn); | ||
| + | Trans.StartTransaction; | ||
| + | |||
| + | try | ||
| + | Query := TSQLQuery.Create(nil); | ||
| + | Query.DataBase := Conn; | ||
| + | Query.Transaction := Trans; | ||
| + | Query.SQL.Text := | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | 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: | ||
| + | var | ||
| + | Query: TSQLQuery = nil; | ||
| + | begin | ||
| + | Result := ''; | ||
| + | AbrirConexion(Conn); | ||
| + | |||
| + | try | ||
| + | Query := TSQLQuery.Create(nil); | ||
| + | Query.DataBase := Conn; | ||
| + | Query.Transaction := Trans; | ||
| + | Query.SQL.Text := ' | ||
| + | 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: | ||
| + | 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 := | ||
| + | ' | ||
| + | ' | ||
| + | |||
| + | Query.Params.ParamByName(' | ||
| + | Query.Params.ParamByName(' | ||
| + | Query.Params.ParamByName(' | ||
| + | Query.Params.ParamByName(' | ||
| + | |||
| + | Query.ExecSQL; | ||
| + | Trans.Commit; | ||
| + | WriteLn(' | ||
| + | except | ||
| + | if Trans.Active then | ||
| + | Trans.Rollback; | ||
| + | Query.Free; | ||
| + | raise; | ||
| + | end; | ||
| + | Query.Free; | ||
| + | end; | ||
| + | |||
| + | { ============================================================================ } | ||
| + | { BLOQUE PRINCIPAL | ||
| + | { ============================================================================ } | ||
| + | |||
| + | var | ||
| + | Connection: TSQLite3Connection = nil; | ||
| + | Transaction: | ||
| + | AccountId, UltimaFechaBD, | ||
| + | Post: TMastodonPost; | ||
| + | Clima: TDatosClima; | ||
| + | |||
| + | begin | ||
| + | {$IFDEF UNIX} | ||
| + | SQLiteDefaultLibrary := ' | ||
| + | {$ENDIF} | ||
| + | {$IFDEF WINDOWS} | ||
| + | SQLiteDefaultLibrary := ' | ||
| + | {$ENDIF} | ||
| + | |||
| + | Connection := TSQLite3Connection.Create(nil); | ||
| + | Transaction := TSQLTransaction.Create(nil); | ||
| + | Connection.Transaction := Transaction; | ||
| + | |||
| + | try | ||
| + | WriteLn(' | ||
| + | InicializarBaseDatos(Connection, | ||
| + | |||
| + | WriteLn(Format(' | ||
| + | AccountId := ObtenerAccountId(INSTANCIA_MASTODON, | ||
| + | |||
| + | if AccountId = '' | ||
| + | begin | ||
| + | WriteLn(' | ||
| + | Exit; | ||
| + | end; | ||
| + | |||
| + | WriteLn(' | ||
| + | if not ObtenerUltimoPost(INSTANCIA_MASTODON, | ||
| + | begin | ||
| + | WriteLn(' | ||
| + | Exit; | ||
| + | end; | ||
| + | |||
| + | TextoLimpio := LimpiarTextoHTML(Post.Content); | ||
| + | FechaLocal | ||
| + | |||
| + | WriteLn(' | ||
| + | WriteLn(' | ||
| + | WriteLn(' | ||
| + | |||
| + | UltimaFechaBD := ObtenerUltimaFechaBD(Connection, | ||
| + | |||
| + | if UltimaFechaBD <> '' | ||
| + | WriteLn(' | ||
| + | else | ||
| + | WriteLn(' | ||
| + | |||
| + | // Verificar si la fecha actual es posterior a la almacenada | ||
| + | if (UltimaFechaBD = '' | ||
| + | begin | ||
| + | Clima := ParsearDatosClima(TextoLimpio); | ||
| + | |||
| + | WriteLn(Format(' | ||
| + | [Clima.Temperatura, | ||
| + | |||
| + | GuardarLectura(Connection, | ||
| + | end; | ||
| + | |||
| + | finally | ||
| + | Transaction.Free; | ||
| + | Connection.Free; | ||
| + | end; | ||
| + | end. | ||
| + | </ | ||
| + | <WRAP collarge> | ||
| + | |||
| + | === Estructura de Datos === | ||
| + | |||
| + | * **TMastodonPost**: | ||
| + | * **TDatosClima**: | ||
| + | |||
| + | === Arquitectura y Módulos del Sistema === | ||
| + | |||
| + | - **Módulo de Red y API de Mastodon** | ||
| + | * **HttpGet(URL: | ||
| + | * **ObtenerAccountId(Instancia, | ||
| + | * **ObtenerUltimoPost(Instancia, | ||
| + | - **Módulo de Tratamiento de Texto y Valores Meteorológicos** | ||
| + | * **LimpiarTextoHTML(TextoHTML: | ||
| + | * **UTCaFechaLocal(FechaUTC: | ||
| + | * **ExtraerValor(Texto, | ||
| + | * **ParsearDatosClima(Texto: | ||
| + | - **Módulo de Base de Datos (SQLite)** | ||
| + | * **AbrirConexion(Conn: | ||
| + | * **InicializarBaseDatos(Conn: | ||
| + | * **ObtenerUltimaFechaBD(Conn: | ||
| + | * **GuardarLectura(Conn: | ||
| + | |||
| + | === 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: | ||
| + | * **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, | ||
| + | - **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**: | ||
| + | - **Evaluación y Persistencia**: | ||
| + | </ | ||
| + | {{tag> informática_y_cacharreo Pascal FreePascal Lazarus}} | ||
