Este problema (y la misma URL) ya fue respondida en otras dos ocasiones, pero ya no soy capaz de encontrarlas en el histórico del foro.
Así que toca rehacer la respuesta
Primero, hay que ver exactamente de qué estamos hablando. Los datos que leemos y filtramos tienen este aspecto:
Using text Syntax Highlighting
-----------------------------------------------------------------------------
PRES HGHT TEMP DWPT RELH MIXR DRCT SKNT THTA THTE THTV
hPa m C C % g/kg deg knot K K K
-----------------------------------------------------------------------------
1003.0 115 18.4 15.8 85 11.38 150 3 291.3 323.6 293.3
1002.0 124 18.2 12.2 68 8.98 152 3 291.2 316.9 292.8
1000.0 142 17.8 11.8 68 8.76 155 4 290.9 316.0 292.5
977.0 339 16.1 11.1 72 8.55 195 11 291.2 315.7 292.7
967.0 427 15.4 10.8 74 8.46 195 14 291.3 315.6 292.8
925.0 803 12.2 9.4 83 8.06 200 9 291.8 315.0 293.2
124.0 15363 -71.9 -91.3 4 0.00 294 40 365.4 365.4 365.4
Coloreado en 0.000 segundos, usando
GeSHi 1.0.8.4
donde algunos de los valores puede estar en blanco. En ese caso, hay que sustituirlos por 'NaN'.
El problema con este tipo de problemas es que no podemos usar la función split(), ya que pueden faltar valores y se nos descuadra la información (no sabríamos qué valor del arreglo devuelto por split() corresponde a qué columna).
Así que hay que romper las líneas de otra manera. Lo normal es extraer una de las líneas y agregar algo de
ASCII-ART para saber dónde empiezan y acaban las columnas. Tomamos la primera línea y empezamos a dibujar:
Using text Syntax Highlighting
1003.0 115 18.4 15.8 85 11.38 150 3 291.3 323.6 293.3
012345678901234567890123456789012345678901234567890123456789012345678901234567890
1 2 3 4 5 6 7 8
Coloreado en 0.000 segundos, usando
GeSHi 1.0.8.4
Así es fácil saber dónde empieza y acaba cada una. La mayoría de los editores de texto informan de la columna donde está el cursor, así que no haría falta hacer esto. Otros, como el Notepad++ tienen una regla horizontal, para operaciones parecidas.
Y de resultas de ver la primera línea, observamos que, por fortuna, todos los campos tienen exactamente 7 caracteres:
Using text Syntax Highlighting
1 2 3 4 5 6 7 8 012345678901234567890123456789012345678901234567890123456789012345678901234567890
1003.0 115 18.4 15.8 85 11.38 150 3 291.3 323.6 293.3
123456 123456 123456 123456 123456 123456 123456 123456 123456 123456 123456
Coloreado en 0.000 segundos, usando
GeSHi 1.0.8.4
Así que la extracción es aún más sencilla. Con una línea como
Using perl Syntax Highlighting
my @valores = unpack('(A7)*', $linea);Coloreado en 0.001 segundos, usando
GeSHi 1.0.8.4
ya tenemos leídos todos los valores de los campos.
Y ahora, solo quedaría hacer la transformación de los campos vacíos a 'Nan'. Pues dicho y hecho:
Using perl Syntax Highlighting
for my $valor (@valores) {
$valor = ' NaN' if $valor =~ /^\s*$/;
}Coloreado en 0.001 segundos, usando
GeSHi 1.0.8.4
Para el que no lo vea claro: la variable $valor está haciendo de alias de cada uno de los valores de @valores, por lo que modificarla es lo mismo que modificar los valores del arreglo.
Solo quedaría sacarlos al fichero:
Using perl Syntax Highlighting
my $fmt_salida = "%6.0f %6.0f %6.1f %6.1f %6.0f %6.2f %6.0f %6.0f %6.1f %6.1f %6.1f\n";
printf $fmt_salida, @valores;Coloreado en 0.001 segundos, usando
GeSHi 1.0.8.4
Ahora bien, estamos sacando valores todos numéricos. ¿Qué hara printf() cuando se encuentre con un valor 'NaN'? Pues esto:
Using text Syntax Highlighting
18 27134 -46.7 -81.7 1 0.03 nan nan 713.6 713.9 713.6
Coloreado en 0.000 segundos, usando
GeSHi 1.0.8.4
Bueno, si en vez de 'NaN' nos vale 'nan' (en minúsculas), pues entonces el problema se daría por terminado.
El programa quedaría así:
Using perl Syntax Highlighting
#!/usr/bin/perl
use warnings;
use strict;
use LWP::Simple;
# Formato de salida
my $fmt_salida = "%6.0f %6.0f %6.1f %6.1f %6.0f %6.2f %6.0f %6.0f %6.1f %6.1f %6.1f\n";
# Bucle por todos los datos
for my $ano ( 2005 ) {
for my $mes ( "03" ) {
for my $dia ( "01" .. "31" ) {
for my $hora ( 12 ) {
my $pagina = get(
'http://weather.uwyo.edu/cgi-bin/sounding?'
. join '&'
, 'region=samer'
, 'TYPE=TEXT%3ALIST'
, "YEAR=$ano"
, "MONTH=$mes"
, "FROM=$dia$hora"
, "TO=$dia$hora"
, 'STNM=85442'
);
if (defined $pagina) {
if ((my $datos) = $pagina =~ m/^<PRE>(.+?)<\/PRE>/sim) {
open FICHERO, ">$ano-$mes-$dia.dat";
for my $linea (split "\n", $datos) {
next if $linea !~ /\d/; # Solo nos interesan las líneas con números
chomp $linea;
my @valores = unpack('(A7)*', $linea);
for my $valor (@valores) {
$valor = ' NaN'
if $valor =~ /^\s*$/;
}
printf FICHERO $fmt_salida, @valores;
}
close FICHERO;
}
}
}}}}
Coloreado en 0.003 segundos, usando
GeSHi 1.0.8.4
Ahora bien... ¿qué ocurre si un día cambian los datos de entrada? Pues que tenemos que reeditar el programa.
Una de las características de los buenos programas es su
fortaleza frente a los cambios.
En el problema que nos ocupa, vamos a fijarnos en las primeras líneas de los datos de entrada:
Using text Syntax Highlighting
-----------------------------------------------------------------------------
PRES HGHT TEMP DWPT RELH MIXR DRCT SKNT THTA THTE THTV
hPa m C C % g/kg deg knot K K K
-----------------------------------------------------------------------------
1003.0 115 18.4 15.8 85 11.38 150 3 291.3 323.6 293.3
1002.0 124 18.2 12.2 68 8.98 152 3 291.2 316.9 292.8
Coloreado en 0.000 segundos, usando
GeSHi 1.0.8.4
Vemos algo interesante:
* los titulares de las columnas están escritos en mayúsculas, mientras que la línea de unidades contienen, sobre todo minúsculas
* esos mismos titulares están alineados, a la derecha, en su respectiva columna.
Es decir: la línea de titulares nos dice:
* cuántas columnas hay
* cómo se llaman
* dónde empiezan y terminan cada una de ellas
Sospechamos que, en el futuro, si hay algún cambio, tanto en el número de columnas, como en su disposición, la línea de titulares nos puede chivar cómo están organizados los datos.
Así que tenemos que hacer dos cosas: saber dónde comienzan y terminan las columnas y cómo ha de ser la salida.
En cuanto a lo segundo tenemos un nuevo problema: no sabemos a priori, con sólo los datos de entrada, cómo es la salida que tenemos que generar. Por eso estamos obligados a hacerlo nosotros mismos a mano:
Using perl Syntax Highlighting
my @fmt_salida = qw(
%6
.0f
%6
.0f
%6
.1f
%6
.1f
%6
.0f
%6
.2f
%6
.0f
%6
.0f
%6
.1f
%6
.1f
%6
.1f
);Coloreado en 0.001 segundos, usando
GeSHi 1.0.8.4
Lo que sí nos servirán estos valores es para luego poder formatear correctamente la salida del texto 'NaN'.
Sólo queda averiguar dónde está cada columna. Para ello, usaremos una expresión regular que recorra la línea de titulares y nos vaya guardando en un bucle el ancho de cada una.
Using perl Syntax Highlighting
if ($linea =~ /[A-Z]/) { # Si estamos en la línea de titulares...
my $columna_anterior = 0; # Índice a la columna del campo anterior
while ($linea =~ /\w+/g) { # Vamos buscando cada título
push @columnas, pos($linea) - $columna_anterior; # Guardamos el ancho de cada columna
$columna_anterior = pos($linea);
}
$columnas = join '', map { "A$_" } @columnas; # Formato de la línea
}Coloreado en 0.002 segundos, usando
GeSHi 1.0.8.4
Usamos la función pos() para saber la posición dentro de la $linea la posición de final de la columna. Ahora ya tenemos en $formato_columnas el formato de la línea de datos, que tendrá un aspecto parecido a este: 'A7A7A7A7A7A7A7A7A7A7A7', que es del tipo de formato especial para la función unpack().
El programa queda así:
Using perl Syntax Highlighting
#!/usr/bin/perl
use warnings;
use strict;
use LWP::Simple;
# Formato de salida. Almacena la forma de salida de cada campo
my @fmt_salida = qw(
%6.0f
%6.0f
%6.1f
%6.1f
%6.0f
%6.2f
%6.0f
%6.0f
%6.1f
%6.1f
%6.1f
);
# Bucle por todos los datos
for my $ano ( 2005 ) {
for my $mes ( "03" ) {
for my $dia ( "01" .. "31" ) {
for my $hora ( 12 ) {
my $pagina = get(
'http://weather.uwyo.edu/cgi-bin/sounding?'
. join '&'
, 'region=samer'
, 'TYPE=TEXT%3ALIST'
, "YEAR=$ano"
, "MONTH=$mes"
, "FROM=$dia$hora"
, "TO=$dia$hora"
, 'STNM=85442'
);
if (defined $pagina) {
if ((my $datos) = $pagina =~ m/^<PRE>(.+?)<\/PRE>/sim) {
my @titulares;
open FICHERO, ">$ano-$mes-$dia.dat";
my $formato_columnas;
for my $linea (split "\n", $datos) {
chomp $linea;
next if $linea =~ /^--+/; # Obviamos las líneas con guiones
next if $linea =~ /[a-z]/; # Obviamos las líneas con minúsculas
if ($linea =~ /[A-Z]/) { # Si estamos en la línea de titulares...
my $columna_anterior = 0; # Índice a la columna del campo anterior
my @columnas;
while ($linea =~ /\w+/g) { # Vamos buscando cada título
push @columnas, pos($linea) - $columna_anterior; # Guardamos el ancho de cada
$columna_anterior = pos($linea);
}
$formato_columnas = join '', map { "A$_" } @columnas; # Formato de la línea
}
elsif ($linea =~ /\d/) { # Aquí es una línea normal
my @valores = unpack($formato_columnas, $linea);
# Preparando la salida
# Tenemos que recorrer los valores, para poner los valores NaN y además, crear el formato de salida
my @formatos_salida;
for(my $i = 0; $i < @valores; $i++) {
my $valor = $valores[$i]; # Valor de la columna
my $fmt = $fmt_salida[$i]; # Formato con el que debe salir por defecto
if ($valor =~ /^\s*$/) { # Campo vacío
my ($ancho) = $fmt_salida[$i] =~ m/(\d+)/; # Leemos del formato la cifra del tamaño del campo
$fmt = '%' . $ancho . 's'; # Creamos el nuevo formato para sprintf()
$valores[$i] = sprintf $fmt, 'NaN'; # Colocamos el 'NaN' formateado a la derecha
}
push @formatos_salida, $fmt; # Guardamos el formato
}
my $fmt_salida = join(' ', @formatos_salida) . "\n";
printf FICHERO $fmt_salida, @valores;
}
}
close FICHERO;
}
}
}}}}
Coloreado en 0.004 segundos, usando
GeSHi 1.0.8.4
Y ya tenemos la solución perfecta (¡esperemos!).
Falta comentar el jaleo de líneas que prepara la salida.
Lo que hacemos es recorrer todos los @valores de las columnas junto con el formato que esperamos, por defecto, usar para sacarle. Miramos a continuación si ese $valor es un blanco o vacío. Si es así, extraemos el $ancho del campo de salida del primer valor decimal del formato. Ejemplo: si el formato es '%6.2f' entonces de ahí extraemos el '6'.
Modificamos a continuación el formato de salida, para pasar de, por ejemplo, '%6.2f' a '%6s'. Y guardamos como nuevo valor de columna el valor de 'NaN' formateado a ese $ancho.
Finalmente, para sacar la línea entera al fichero de salida, creamos un formato de salida para un printf() consistente en la unión (join()) de todos los formatos y le aplicamos a él todos los @valores de las columnas.
Una salida típica ya es como la que queremos:
Using text Syntax Highlighting
380 7934 -17.2 -40.3 12 0.30 300 36 337.4 338.6 337.4
375 8033 -17.7 -38.7 14 0.36 301 35 338.1 339.5 338.1
339 8765 -24.3 -39.0 24 0.39 310 31 339.0 340.5 339.1
300 9650 -32.3 -39.3 50 0.42 315 33 339.7 341.4 339.8
287 9961 -35.1 -40.1 60 0.41 319 36 340.1 341.7 340.1
269 10406 -38.5 -44.4 53 0.27 325 41 341.5 342.6 341.6
250 10910 -42.3 -49.3 46 0.17 320 45 343.0 343.8 343.1
230 11471 -44.9 -59.9 17 0.05 318 53 347.4 347.6 347.4
208 12132 -50.2 -64.5 17 0.03 315 62 349.1 349.3 349.1
200 12390 -52.3 -66.3 17 0.03 315 58 349.8 349.9 349.8
180 13061 -58.0 -72.8 13 0.01 300 49 351.1 351.1 351.1
174 13277 -59.9 -74.9 12 0.01 303 47 351.5 351.5 351.5
151 14150 -67.1 -81.1 12 0.00 314 37 353.6 353.6 353.6
150 14190 -67.1 -81.1 12 0.00 315 37 354.3 354.3 354.3
140 14594 -69.8 NaN NaN NaN 325 40 356.7 NaN 356.7
129 15073 -72.9 NaN NaN NaN 295 35 359.4 NaN 359.4
110 16006 -79.1 NaN NaN NaN 300 27 364.6 NaN 364.6
106 16217 -78.3 NaN NaN NaN 300 27 369.9 NaN 369.9
100 16550 -77.1 NaN NaN NaN 245 21 378.5 NaN 378.5
95 16840 -78.7 NaN NaN NaN 180 5 380.9 NaN 380.9
94 16900 -79.0 NaN NaN NaN 160 4 381.4 NaN 381.4
90 17146 -80.4 NaN NaN NaN 30 11 383.5 NaN 383.5
87 17338 -81.5 NaN NaN NaN 20 15 385.1 NaN 385.1
86 17377 -81.7 NaN NaN NaN 0 14 385.4 NaN 385.4
85 17470 -81.3 NaN NaN NaN 330 15 388.0 NaN 388.0
82 17674 -80.5 NaN NaN NaN 315 23 393.7 NaN 393.7
78 17957 -79.3 NaN NaN NaN 230 5 401.9 NaN 401.9
Coloreado en 0.001 segundos, usando
GeSHi 1.0.8.4
(Observar que los valores 0 se respetan)