今回はsubscriberへ配信された内容を元に記事をダウンロードします。
気象庁防災情報XMLではsubscriberへ以下のようなfeedが配信されます。
entryの数はfeedによって異なり、実際の記事内容はlinkとしてURLが記載されています。
今回はこのlinkをダウンロードします。
なお、linkのURLは配信から数時間だけ有効で、それを過ぎると404エラーでダウンロードできなくなります。
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="https://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="https://www.jma.go.jp/" rel="related"/> <link href="https://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="https://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="https://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="https://www.jma.go.jp/" rel="related"/> <link href="https://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="https://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="https://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です。
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(@"https://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="https://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="https://www.jma.go.jp/" rel="related"/> //<link href="https://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="https://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="https://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=https://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; } }