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 対応が存在しないため