« 2016年03月 | メイン | 2016年07月 »

前の10件 1  2  3

2016年04月 記事一覧

第02回 subscriberを構築する

今回は気象庁防災情報XMLを受信するためのsubscriberを作成します。
サーバー側アプリはsubscriber、XML記事のダウンロード、XML記事内容を整形するツールの合計3つ以上作る予定です。



まずはソリューション/プロジェクトを作成します。
ソリューション名を「JmaForecast」、subscriber用のプロジェクト名を「jmasubscriber」、プロジェクトの種類は「コンソールアプリケーション」とします。

今回はVisual Studio 2012を利用しました。
「ファイル」メニューの「新規作成」から「プロジェクト」を選択し、「Visual C#」の「コンソールアプリケーション」としてプロジェクトを作成。
プロジェクト名は「JmaForecast」としました。

そしてソリューションウインドウの「JmaForecast」プロジェクトを右クリックして現れたメニューから「削除」を選択して作成したばかりのプロジェクトを削除。
これだけではプロジェクトは完全には削除されていないので、エクスプローラーでソリューションのあるフォルダを開き、そこにある「JmaForecast」フォルダを削除してしまいます。

これで名前が「JmaForecast」で、プロジェクトが何もない空のソリューションができました。

次にソリューションウインドウの「JmaForecast」ソリューションを右クリックして現れたメニューから「追加」にある「新しいプロジェクト」を選択し、
「Visual C#」の「コンソールアプリケーション」を追加します。
ここでプロジェクト名は「jmasubscriber」、.NETのバージョンは「.NET Framework 4」としました。
これでソリューション/プロジェクトの準備は完了です。



次に一気にsubscriberのコードを作ってしまいます。

これは一番古典的な方法ですが、
C#アプリをCGIとして扱いたい場合は、アプリをコンソールアプリケーションとして作成し、
・GETで渡された内容は環境変数の「QUERY_STRING」を読む
・POSTで渡された内容はConsole.OpenStandardInput()からStreamで読む
という形で作れます。
CGIはコンソールアプリケーションではなくasp.netなどを使うのが一般的ですが、デバッグや応用が簡単なので今回もこれでいきます。

今回は関係ないですが、C#アプリはwindowsのテンポラリフォルダへ勝手に一時ファイルを作ることがあります。
そのためサーバー側ではWEBサーバーのユーザーに対してc:\Windows\tmp\へ書き込み権限を与える必要があります。

■Program.cs
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;

namespace jmasubscriber
{
	class Program
	{
		const string _strVerifyToken = "test_token";
		const string _strFolerOut = "jma/";

		static void Main(string[] args)
		{
			//POSTかGETか、というアクセス種別を取得する
			string strMethod = Environment.GetEnvironmentVariable("REQUEST_METHOD");

			//GETの場合は、subscriberチェック→チャレンジコードを返す
			if (strMethod == "GET")
			{
				try
				{
					//GETで↓のような文字列が渡される
					// 「hub.topic=http://www.example.com/test.txt&hub.challenge=9567222552380101910&hub.verify_token=test_token&hub.mode=subscribe&hub.lease_seconds=432000」
					string strQuery = Environment.GetEnvironmentVariable("QUERY_STRING");

					NameValueCollection col = QueryToNameValueCollection(strQuery);

					string strHubMode = col["hub.mode"];
					string strHubChallenge = col["hub.challenge"];
					string strHubVerifyToken = col["hub.verify_token"];

					if (strHubMode == "subscribe" || strHubMode == "unsubscribe")
					{
						if (strHubVerifyToken != _strVerifyToken || strHubChallenge == null || strHubChallenge == "")
						{
							Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");		//unknown reqは404じゃないけど気にしない
							Console.WriteLine("Content-Type: text/plain");
							Console.WriteLine("");
							Console.WriteLine("failed(1).");
							return;
						}

						Console.WriteLine("HTTP/1.1 200 \"OK\"");
						Console.WriteLine("Content-Type: text/plain");
						Console.WriteLine("");
						Console.Write(strHubChallenge);		//WriteLine()はNG。チャンレンジコードを返す
						return;
					}
				}
				catch (Exception)
				{
				}

				Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");
				Console.WriteLine("Content-Type: text/plain");
				Console.WriteLine("");
				Console.WriteLine("failed(2).");
				return;
			}


			//POSTの場合は、情報の配信→受信内容を保存する
			if (strMethod == "POST")
			{
				//何らかのエラーがありえる状態でも、とりあえず先に200 okを返しておく
				Console.WriteLine("HTTP/1.1 200 \"OK\"");
				Console.WriteLine("Content-Type: text/html");
				Console.WriteLine("");

				try
				{
					using (Stream st = Console.OpenStandardInput())			//標準入力から配信内容取得
					using (BinaryReader br = new BinaryReader(st))			//StreamReader.ReadLine()などは使えない。BinaryReaderで処理
					{
						string strLength = Environment.GetEnvironmentVariable("CONTENT_LENGTH");
						int nLength = Int32.Parse(strLength);

						if (nLength < 1000000)		//約1MB以上ある場合は無視する(そんなに大きなサイズが来ることはないはず)
						{
							byte[] data = new byte[nLength];
							br.Read(data, 0, nLength);

							//効率は悪いけど一度文字列に変換しちゃう
							string strQuery = Encoding.UTF8.GetString(data, 0, nLength);

							//ファイル保存。ファイル名は日時ticks数値
							string strFile = _strFolerOut + DateTime.Now.Ticks + ".txt";
							using (StreamWriter sw = new StreamWriter(strFile, false, Encoding.UTF8))
							{
								sw.Write(strQuery);
							}
						}
					}
				}
				catch (Exception)
				{
				}
				return;
			}
		}


		/// <summary>
		/// 「a=123&b=456&c=789」という形のクエリ文字列を、名前/値に分離してNameValueCollectionに格納する
		/// </summary>
		static NameValueCollection QueryToNameValueCollection(string strQuery)
		{
			NameValueCollection ret = new NameValueCollection();

			string[] astrQuery = strQuery.Split('&');

			foreach (string strItem in astrQuery)
			{
				string[] astrPair = strItem.Split('=');

				if (astrPair.Length == 0)
					continue;

				string strName = "";
				string strValue = "";

				if (astrPair.Length >= 1)
					strName = astrPair[0];
				if (astrPair.Length >= 2)
					strValue = astrPair[1];

				if (strName == "")
					continue;

				ret.Add(strName, strValue);
			}

			return ret;
		}

	}
}




最後に作成したsubscriber CGIの動作確認を簡単に。

まずはビルドする前に、「プロジェクト」メニューにある「jmasubscriberのプロパティ」を選択し、構成「Release」にたいして、デバッグ」タブにある「Visual Studioホスティングプロセスを有効にする」のチェックをはずします。

そしてReleaseビルド。
出来上がった「jmasubscriber.exe」をサーバーへアップロードし、実行権限を付加します。
そしてアップロード先へWEBブラウザーでアクセスして404エラーが出れば成功です(「http://www.example.com/jmasubscriber.exe」などへアクセス)。

次にsubscriberのチェックをします。

汎用的なsubscriberのテストように公開されているサービス「https://pubsubhubbub.appspot.com/」を利用します。
Subscribe to a feed or debug your subscriber」へアクセスし、

・「Callback URL」にexeへのURL(「http://www.example.com/jmasubscriber.exe」など)
・「Topic URL」にダミーとなるURL(「http://www.example.com/test.txt」など。とりあえずなんでもok)
・「Verify type」を「Synchronous」
・「Mode」を「Subscribe」
・「Verify token」を「test_token」(ソースコード内で設定した値)

として、「Do It!」ボタンを押して何も起きなければ成功。ページが切り替わってエラーが表示されたら失敗です。
さらに「Verify token」を「aaaaaa」のように適当な文字列に設定して「Do It!」ボタンを押してエラーが表示されたら成功です。

同様にpublishの動作確認をしたい場合はTopic URLに適当に作成したatom feedのxmlを置いて「publish」。feedを更新して「publish」というようにします。




subscriberが出来上がったら、「Callback URL」のURLと「Verify token」の値や住所氏名などを添えて指定様式で気象庁に登録申請します。

プロジェクトファイルをダウンロード

第03回 subscribeをダウンロードする

今回はsubscriberへ配信された内容を元に記事をダウンロードします。

気象庁防災情報XMLではsubscriberへ以下のようなfeedが配信されます。
entryの数はfeedによって異なり、実際の記事内容はlinkとしてURLが記載されています。
今回はこのlinkをダウンロードします。

なお、linkのURLは配信から数時間だけ有効で、それを過ぎると404エラーでダウンロードできなくなります。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">
<title>JMAXML publishing feed</title>
<subtitle>this feed is published by JMA</subtitle>
<updated>2016-04-06T17:45:01+09:00</updated>
<id>urn:uuid:d38e0e80-12ba-3236-b10f-256b78a08995</id>
<link href="http://www.jma.go.jp/" rel="related"/>
<link href="http://xml.kishou.go.jp/feed/other.xml" rel="self"/>
<rights>Published by Japan Meteorological Agency</rights>

<entry>
<title>地方海上警報</title>
<id>urn:uuid:e89f7227-becb-3cdf-b7f8-c665434602d4</id>
<updated>2016-04-06T08:44:37Z</updated>
<author><name>鹿児島地方気象台</name></author>
<link href="http://xml.kishou.go.jp/data/e89f7227-becb-3cdf-b7f8-c665434602d4.xml" type="application/xml"/>
<content type="text">【鹿児島海上気象】</content>
</entry>
</feed>
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">
<title>JMAXML publishing feed</title>
<subtitle>this feed is published by JMA</subtitle>
<updated>2016-04-06T17:46:01+09:00</updated>
<id>urn:uuid:d38e0e80-12ba-3236-b10f-256b78a08995</id>
<link href="http://www.jma.go.jp/" rel="related"/>
<link href="http://xml.kishou.go.jp/feed/other.xml" rel="self"/>
<rights>Published by Japan Meteorological Agency</rights>

<entry>
<title>地方海上警報</title>
<id>urn:uuid:37eb566f-9585-36d6-8972-d9473d134bf0</id>
<updated>2016-04-06T08:45:25Z</updated>
<author><name>沖縄気象台</name></author>
<link href="http://xml.kishou.go.jp/data/37eb566f-9585-36d6-8972-d9473d134bf0.xml" type="application/xml"/>
<content type="text">【沖縄海上気象】</content>
</entry>
<entry>
<title>生物季節観測</title>
<id>urn:uuid:a43b264a-cefc-3495-9f81-1b315359cbb7</id>
<updated>2016-04-06T08:45:32Z</updated>
<author><name>甲府地方気象台</name></author>
<link href="http://xml.kishou.go.jp/data/a43b264a-cefc-3495-9f81-1b315359cbb7.xml" type="application/xml"/>
<content type="text">【生物季節観測】</content>
</entry>
</feed>



前回は↑のファイルが保存されるようにしました。
今回はこのファイルを読み込んで、linkを抽出、それをファイルへダウンロードします。

まずはプロジェクトを作成します。
前回のソリューションを開き、ソリューションウインドウの「JmaForecast」ソリューションを右クリックして現れたメニューから「追加」にある「新しいプロジェクト」を選択し、
「Visual C#」の「コンソールアプリケーション」を追加します。
ここでプロジェクト名は「jmadownload」、.NETのバージョンは「.NET Framework 4」としました。
さらに「プロジェクト」メニューにある「jmadownloadのプロパティ」を選択し、構成「Release」にたいして、デバッグ」タブにある「Visual Studioホスティングプロセスを有効にする」のチェックをはずしておきます。

これでダウンロード処理を用意し、さらにsubscriber側にjmadownload.exeを呼ぶ処理を追加します。
そして出来上がったexeを2つともサーバーへアップロードすればokです。

■Program.cs(jmadownload.exe)
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Xml;

namespace jmadownload
{
	/// <summary>
	/// 気象庁からpush配信されたxmlデータに記載されているリンクをダウンロードする
	/// 
	/// 呼び出すときにxmlへのファイルパスを渡すこと
	/// </summary>
	class Program
	{
		/// <summary>
		/// argsにファイル名を渡すこと!
		/// </summary>
		static void Main(string[] args)
		{
			if (args.Length == 0)
			{
#if DEBUG
				//デバッグ時のテスト用
				args = new string[1];
				//args[0] = @"..\..\635955615261813750.txt";		//サンプルxml
				args[0] = @"..\..\635955616026657500.txt";			//サンプルxml
#else
				return;
#endif
			}


			try
			{
				List<Entry> listEntry = new List<Entry>();

				//xmlファイルからエントリーリストを取得
				ListUpEntry(args[0], listEntry);

				bool bDownloaded = true;

				foreach (Entry entry in listEntry)
				{
					if (entry.strLink == "" || entry.strTitle == "")
						continue;
					if (entry.strLink.IndexOf(@"http://xml.kishou.go.jp/data/") != 0)		//気象庁のURL以外はダウンロードしない
						continue;

					string strFile;
					string strRootFolder;

					strRootFolder = Path.GetDirectoryName(args[0]) + @"\";

					PrepareFolerFile(entry, strRootFolder, out strFile);					//保存ファイル名の決定/保存先フォルダ作成

					if (strFile == "")
						continue;


					//すでにファイルがあるならダウンロードしない
					if (File.Exists(strFile))
						continue;

					//ブロッキングでダウンロード
					try
					{
						bool bSuccess = false;
						for (int i = 0; i < 10; i++)		//ダウンロード試行回数は10回
						{
							using (MemoryStream ms = new MemoryStream())
							{
								bool ret;
								Stream strm = (Stream)ms;

								//メモリストリームへダウンロード
								ret = DownloadStream(entry.strLink, ref strm);
								if (ret == false)
									continue;

								//ファイルへ保存
								using (FileStream fs = new FileStream(strFile, FileMode.Create, FileAccess.Write))
								{
									ms.CopyTo(fs);
									fs.Flush();
								}
								bSuccess = true;
								break;		//ダウンロード成功!
							}
						}
						if (bSuccess == false)
							bDownloaded = false;
					}
					catch (Exception)
					{
					}
				}

				if (bDownloaded)
					File.Delete(args[0]);
			}
			catch (Exception)
			{
			}
		}



		/// <summary>
		/// URLをStreamへダウンロードする
		/// </summary>
		static bool DownloadStream(string strURL, ref Stream outStream)
		{
			if (outStream == null)
				outStream = new MemoryStream();

			try
			{
				WebRequest request = WebRequest.Create(strURL);

				request.Method = "GET";

				using (WebResponse response = request.GetResponse())
				using (Stream dataStream = response.GetResponseStream())
				{
					dataStream.CopyTo(outStream);
				}
				if (outStream.CanSeek)
					outStream.Seek(0, SeekOrigin.Begin);
				return true;
			}
			catch (Exception)
			{
			}
			return false;
		}




		class Entry
		{
			public string strTitle
			{
				get { return _strTitle; }
				set { _strTitle = value; }
			}
			string _strTitle = "";

			public string strID
			{
				get { return _strID; }
				set { _strID = value; }
			}
			string _strID = "";

			public DateTime dtUpdated
			{
				get { return _dtUpdated; }
				set { _dtUpdated = value; }
			}
			DateTime _dtUpdated = DateTime.MinValue;

			public string strAuthorName
			{
				get { return _strAuthorName; }
				set { _strAuthorName = value; }
			}
			string _strAuthorName = "";

			public string strLink
			{
				get { return _strLink; }
				set { _strLink = value; }
			}
			string _strLink = "";

			public string strContent
			{
				get { return _strContent; }
				set { _strContent = value; }
			}
			string _strContent = "";



			public bool SetUpdated(string strDate)
			{
				bool ret;
				DateTime dtDate;

				ret = StringToDateTime(strDate, out dtDate);
				if (ret)
					_dtUpdated = dtDate;

				return ret;
			}

		}


		/// <summary>
		/// エントリーの列挙
		/// 
		/// 指定されたファイルパスのサブスクライブxmlを読み込み、中にあるエントリー情報をlistEntryへ追加する
		/// 失敗したらマイナス1
		/// 成功したら取得できたエントリー数を返す
		/// </summary>
		static int ListUpEntry(string strSubscribeXMLFile, List<Entry> listEntry)
		{
			//XML例。取得するのは/feed/entry
			//
			//<?xml version="1.0" encoding="utf-8"?>
			//<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">
			//<title>JMAXML publishing feed</title>
			//<subtitle>this feed is published by JMA</subtitle>
			//<updated>2016-04-07T19:01:01+09:00</updated>
			//<id>urn:uuid:f57b5866-0c8c-3c92-9aff-10a715cdf48b</id>
			//<link href="http://www.jma.go.jp/" rel="related"/>
			//<link href="http://xml.kishou.go.jp/feed/extra.xml" rel="self"/>
			//<rights>Published by Japan Meteorological Agency</rights>
			//
			//<entry>
			//<title>気象特別警報・警報・注意報</title>
			//<id>urn:uuid:0e212e5b-fb6f-3409-a8e9-d96854a4346c</id>
			//<updated>2016-04-07T10:00:44Z</updated>
			//<author><name>岡山地方気象台</name></author>
			//<link href="http://xml.kishou.go.jp/data/0e212e5b-fb6f-3409-a8e9-d96854a4346c.xml" type="application/xml"/>
			//<content type="text">【岡山県気象警報・注意報】注意報を解除します。</content>
			//</entry>
			//<entry>
			//<title>気象警報・注意報</title>
			//<id>urn:uuid:1c90ac60-8ab6-3d80-b619-a1b7d9be6b71</id>
			//<updated>2016-04-07T10:00:44Z</updated>
			//<author><name>岡山地方気象台</name></author>
			//<link href="http://xml.kishou.go.jp/data/1c90ac60-8ab6-3d80-b619-a1b7d9be6b71.xml" type="application/xml"/>
			//<content type="text">【岡山県気象警報・注意報】注意報を解除します。</content>
			//</entry>
			//</feed>
			//

			try
			{
				int nCount = 0;

				using (XmlTextReader reader = new XmlTextReader(strSubscribeXMLFile))
				{
					//ネームスペースを無視
					reader.Namespaces = false;

					XmlDocument xml = new XmlDocument();
					xml.Load(reader);


					//エントリーの選択
					XmlNodeList entries = xml.DocumentElement.SelectNodes(@"/feed/entry");

					foreach (XmlNode entry in entries)
					{
						Entry item = new Entry();

						XmlNode node;

						node = entry.SelectSingleNode("title");
						if (node != null)
							item.strTitle = node.InnerText;

						node = entry.SelectSingleNode("id");
						if (node != null)
							item.strID = node.InnerText;

						node = entry.SelectSingleNode("author/name");
						if (node != null)
							item.strAuthorName = node.InnerText;

						node = entry.SelectSingleNode("updated");
						if (node != null)
							item.SetUpdated(node.InnerText);


						node = entry.SelectSingleNode("link");
						if (node != null)
						{
							XmlAttribute type = node.Attributes["type"];
							if (type != null && type.Value == @"application/xml")
							{
								XmlAttribute href = node.Attributes["href"];
								if (href != null)
									item.strLink = href.Value;
							}
						}

						node = entry.SelectSingleNode("content");
						if (node != null)
							item.strContent = node.InnerText;

						//タイトルがなかったら無視
						if (item.strTitle == "")
							continue;

						nCount++;
						listEntry.Add(item);
					}
				}

				return nCount;
			}
			catch (Exception)
			{
				return -1;
			}
		}



		/// <summary>
		/// ファイル名として不適な文字を置き換える
		/// </summary>
		static string ReplaceInvalidChar(string strText)
		{
			char[] pcbInvalid = Path.GetInvalidFileNameChars();


			//IDをファイル名にする
			// →ファイル名に使えない文字を除去
			{
				//ありがちな文字は決め打ちで全角に
				strText = strText.Replace('*', '*');
				strText = strText.Replace('\\', '¥');
				strText = strText.Replace(':', ':');
				strText = strText.Replace('<', '<');
				strText = strText.Replace('>', '>');
				strText = strText.Replace('?', '?');
				strText = strText.Replace('|', '|');

				//使えない文字除去
				foreach (char c in pcbInvalid)
				{
					strText = strText.Replace(c.ToString(), "");
				}
			}

			return strText;
		}



		/// <summary>
		/// フォルダーを作成して、保存すべきファイルパスを返す
		/// 
		/// 保存先はrootfolderの下にyyyymmddフォルダの下、
		/// ファイル名は「タイトル_ID.txt」
		/// </summary>
		static bool PrepareFolerFile(Entry entry, string strRootFolder, out string strFilePath)
		{
			strFilePath = "";
			if (entry == null || entry.strTitle == "" || entry.dtUpdated == DateTime.MinValue || strRootFolder == "")
				return false;

			try
			{
				strFilePath = ReplaceInvalidChar(entry.strTitle) + "_" + ReplaceInvalidChar(entry.strID) + ".txt";

				string strDate = string.Format("{0:0000}{1:00}{2:00}", entry.dtUpdated.Year, entry.dtUpdated.Month, entry.dtUpdated.Day);

				if (strRootFolder.Substring(strRootFolder.Length - 1) != @"\")
					strRootFolder += @"\";

				string strFolder = strRootFolder + strDate + @"\";
				Directory.CreateDirectory(strFolder);

				strFilePath = strFolder + strFilePath;

				return true;
			}
			catch (Exception)
			{
			}
			return false;
		}




		/// <summary>
		/// 「yyyy/mm/dd」「yyyymmdd」「yyyy/mm/dd hh:mm」「yyyy/mm/dd hh:mm:ss」をDateTimeにする
		/// 
		///「2016-04-07T19:01:01+09:00」の形式対応(時差がプラス9時間でなければそれに合わせて変換後返す)
		///「2016-04-07T10:00:44Z」(UTC日時)の形式対応(プラス9時間して返す)
		/// 
		/// yyyymmddの区切りは「年月日」「/」「:」「-」に対応
		/// hhmmssの区切りは「時分秒」「:」に対応
		/// カッコで囲まれた曜日表記に対応 ex. 「(日)」「(水)」「(Wed)」「(sat.)」
		/// </summary>
		public static bool StringToDateTime(string strDate, out DateTime dtDate)
		{
			//「yyyy年mm月dd日」				→「yyyy/mm/dd」
			//「yyyy年mm月dd日hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日 hh時mm分」		→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh時mm分ss秒」	→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日 hh時mm分ss秒」	→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日hh:mm」			→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日 hh:mm」			→「yyyy/mm/dd hh:mm」
			//「yyyy年mm月dd日hh:mm:ss」		→「yyyy/mm/dd hh:mm:ss」
			//「yyyy年mm月dd日 hh:mm:ss」		→「yyyy/mm/dd hh:mm:ss」
			{
				//カッコで囲まれた曜日の除去
				{
					//カッコを全角に統一
					strDate = strDate.Replace("(", "(");
					strDate = strDate.Replace(")", ")");

					//カッコを1つのスペースに変換
					Regex regex = new Regex(@"((.*))");
					MatchCollection matchCol = regex.Matches(strDate);
					if (matchCol.Count > 0)
						strDate = strDate.Replace(matchCol[0].Groups[1].Value, " ");
				}

				strDate = strDate.Replace(" ", " ");	//全角スペースの半角化
				strDate = strDate.Replace("年", "/");
				strDate = strDate.Replace("月", "/");
				strDate = strDate.Replace("日 ", " ");	//後ろにスペースのある「日」はそのまま除去
				strDate = strDate.Replace("時", ":");
				strDate = strDate.Replace("分", ":");
				strDate = strDate.Replace("秒", "");

				//2つ以上のスペースを1つに変換
				while (strDate.IndexOf("  ") >= 0)
				{
					strDate = strDate.Replace("  ", " ");
				}
				//前後のスペースを除去
				{
					strDate = strDate.TrimStart();
					strDate = strDate.TrimEnd();
				}

				//以下の4パターンを考慮して「日」を除去する
				//「yyyy/mm/dd日」→「yyyy/mm/dd」
				//「yyyy/mm/dd日hh:mm」→「yyyy/mm/dd hh:mm」
				//「yyyy/mm/dd日hh:mm:ss」→「yyyy/mm/dd hh:mm:ss」
				//「yyyy/mm/dd日 hh:mm:ss」→「yyyy/mm/dd hh:mm:ss」
				if (strDate.IndexOf("日") > 0)
				{
					strDate = strDate.Replace("日 ", "");			//スペース除去
					Regex regex = new Regex(@"(\d+)日(\d+)");
					MatchCollection matchCol = regex.Matches(strDate);
					if (matchCol.Count > 0)
						strDate = strDate.Replace("日", " ");
					else
						strDate = strDate.Replace("日", "");
				}
			}



			//「yyyy/mm/dd」「yyyy/mm/d」「yyyy/m/dd」「yyyy/m/d」
			if (strDate.Length == 10 || strDate.Length == 9 || strDate.Length == 8)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+)");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 4)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyymmdd」
			if (strDate.Length == 8)
			{
				Regex regex = new Regex(@"(\d{4})(\d{2})(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 4)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyy/mm/dd hh:mm」「yyyy/mm/d hh:mm」「yyyy/m/dd hh:mm」「yyyy/m/d hh:mm」「yyyy/m/d h:mm」
			if (strDate.Length == 16 || strDate.Length == 15 || strDate.Length == 14 || strDate.Length == 13)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+) (\d+):(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 6)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
									, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), 0);
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}

			//「yyyy/mm/dd hh:mm:ss」「yyyy/m/dd hh:mm:ss」「yyyy/mm/d hh:mm:ss」「yyyy/m/d hh:mm:ss」「yyyy/m/d h:mm:ss」
			if (strDate.Length == 19 || strDate.Length == 18 || strDate.Length == 17 || strDate.Length == 16)
			{
				Regex regex = new Regex(@"(\d{4})[-/:](\d+)[-/:](\d+) (\d+):(\d{2}):(\d{2})");
				MatchCollection matchCol = regex.Matches(strDate);
				for (int i = 0; i < matchCol.Count; i++)
				{
					if (matchCol[i].Groups.Count == 7)
					{
						try
						{
							dtDate = new DateTime(Int32.Parse(matchCol[i].Groups[1].Value), Int32.Parse(matchCol[i].Groups[2].Value), Int32.Parse(matchCol[i].Groups[3].Value)
									, Int32.Parse(matchCol[i].Groups[4].Value), Int32.Parse(matchCol[i].Groups[5].Value), Int32.Parse(matchCol[i].Groups[6].Value));
							return true;
						}
						catch (Exception)
						{
						}
					}
				}
			}


			//「2016-04-07T19:01:01+09:00」の形式
			//
			//「2016-04-07T19:01:01」がオフセット+09:00(日本時間)という意味。戻り値は「2016-04-07 19:01:01」
			//「2016-04-07T19:01:01+00:00」の場合、オフセット+00:00(UTC)という意味。戻り値はJST変換して「2016-04-08 04:01:01」
			if (strDate.Length == 25)
			{
				//Parseですませちゃう
				try
				{
					dtDate = DateTime.Parse(strDate);
					return true;
				}
				catch (Exception)
				{
				}
			}

			//「2016-04-07T10:00:44Z」の形式
			//
			//↑はタイムオフセットゼロ=UTCという意味。日本時間にするため時差9時間プラスして返す
			if (strDate.Length == 20)
			{
				//Parseですませちゃう
				try
				{
					dtDate = DateTime.Parse(strDate);
					return true;
				}
				catch (Exception)
				{
				}
			}

			//Parseしてみる
			try
			{
				dtDate = DateTime.Parse(strDate);
				return true;
			}
			catch (Exception)
			{
			}


			dtDate = DateTime.MinValue;
			return false;
		}

	}
}

■Program.cs(jmasubscriber.exe)
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;

namespace jmasubscriber
{
	class Program
	{
		const string _strVerifyToken = "test_token";
		const string _strFolerOut = "jma/";
		const string _strFolerJmaDonwload = "";				//"jmadownload.exe"のあるフォルダ


		static void Main(string[] args)
		{
			//POSTかGETか、というアクセス種別を取得する
			string strMethod = Environment.GetEnvironmentVariable("REQUEST_METHOD");

			//GETの場合は、subscriberチェック→チャレンジコードを返す
			if (strMethod == "GET")
			{
				try
				{
					//GETで↓のような文字列が渡される
					// 「hub.topic=http://www.example.com/test.txt&hub.challenge=9567222552380101910&hub.verify_token=test_token&hub.mode=subscribe&hub.lease_seconds=432000」
					string strQuery = Environment.GetEnvironmentVariable("QUERY_STRING");

					NameValueCollection col = QueryToNameValueCollection(strQuery);

					string strHubMode = col["hub.mode"];
					string strHubChallenge = col["hub.challenge"];
					string strHubVerifyToken = col["hub.verify_token"];

					if (strHubMode == "subscribe" || strHubMode == "unsubscribe")
					{
						if (strHubVerifyToken != _strVerifyToken || strHubChallenge == null || strHubChallenge == "")
						{
							Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");		//unknown reqは404じゃないけど気にしない
							Console.WriteLine("Content-Type: text/plain");
							Console.WriteLine("");
							Console.WriteLine("failed(1).");
							return;
						}

						Console.WriteLine("HTTP/1.1 200 \"OK\"");
						Console.WriteLine("Content-Type: text/plain");
						Console.WriteLine("");
						Console.Write(strHubChallenge);		//WriteLine()はNG。チャンレンジコードを返す
						return;
					}
				}
				catch (Exception)
				{
				}

				Console.WriteLine("HTTP/1.1 404 \"Unknown Request\"");
				Console.WriteLine("Content-Type: text/plain");
				Console.WriteLine("");
				Console.WriteLine("failed(2).");
				return;
			}


			//POSTの場合は、情報の配信→受信内容を保存する
			if (strMethod == "POST")
			{
				//何らかのエラーがありえる状態でも、とりあえず先に200 okを返しておく
				Console.WriteLine("HTTP/1.1 200 \"OK\"");
				Console.WriteLine("Content-Type: text/html");
				Console.WriteLine("");

				try
				{
					using (Stream st = Console.OpenStandardInput())			//標準入力から配信内容取得
					using (BinaryReader br = new BinaryReader(st))			//StreamReader.ReadLine()などは使えない。BinaryReaderで処理
					{
						string strLength = Environment.GetEnvironmentVariable("CONTENT_LENGTH");
						int nLength = Int32.Parse(strLength);

						if (nLength < 1000000)		//約1MB以上ある場合は無視する(そんなに大きなサイズが来ることはないはず)
						{
							byte[] data = new byte[nLength];
							br.Read(data, 0, nLength);

							//効率は悪いけど一度文字列に変換しちゃう
							string strQuery = Encoding.UTF8.GetString(data, 0, nLength);

							//ファイル保存。ファイル名は日時ticks数値
							string strFile = _strFolerOut + DateTime.Now.Ticks + ".txt";
							using (StreamWriter sw = new StreamWriter(strFile, false, Encoding.UTF8))
							{
								sw.Write(strQuery);
							}

							try
							{
								//配信内容のダウンロードアプリを起動
								System.Diagnostics.Process.Start(_strFolerJmaDonwload + @"jmadownload.exe", "\"" + strFile + "\"");
							}
							catch (Exception)
							{
							}
						}
					}
				}
				catch (Exception)
				{
				}
				return;
			}
		}

プロジェクトファイルをダウンロード

2016年04月13日

第04回 府県天気予報XMLを整形する

気象庁防災情報XMLでは天気予報関連の配信は
・府県天気概況
・府県天気予報
・府県週間天気予報
・地方週間天気予報
の4種類があります。

今回はこのうち「府県天気予報」のXMLを読み込み/変換して出力します。


府県天気予報XMLには、
例えば宮崎地方気象台発表のものであれば、

・南部平野部
・北部平野部
・南部山沿い
・北部山沿い
の4地域の今日/明日/明後日の天気や風などの予報

・宮崎
・延岡
・都城
・高千穂
の4箇所の今後24時間までの3時間ごとの気温/風予報

・宮崎
・延岡
・都城
・高千穂
・油津
の5箇所の今日/明日の最高最低気温の予報

などが記載されています。

情報量が多くていいのですが、大きく2つの問題があります。
・どの都道府県の予報なのか分からない(地域名や○○気象台の名前などから判断するしかない)
・「宮崎」「延岡」などがどの地域(「南部山沿い」などの地域)に属するか分からない(別途対応表の用意が必要)

「宮崎県」だけであればたいした問題ではないのですが、47都道府県の処理を考えると少し面倒です。
さらにデータ構造に癖のあるXMLで扱いにくいため、気象庁のXMLから欲しい情報だけを抜き出して、別構造のXMLへと変換してしまいます。

データの階層構造は、地域の下にタイムシリーズ(「今日」「明日」などの時間区分)、その下に天気予報を配置する形にしました。
本来ならある程度汎用的な様式でのXMLに変換するべきなのでしょうが、そこまで拘る必要もないので特化したXMLです。

最終的な出力は、
・変換した結果の独自形式XML forecast○○.xml
・気象庁から配信された生XML forecast○○_raw.xml
の2ファイル×47都道府県=合計94ファイルとしました。




地点名、地域、都道府県の対応表は気象庁が技術資料として用意しているExcelファイルをCSV変換/修正したものを用意して対応しました。

以下の2ファイルをソースコード内の_strFolderDataで指定したフォルダへ保存しておきます。
・20160301_AreaInformationCity-AreaForecastLocalM.csv
・20160314_PointAmedas_mod.csv

気象庁防災情報XMLフォーマット 技術資料
http://xml.kishou.go.jp/tec_material.html




プロジェクトを作成します。
前回のソリューションを開き、ソリューションウインドウの「JmaForecast」ソリューションを右クリックして現れたメニューから「追加」にある「新しいプロジェクト」を選択し、
「Visual C#」の「コンソールアプリケーション」を追加します。
ここでプロジェクト名は「jmacnvforecast」、.NETのバージョンは「.NET Framework 4」としました。
さらに「プロジェクト」メニューにある「jmacnvforecastのプロパティ」を選択し、構成「Release」にたいして、デバッグ」タブにある「Visual Studioホスティングプロセスを有効にする」のチェックをはずしておきます。

今回はクライアント側でアクセスするためのXMLを整形/出力する処理があります。
その出力用XMLのクラス類を保存するためにプロジェクト内にソースファイルを1つ追加しておきます。
ソリューションウインドウの「jmacnvforecast」を右クリックして現れたメニューから「追加」にある「新しい項目」を選択し、「コードファイル」を追加します。
ファイル名は「SerializeData.cs」としました。

そしてソースコードを作成。
最後にjmadownload.exe内でjmacnvforecast.exeを実行するように修正したら完成です。

プロジェクトファイルをダウンロード

前の10件 1  2  3





usefullcode@gmail.com

About 2016年04月

2016年04月にブログ「UsefullCode.net」に投稿されたすべてのエントリーです。

前の記事は2016年03月です。

次の記事は2016年07月です。

他にも多くのエントリーがあります。メインページ記事一覧も見てください。