諸事情あって Instagram の投稿の情報を取得することになりました。とりあえず API 使うなんて超メンドクサイので、個人ページに表示されている投稿の情報をスクレイピングで取り出そうと考えます。簡単に言うと、以下のサムネイル部分の情報ですね。
Instagram は React を使っている
こんなの超簡単だろうと思っていたらさすが Instagram。ページが React でできているようで、HTML を取得しても DOM のとして展開されていません *1。これは困った...。
とはいえ JavaScript で動的に展開しているだけなので、どこかに表示するのに利用する情報は隠れているでしょうとアタリは付きます。ということで HTML をジロジロと眺めていると script タグに JSON 形式でしっかり入っているじゃないですか!これを読み取れば何とかなる...!
サンプルコード
先の 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 を使ったサイトでもそれっぽくスクレイピングできました。とは言え、ちょっと意地になってしまった...。