空談録

世界で5人くらいに役立ちたい

Power Queryを使ってExcel上にRSS 2.0のフィードリーダーを作成する

気が付いたら顔面フリー素材になってました

というわけでExcel VBAでWebサービス - Excelでフィードを読もう(RSS2.0編) (1) フィードについて | マイナビニュースとか見かけたのでせっかくだしPower Queryで同じようなことをしてみたいと思ってやってみました
しかしPower Query先生は突然の職務放棄をするのであった…

まあとりあえずやっていきましょう
まずは拾ってくるフィードのURLが必要です
今回は我らがblogs.office.comのフィードを拾ってきます
blogs.office.com/feed/  ですね

理由としては特にないのですがWordPressだとRSS 2.0で飛ばしてくれるので解析も楽だったりするというのが大きいです
ATOMは読む気力がなく…

(というよりblogs.msdn.comのフィードがRSS 2.0とATOMとが乱立しててうわあああってなったのでいったん放置)

WordPressのフィードなら改行とかもしてて綺麗なのでまずはこの辺から読むのがいいかなって

まあそんなことは置いといてとりあえずPower Queryに読み込みましょう
まずは適当に= Web.Contents("http://blogs.office.com/feed/")って書いときましょう
すると大体こんな感じになります
https://portalvhdsrp3qt9v47nzbn.blob.core.windows.net/publicphoto/fspic140617.png
すると取得したデータに応じて勝手にルートオブジェクトまで読んでくれます
微妙に嫌な予感もしますが今回は特に問題ないのでこのまま行きます

channelの中身をとりあえず選択してRSSの基本的な情報を展開します
このときのitemがフィードのデータなのですが、そのままitem選択するとブログのタイトルがないのでこの時点で補完します

itemのTableの各要素に対し現在のtitleのデータを追加できればいいのでTable.AddColumnで対応します。このとき要素は一つですがTable.TransformColumnsで変換します
(もちろん関数で一つ目の要素にアクセスしてもいいのですが自力で作るのも手間)

適当に書くとTable.TransformColumns(ChangedType1, {"item", each Table.AddColumn(_, "Feed Title",(x) => ChangedType1{0}[title])})とかになります。
AddColumnの第3引数に値をそのまま入れたいのですがなんかうまくいかないので無駄な関数になっています

ここまでしたらあとは適当にitem選択して無駄な列消して並び替えるだけです
並び替えた後がこんな感じ
https://portalvhdsrp3qt9v47nzbn.blob.core.windows.net/publicphoto/fspic140617-2.png

とりあえずクエリの全文はこんな感じ

let
    ソース = Xml.Tables(Web.Contents("http://blogs.office.com/feed/")),
    ChangedType = Table.TransformColumnTypes(ソース,{{"Attribute:version", type number}}),
    channel = ChangedType{0}[channel],
    ChangedType1 = Table.TransformColumnTypes(channel,{{"title", type text}, {"link", type text}, {"lastBuildDate", type datetime}, {"language", type text}, {"generator", type text}}),
    Custom1 = Table.TransformColumns(ChangedType1, {"item", each Table.AddColumn(_, "Feed Title",(x) => ChangedType1{0}[title])}),
    item = Custom1{0}[item],
    RemovedColumns = Table.RemoveColumns(item,{"comments", "http://purl.org/dc/elements/1.1/", "guid", "http://purl.org/rss/1.0/modules/content/", "http://wellformedweb.org/CommentAPI/", "http://purl.org/rss/1.0/modules/slash/", "enclosure"}),
    ReorderedColumns = Table.ReorderColumns(RemovedColumns,{"title", "Feed Title", "link", "pubDate", "description"})
in
    ReorderedColumns

まあこんなもんかなという感じなのでとりあえずワークシートに読み込みましょう

…そう、ここからが実は問題です
何がってPower Queryはテーブルを出力しますが各要素はすべて"文字列"で出してくれます
えっ? RSSリーダーなのにURLに直接飛べない…? ゴミだ…

何とかして作れたりしないん?ってなるんですがフォーラムにこんなものが
Transforming column content to a hyperlink in Power Queryによるとなるほど、直接Hyperlinkは生成できないから手で直接やってね!って書いてありますね

というわけで非常につらい選択ですが過去の遺産を用いましょう
イベントがどれに対応してるのかいまいち謎なのでこんな感じで対応

Private Sub Worksheet_Change(ByVal Target As Range)
    Dim i As Integer
    i = 2
    Do
        If Cells(i, 3).Value = "" Then
            Exit Sub
        End If
        Call Hyperlinks.Add(Cells(i, 3), Cells(i, 3).Value)
        i = i + 1
    Loop
End Sub

何かずれると一瞬で死ぬソースコードです
まあRSSリーダー専用ワークブックとしてはお仕事してくれると思います

 

とりあえずここまでやったので次のステップに行きましょう。
これだと1つのフィードを読むたびにシートが増殖していってあれなので複数のフィードを一つのシートにまとめて表示してみます

Tableを結合したりする謎関数があればいいんですが、複数のテーブルのフィールドが同じ可能性が少ないからでしょうがそのようなものはないので最初っからまとめて処理していきます
まず取得するフィードのListを作成します。こんな感じの
= {"http://blogs.office.com/feed/","http://grabacr.net/feed","http://blogs.msdn.com/b/shintak/rss.aspx"}
選んだフィードに特に悪意はございません。思いついたの選びました
フィードを増やしたり変更する場合最初のこのListを編集していきます

残りはさっきのクエリをベースとし、List.Transformで変更していきます。
さっきのと共通のletの最初の4行はこんな感じ

    feeds = {"http://blogs.office.com/feed/","http://grabacr.net/feed","http://blogs.msdn.com/b/shintak/rss.aspx"},
    xmls = List.Transform(feeds, each Xml.Tables(Web.Contents(_)){0}[channel]),
    liTrans = List.Transform(xmls, (x) => Table.TransformColumns(x, {"item", each Table.AddColumn(_, "Feed Title",(y) => x{0}[title])})),
    items = List.Transform(liTrans, each _{0}[item]),

わざわざ追加してくれたTransformColumnTypesは今回は必要ないので省略
そもそもさっきのも必要ないんですけどね。自動で追加されるのでまあしょうがない

こうしたらこの時点でテーブルの要素を必要な分だけにしておきます。
おそらく後からでも消せる気はするんですがまあどっちでも
List.Transform(items, each Table.RemoveColumns(_,{"comments", "http://purl.org/dc/elements/1.1/", "guid", "http://purl.org/rss/1.0/modules/content/", "http://wellformedweb.org/CommentAPI/", "http://purl.org/rss/1.0/modules/slash/", "enclosure", "category"},MissingField.Ignore)),
とかで対応できます。MissingField.Ignoreはないとエラーになるので消せませんが他は適宜増やしたり消したりで

とりあえず全部のTableの列が同じになったところで一つのTableにまとめます
TableをいったんListにして展開、その後Tableに戻します
Table→ListはおなじみList.TransformManyでやることで最初からずっといじってたListの要素に変換できます
そうしたらあとはTable.FromRecordsとかで簡単に

    merged = List.TransformMany(reorder, (x) => Table.ToRecords(x), (x,y) => y),
    tbTrans = Table.FromRecords(merged)

これでinにはtbTransでいけます
クエリ全文はこんな感じ

let
    feeds = {"http://blogs.office.com/feed/","http://grabacr.net/feed","http://blogs.msdn.com/b/shintak/rss.aspx"},
    xmls = List.Transform(feeds, each Xml.Tables(Web.Contents(_)){0}[channel]),
    liTrans = List.Transform(xmls, (x) => Table.TransformColumns(x, {"item", each Table.AddColumn(_, "Feed Title",(y) => x{0}[title])})),
    items = List.Transform(liTrans, each _{0}[item]),
    removed = List.Transform(items, each Table.RemoveColumns(_,{"comments", "http://purl.org/dc/elements/1.1/", "guid", "http://purl.org/rss/1.0/modules/content/", "http://wellformedweb.org/CommentAPI/", "http://purl.org/rss/1.0/modules/slash/", "enclosure", "category"},MissingField.Ignore)),
    reorder = List.Transform(removed, each Table.ReorderColumns(_, {"title","Feed Title", "link", "pubDate", "description"})),
    merged = List.TransformMany(reorder, (x) => Table.ToRecords(x), (x,y) => y),
    tbTrans = Table.FromRecords(merged)
in
    tbTrans

reorderのところは後からでもできます。あとからやるほうが楽です。

これでワークシートに読み込んでさっきのVBA使った結果がこんな感じです
https://portalvhdsrp3qt9v47nzbn.blob.core.windows.net/publicphoto/fspic140617-3.png

案外使えそうな感じではありますね
あとはクエリを更新するだけで勝手にフィードが落ちてきてHyperlinkに変換してくれます



というわけで随分と謎記事になりました
実用性はまったく保証しません

そもそもこの程度のフィードリーダーを作るのに2時間くらいかかってるので普通にC#でSyndicationFeed使ったほうが楽という…
行数よりもどの関数でなにするかの把握にすごい時間かかるという

なんかブログ書くのめんどくさいなぁと思ったりしたのでいろいろ頑張らないとなぁ
このへんで