はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】GoogleNewsAPIを使ってニュースを取得してみる

はじめに

今回はGoogleNewsAPIを使ってニュースを取得してみる記事になります!

とある用事でUnity内でニュースを取得するスクリプトを書いてみました。

スクリプト

NewsReader.cs
using System;
using System.Collections.Generic;
using System.Collections;
using UnityEngine;

namespace Hanachiru.News
{
    public static class NewsReader
    {
        private static readonly string GOOGLE_NEWS_API_URL = "https://news.google.com/news/rss/{0}?hl=ja&gl=JP&ned=jp";
        private static readonly string SERCH_URL = "search/section/q/{0}/{0}/";

        /// <summary>
        /// GoogleNewsAPIを使ってニュース(タイトル・概要・リンク)を読み込む
        /// </summary>
        /// <param name="onFinishReading">読み込み終了後のコールバック</param>
        /// <param name="keywords">検索キーワード</param>
        public static IEnumerator ReadNews(Action<NewsData> onFinishReading, IReadOnlyCollection<string> keywords = null)
        {
            string title = null;
            string description = null;
            string link = null;

            string news = null;

            bool isFinished = false;

            //GoogleNewsAPIでXmlを取得
            NewsTools.GetDataFromNet(KeywordsToURL(keywords), (s, flag) => {
                news = s;
                isFinished = flag;
            });

            yield return new WaitUntil(() => isFinished);

            //newsからタイトル・概要・newsDetailURLの抽出
            var root = NewsTools.ParseXml(news);
            title = NewsTools.ExtractDataFromXML(root, "title", true);
            title = NewsTools.RemoveHtmlTags(title, true);
            description = NewsTools.ExtractDataFromXML(root, "description", true);
            description = NewsTools.RemoveHtmlTags(description, true);
            link = NewsTools.ExtractDataFromXML(root, "link", true);

            onFinishReading(new NewsData(title, description, link));
        }

        /// <summary>
        /// keywordのコレクションからGoogleNewsAPI用URLに変換
        /// </summary>
        private static string KeywordsToURL(IReadOnlyCollection<string> keywords)
        {
            if (keywords == null) return String.Format(GOOGLE_NEWS_API_URL, "");

            string serchText = "";

            foreach (var keyword in keywords)
            {
                serchText += NewsTools.EncodeUrl(keyword) + " ";
            }

            return String.Format(GOOGLE_NEWS_API_URL, String.Format(SERCH_URL, serchText));

        }

    }

    public class NewsData
    {
        public string Title { get; }

        public string Description { get; }

        public string Link { get; }

        public NewsData(string title, string description, string link)
        {
            Title = title;
            Description = description;
            Link = link;
        }
    }

}
NewsTools.cs
using System;
using System.Text.RegularExpressions;
using System.Linq;
using System.Net;
using System.IO;
using UniRx;

namespace Hanachiru.News
{
    public static class NewsTools
    {
        private const string PATTERN_FOR_XML_OF_GOOGLE_API = "[(&nbsp)(&shy)(&ensp)(&thinsp)(ニュースですべての記事を表示)(;)(|)]";


        /// <summary>
        /// 指定したURLからJsonやXml(rss)などのデータを取得する
        /// </summary>
        /// <param name="url">対象のURL</param>
        /// <param name="onFinishReading">入手したデータ・終了を知らせるフラグを引数にもつコールバック</param>
        public static void GetDataFromNet(string url, Action<string, bool> onFinishReading)
        {
            //別スレッドで実行
            Observable.Start(() =>
            {
                try
                {
                    var req = (HttpWebRequest)WebRequest.Create(url);
                    var res = (HttpWebResponse)req.GetResponse();
                    using (var reader = new StreamReader(res.GetResponseStream()))
                    {
                        return reader.ReadToEnd();
                    }
                }
                catch
                {
                    return "";
                }
            })
            .ObserveOnMainThread()         //メインスレッドに切り替え
            .Subscribe(data =>
            {
                onFinishReading(data, true);
            });
        }


        /// <summary>
        /// テキストをUTF-8に変換する(URLエンコード)
        /// </summary>
        public static string EncodeUrl(string text)
        {
            if (string.IsNullOrEmpty(text)) return "";

            return Uri.EscapeDataString(text);
        }

        /// <summary>
        /// テキストからHTMLタグを削除する
        /// </summary>
        public static string RemoveHtmlTags(string text, bool shouldDeleteSpecialCharacter = false)
        {
            if (string.IsNullOrEmpty(text)) return "";

            string s = RemoveCharacter(text, "<[^>]*?>");

            if (!shouldDeleteSpecialCharacter) return s;

            return RemoveCharacter(s, PATTERN_FOR_XML_OF_GOOGLE_API);
        }


        /// <summary>
        /// テキストから正規表現を用いて文字列を抽出する
        /// </summary>
        /// <param name="text">テキスト</param>
        /// <param name="pattern">正規表現</param>
        public static string ExtractCharacter(string text,string pattern)
        {
            if (string.IsNullOrEmpty(text)) return "";

            return Regex.Match(text, pattern).Value;
        }

        /// <summary>
        /// テキストから正規表現を用いて文字列を削除
        /// </summary>
        /// <param name="text">テキスト</param>
        /// <param name="pattern">正規表現</param>
        public static string RemoveCharacter(string text,string pattern)
        {
            if (string.IsNullOrEmpty(text)) return "";

            return Regex.Replace(text, pattern, string.Empty);
        }

        public static System.Xml.Linq.XElement ParseXml(string data)
        {
            if (string.IsNullOrEmpty(data)) return null;

            System.Xml.Linq.XDocument xml = System.Xml.Linq.XDocument.Parse(data);
            System.Xml.Linq.XElement root = xml.Root;

            return root;
        }

        /// <summary>
        ///  Xml形式データから指定のタグのデータを抽出する
        /// </summary>
        public static string ExtractDataFromXML(System.Xml.Linq.XElement root, string tag, bool shouldSkip = false)
        {
            if (root == null) return "";

            var targetData = root.Descendants(tag);

            if (shouldSkip)
            {
                return targetData.Select(x => x.Value)
                    .Skip(1)
                    .FirstOrDefault();
            }

            return targetData.Select(x => x.Value)
                .FirstOrDefault();
        }

    }

}

使い方

StartCoroutine(NewsReader.ReadNews(news => {
    Debug.Log("title: " + news.Title);
    Debug.Log("description: " + news.Description);
    Debug.Log("link: " + news.Link);
}));

f:id:hanaaaaaachiru:20190626195724p:plain
※この画像のリンクが間違っていたのですが、コードは修正をしておきました。

さいごに

もっと良いニュースであふれた世界になりますように…。

追記)KeywordsToURLメソッド内でキーワードを繋ぐためにスペースを利用していますが、ちゃんとこれもURLエンコードした方がいいです。あとで時間があれば直しておきますね。