xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

React ベースでもお構いなし!API を使わずに Instagram の投稿サマリーをスクレイピングで取得する

諸事情あって Instagram の投稿の情報を取得することになりました。とりあえず API 使うなんて超メンドクサイので、個人ページに表示されている投稿の情報をスクレイピングで取り出そうと考えます。簡単に言うと、以下のサムネイル部分の情報ですね。

f:id:xin9le:20161130012747p:plain

Instagram は React を使っている

こんなの超簡単だろうと思っていたらさすが Instagram。ページが React でできているようで、HTML を取得しても DOM のとして展開されていません *1。これは困った...。

とはいえ JavaScript で動的に展開しているだけなので、どこかに表示するのに利用する情報は隠れているでしょうとアタリは付きます。ということで HTML をジロジロと眺めていると script タグに JSON 形式でしっかり入っているじゃないですか!これを読み取れば何とかなる...!

f:id:xin9le:20161130014139p:plain

サンプルコード

先の JSON を読み込むサンプルをザッと書いてみました。今回は .NET Core ベースで書いているので、SgmlReader ではなく HtmlAgilityPack.NetCore を利用しています *2

//--- データ取得
public static async Task<InstagramPostSummary[]> GetInstagramPostSummaris(string userAccount)
{
    //--- HTML 取得
    var url = $"https://www.instagram.com/{userAccount}";
    var html = await GetHtmlAsync(url);

    //--- React でできているので DOM の直接解析はできない
    //--- データ部分の JSON を取得
    var targetText = "window._sharedData = ";
    var json = html.DocumentNode
             .SelectNodes(@"/html[1]/body[1]/script")
             .First(x => x.InnerText.StartsWith(targetText))
             .InnerText
             .TrimEnd(';')
             .Replace(targetText, string.Empty);

    //--- JSON を扱いやすい型に変換
    dynamic jObj = JsonConvert.DeserializeObject(json);
    JArray nodes = jObj.entry_data.ProfilePage[0].user.media.nodes;
    return nodes.Select(InstagramPostSummary.From).ToArray();
}

//--- HTML 取得
public static async Task<HtmlDocument> GetHtmlAsync(string url)
{
    using (var client = new HttpClient())
    using (var stream = await client.GetStreamAsync(url))
    {
        var doc = new HtmlDocument();
        doc.Load(stream);
        return doc;
    }
}

//--- データの入れ物になる型
public class InstagramPostSummary
{
    public long Id { get; private set; }
    public string Code { get; private set; }
    public long OwnerId { get; private set; }
    public DateTime Date { get; private set; }
    public string Caption { get; private set; }
    public int Width { get; private set; }
    public int Height { get; private set; }
    public string DisplaySource { get; private set; }
    public string ThumbnailSource { get; private set; }
    public bool CommentsDisabled { get; private set; }
    public int Comments { get; private set; }
    public int Likes { get; private set; }
    public bool IsVideo { get; private set; }
    public int? VideoViews { get; private set; }

    private InstagramPostSummary()
    {}

    public static InstagramPostSummary From(JToken token)
    {
        dynamic x = token;
        return new InstagramPostSummary
        {
            Id = (long)x.id,
            Code = (string)x.code,
            OwnerId = (long)x.owner.id,
            Date = new DateTime(1970, 1, 1) + TimeSpan.FromSeconds((double)x.date),
            Caption = (string)x.caption,
            Width = (int)x.dimensions.width,
            Height = (int)x.dimensions.height,
            DisplaySource = (string)x.display_src,
            ThumbnailSource = (string)x.thumbnail_src,
            CommentsDisabled = Convert.ToBoolean((string)x.comments_disabled),
            Comments = (int)x.comments.count,
            Likes = (int)x.likes.count,
            IsVideo = Convert.ToBoolean((string)x.is_video),
            VideoViews = (int?)x.video_views,
        };
    }
}

サイトによってやり方はまちまちですが、React を使ったサイトでもそれっぽくスクレイピングできました。とは言え、ちょっと意地になってしまった...。

*1:React は実行時に仮想 DOM から DOM を動的生成するため

*2:SgmlReader は投稿時点で .NET Core 対応が存在しないため

HTC Vive でコナミコマンド実装してみた

先日 Japan VR Summit 2 に向けて制作し、ブース展示した Grani VR Office Tour。その開発の一環でデバッグコマンドを入れていました。要件はこんな感じ。

  • 特殊なコマンド入力したらとある処理が発動
  • 普段の操作ではまず入力できない難易度
  • 覚えやすい

「それってコナミコマンドだよね!」ということで、HTC Vive コントローラーで動くコナミコマンドを実装しました。

f:id:xin9le:20161122164006p:plain

サンプルコード

実装は極めてシンプルで、UniRx を使えばチョチョイのチョイで終わります。以下がその実装例。なんとコレだけ!あとは発動時に実行したい任意の処理を書くだけです。

//--- こんな感じでボタンが定義されていたとして
public enum Buttons
{
    Top,
    Bottom,
    Left,
    Right,
    A,
    B,
}


public class KonamiCommand : MonoBehaviour
{
    [SerializeField]
    private GameObject controller;

    private void Start()
    {
        //--- コナミコマンドのボタン順序定義
        var buttonOrder = new []
        {
            Buttons.Top,
            Buttons.Top,
            Buttons.Bottom,
            Buttons.Bottom,
            Buttons.Left,
            Buttons.Right,
            Buttons.Left,
            Buttons.Right,
            Buttons.B,
            Buttons.A,
        };

        //--- UniRx を使って IObservable<T> 化したコントローラーのイベント
        var events = controller.GetComponent<ReactiveControllerEvents>();
        Observable.Merge
        (
            events.TouchpadTopPressedAsObservable().Select(_ => Buttons.Top),
            events.TouchpadBottomPressedAsObservable().Select(_ => Buttons.Bottom),
            events.TouchpadLeftPressedAsObservable().Select(_ => Buttons.Left),
            events.TouchpadRightPressedAsObservable().Select(_ => Buttons.Right),
            events.GripPressedAsObservable().Select(_ => Buttons.B),
            events.ApplicationMenuPressedAsObservable().Select(_ => Buttons.A)
        )
        .Buffer(buttonOrder.Length, 1)
        .Do(xs =>
        {
            //--- とりあえず確認のために console に表示
            var message = string.Join(", ", xs.Select(x => x.ToString()).ToArray());
            Debug.Log(message);
        })
        .Where(xs => buttonOrder.SequenceEqual(xs))  //--- 順序が一致していたら
        .Subscribe(_ =>
        {
            Debug.Log("コナミコマンド発動!");
            //--- do something
        })
        .AddTo(this);
    }
}

f:id:xin9le:20161122190532p:plain

当然ですが、Oculus Touch や他のデバイスでも同様にできます。Let's try!!

ConcurrentDictionary.GetOrAdd のファクトリーメソッドは排他制御されていない

というタイトルの通りなのですが、案外忘れがちです。例えば以下のような期待をしてはいけません。

//--- こんな排他制御機能付きの辞書があるとする
var dic= new ConcurrentDictionary<string, int>();

//--- 値がなければ追加したいけれど...
var value = dic.GetOrAdd(key, x =>
{
    //--- このスコープの処理は排他制御されていないんだZE!!
    return newValue;
});

内部実装を確認

この挙動は Reference Source で .NET Framework の内部実装を見てみれば分かります。ConcurrentDictionary の実装から GetOrAdd の部分をピックアップしたのが以下です。

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
    if (key == null) throw new ArgumentNullException("key");
    if (valueFactory == null) throw new ArgumentNullException("valueFactory");

    TValue resultingValue;
    if (TryGetValue(key, out resultingValue))
    {
        return resultingValue;
    }

    //--- valueFactory の実行が何もロックされていない
    TryAddInternal(key, valueFactory(key), false, true, out resultingValue);
    return resultingValue;
}

上記の通り valueFactory(key) で引数で与えたファクトリーメソッドを実行しているのですが、何も排他制御されていません。排他制御がかかっていると思って気を緩めてはいけないので気を付けましょう!

Grani VR Office Tour - 最先端を追求するグラニの新たな取り組み

今回は少し趣向を変えて (?)、僕が所属する Grani, Inc. の新たな取り組みについて紹介します!先日の CLR/H in Tokyo 第 11 回でも「one more thing...」として軽く触れたのですが、来月 11/16 (水) に開催される Japan VR Summit 2グラニも VR コンテンツのブースを出展します。

Grani VR Office Tour

内容は弊社グラニのオフィスツアー。「VR なら会社まで来なくても会社見学できるよね」という発想ですね!専用のサイトもあります。

最先端のスキャン技術を用いたオフィスツアー

やるからには当然リアリティを追求したい。ということで、最先端のスキャン技術で超高精細な 3D モデルを構築しています。どれくらい精細かというと、社員ですら「ここ、もう本当に会社じゃん...」と錯覚するレベルです。

以下は社内をスキャンしたポイントクラウド (点群) データを可視化したもののひとつ。非常に高い精度で座標が取得できていて、形状がハッキリ出ているのが分かるのではないかと思います。

f:id:xin9le:20161114105250j:plain

先の VR Office Tour のロゴも実は写真じゃないんですよ!しかも、現在はそのロゴで使われている 3D モデルよりもさらにテクスチャなどが進化しています。

FPS の維持

VR の最大の敵は VR 酔いです。「コレが起きるコンテンツには価値がない」と言われても過言ではないくらい、シッカリと対策しなければなりません。

特に気を付けなければならないのは FPS (= 1 秒間あたりの画面描画回数) で、PC VR では *1 90 FPS 程度の確保がひとつのラインと言われています。そして、この値がある程度出る (= VR コンテンツをある程度快適に楽しめる) 高性能なマシン/グラフィックカードを「VR Ready」と呼んだりします。

現在のグラフィックカードで最上位モデルと言われる GeForce GTX 1080 でこの数字を出せればいいと言えばそうなのかもしれませんが、グラニでは FPS に対してよりシビアな肌感覚を養うため、敢えて VR Ready ではない GTX 960 をメインで使って開発しています。今回制作している VR Office は、そんな環境下でも一定の FPS が出せる程度にはパフォーマンスに気を配っています。

今後は?

今回の VR Office 制作にはかなり多くの要素を詰め込みました。当然試行錯誤なので作ったけれど捨てたものもあります。この間、非常に多くの知見を得てきたので、今後少しずつでも公開できていければと思っています。

*1:現在は 120 FPS が目安とも言われています

1Password for Windows Store をインストールする方法

数年愛用しているパスワード管理ツールの 1Password。iOS や Mac 向けのアプリは非常に素晴らしく、なくてはならないアプリのひとつです。パスワードデータの同期機能があるので、Windows 環境でも (なんなら Windows 10 Mobile でも) 使いたいものです。

そしていつ頃だったかは分かりませんが UWP アプリも提供され始めました。なのですが、いつの間にか Windows Store で「1Password」と検索してもヒットしなくなっていました。このままではインストールできない!

f:id:xin9le:20161107001125p:plain

直リンクでストアにアクセス

ということで、直リンクで Windows Store の 1Password にアクセスしましょう。以下のサイトを開けば Windows Store を開くための Popup が出てくるので、あとはダウンロードするだけ!

f:id:xin9le:20161107001621p:plain

UWP 版は機能制限が非常に多く、編集用途には全く使えませんのでご注意ください。