In a previous article, I covered 5 Sources of Free Weather Data for your Site, but did not provide any actual code to use the data. Last week, I covered source #1 and showed how to Parse Weather Forecast Data (from the NDFD) in PHP. For this article, I will show how to parse source #2 – the current weather conditions data as provided by the National Weather Service.
Fetching the Data for One Station at a Time
This is ideal for a website that wants to show the current conditions for one location. Go to the XML Feeds page, and find your station. We need to know the station id for your area, so select your state, and then find the closest station to you.
The example below shows how easy it is to fetch the XML and format it into something we can read.
<?php /** * Download a copy here: * http://sourceforge.net/projects/snoopy/ */ require('../includes/Snoopy.class.php'); /** * Load a single station and return the data */ function load_single_station($station_id, &$xml) { $snoopy= new Snoopy(); $snoopy->fetch('http://www.weather.gov/xml/current_obs/' . $station_id . '.xml', $xml_tmp . $station_id . '.xml'); if (!$snoopy->results) { /* oops */ return false; } $data = $snoopy->results; try { /* convert the XML into a data object */ $xml = @new SimpleXMLElement($data); /* convert that data object into an array, and return it */ return get_object_vars($xml); } catch (Exception $e) { /* we got an empty or invalid XML file */ return false; } } $conditions = load_single_station('KLOT', $xml); var_dump($conditions); /** * Why do I comment out the PHP closing tag? * See: http://phpstarter.net/2009/01/omit-the-php-closing-tag/ */ /* ?> */
Although that example gets the job done, we are going to want to do some caching. It’s a bit pointless to be fetching that XML file every time a page is requested. With a little bit of database code, we can make it relatively easy. First, setup a database that your script has access to, and create this table:
CREATE TABLE `Current` ( `suggested_pickup` varchar(55) NOT NULL, `last_update` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, `suggested_pickup_period` smallint(6) NOT NULL, `location` varchar(55) NOT NULL, `station_id` varchar(10) NOT NULL, `latitude` decimal(5,2) NOT NULL, `longitude` decimal(5,2) NOT NULL, `observation_time` varchar(255) NOT NULL, `observation_time_rfc822` varchar(255) NOT NULL, `weather` varchar(55) NOT NULL, `temperature_string` varchar(55) NOT NULL, `temp_f` smallint(6) NOT NULL, `temp_c` smallint(6) NOT NULL, `relative_humidity` tinyint(4) NOT NULL, `wind_string` varchar(55) NOT NULL, `wind_dir` varchar(55) NOT NULL, `wind_degrees` smallint(6) NOT NULL, `wind_mph` smallint(6) NOT NULL, `wind_gust_mph` smallint(6) NOT NULL, `pressure_string` varchar(55) NOT NULL, `pressure_mb` smallint(6) NOT NULL, `pressure_in` decimal(5,2) NOT NULL, `dewpoint_string` varchar(55) NOT NULL, `dewpoint_f` smallint(6) NOT NULL, `dewpoint_c` smallint(6) NOT NULL, `heat_index_string` varchar(55) NOT NULL, `heat_index_f` smallint(6) NOT NULL, `heat_index_c` smallint(6) NOT NULL, `windchill_string` varchar(55) NOT NULL, `windchill_f` smallint(6) NOT NULL, `windchill_c` smallint(6) NOT NULL, `visibility_mi` decimal(5,2) NOT NULL, `icon_url_base` varchar(255) NOT NULL, `icon_url_name` varchar(55) NOT NULL, `two_day_history_url` varchar(255) NOT NULL, `ob_url` varchar(255) NOT NULL, PRIMARY KEY (`station_id`), KEY `latitude` (`latitude`,`longitude`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1;
After that MySQL table is created, have a look at this next example. It’s the same as the last one, but with two new functions: load_single_station_cache() & save_conditions(). The load_single_station_cache() function checks the table for a non-stale record for the specified station. If it finds one, it returns that database record, and no XML file is downloaded. If it doesn’t find one, it calls up load_single_station(), just like the first example, and then saves that data with the save_conditions() function.
<?php /** * Download a copy here: * http://sourceforge.net/projects/snoopy/ */ require('../includes/Snoopy.class.php'); /** * replace with your database connection code */ require('../includes/database.php'); db_connect(); /** * Load a single station and return the data */ function load_single_station($station_id, &$xml) { $snoopy= new Snoopy(); $snoopy->fetch('http://www.weather.gov/xml/current_obs/' . $station_id . '.xml', $xml_tmp . $station_id . '.xml'); if (!$snoopy->results) { /* oops */ return false; } $data = $snoopy->results; try { /* convert the XML into a data object */ $xml = @new SimpleXMLElement($data); /* convert that data object into an array, and return it */ return get_object_vars($xml); } catch (Exception $e) { /* we got an empty or invalid XML file */ return false; } } /** * Save the weather conditions to the database table. */ function save_conditions($conditions) { /** * Get the columns and interect that list with the data array, * so we don't try to insert some fields that don't exist */ $query = "SHOW COLUMNS FROM Current"; $result = mysql_query($query); $fields = array(); for ($i = 0, $n = mysql_num_rows($result); $i < $n; $i++) { $row = mysql_fetch_row($result); $fields[] = $row[0]; } $fields = array_flip($fields); $conditions = array_intersect_key($conditions, $fields); /** * Form the data pairs into query format. * Uncomment the echo statement below to see what it's doing. */ $fields = $values = ''; foreach ($conditions as $field => $value) { $fields .= $field . ","; $values .= "'" . mysql_real_escape_string($value) . "',"; } /* remove the last comma from both variables */ $fields = substr($fields, 0, strlen($fields) - 1); $values = substr($values, 0, strlen($values) - 1); $query = "DELETE FROM Current WHERE station_id = '{$conditions['station_id']}' LIMIT 1"; mysql_query($query); $query = "INSERT INTO Current ($fields) VALUES ($values)"; //echo $query; $result = mysql_query($query); if (!$result) die(mysql_error()); } /** * Load the weather conditions from the DB table, and make the necessary * calls if a recent record does not exist. */ function load_single_station_cache($station_id, &$xml) { $station_id_esc = mysql_real_escape_string($station_id); /* no results will be returned if there is no record, *or* if the record is more than 5400 seconds (90 minutes) old */ $query = "SELECT * FROM Current WHERE station_id = '$station_id_esc' && last_update + 5400 > NOW() LIMIT 1"; $result = mysql_query($query); if (!$result) die(mysql_error()); if (mysql_num_rows($result) == 0) { /* load a fresh set of conditions */ $conditions = load_single_station($station_id, &$xml); /* don't forget to save it for later */ save_conditions($conditions); return $conditions; } else { /* we got it */ return mysql_fetch_assoc($result); } } $conditions = load_single_station_cache('KLOT', $xml); header('Content-type: text/plain'); var_dump($conditions); /** * Why do I comment out the PHP closing tag? * See: http://phpstarter.net/2009/01/omit-the-php-closing-tag/ */ /* ?> */
Fetching the Data for all Stations
Higher traffic sites that show conditions for all or most of the available stations may want to download all of the stations at once from their data feeds page, and import them into a database. To do this, we are going to need to download the zip file from the website, and then extract it. I am not going to provide specific code, because it’s unique to the server environment. Binary files can be downloaded via cURL or fsockopen() as an alternative. Once downloaded, use PHP’s Zip functions to extract the files to a temp directory. Zlib is required to open a zip archive in PHP, so if you don’t have it, you can use the shell_exec() function to run the necessary unzip functions in the shell, assuming that your server supports it.
Once you have the XML files at your disposal, use something like the scandir() function and import all files using something like the single-station example.
So there you have it – XML files turned into an easy to manage, cache-able associative array.