今回もSeleniumを使います。NuGetでSelenium.WebDriverとSelenium.Support、そして操作したいブラウザのDriverをインストールします。今回はChromeを操作するのでSelenium.Chrome.WebDriverをインストールします。
またHTMLを解析するためにAngleSharpが必要なのでNuGetでインストールしておきましょう。
Twitterで「プログラミング」と検索してみます。HTMLがどのようになっているかみてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using OpenQA.Selenium.Chrome; public partial class Form1 : Form { ChromeDriver driver = null; private void button1_Click(object sender, EventArgs e) { driver = new ChromeDriver(); // 「プログラミング」で検索 driver.Url = "https://twitter.com/search?q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0"; // source がどうなっているのか調べてみる string source = driver.PageSource; driver.Quit(); driver.Dispose(); } } |
検索結果に出てくるリンクをみてみると以下のようになっています。a タグで class が”css-4rbku5 css-18t94o4 css-1dbjc4n r-1loqt21 r-1wbh5a2 r-dnmrzs r-1ny4l3l”になっているものを抽出すればよいということがわかります。相対urlになっているので先頭に”https://twitter.com”を追加すればリンクを取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<a href="/XXXX" role="link" data-focusable="true" class="css-4rbku5 css-18t94o4 css-1dbjc4n r-1loqt21 r-1wbh5a2 r-dnmrzs r-1ny4l3l"> <div class="css-1dbjc4n r-1awozwy r-18u37iz r-1wbh5a2 r-dnmrzs r-1ny4l3l"> <div class="css-1dbjc4n r-1awozwy r-18u37iz r-dnmrzs"> <div dir="auto" class="css-901oao css-bfa6kz r-18jsvk2 r-1tl8opc r-a023e6 r-b88u0q r-ad9z0x r-bcqeeo r-3s2u2q r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> プログラミング教室(○○市) </span> </span> </div> <div dir="auto" class="css-901oao r-18jsvk2 r-18u37iz r-1q142lx r-1tl8opc r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0"> </div> </div> <div class="css-1dbjc4n r-18u37iz r-1wbh5a2 r-13hce6t"> <div dir="ltr" class="css-901oao css-bfa6kz r-m0bqgq r-18u37iz r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> @XXXXX </span> </div> </div> </div> </a> |
そこで検索結果からアカウントへのリンクであれば、これで取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
using OpenQA.Selenium.Chrome; using AngleSharp.Html.Parser; public partial class Form1 : Form { ChromeDriver driver = null; private void button1_Click(object sender, EventArgs e) { driver = new ChromeDriver(); // 「プログラミング」で検索 driver.Url = "https://twitter.com/search?q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0"; // 10件だけ取得する List<string> ret = GetLinksFromSearchResult(10); driver.Quit(); driver.Dispose(); } List<string> GetLinksFromSearchResult(int max) { List<string> ret = new List<string>(); while (true) { List<string> vs = new List<string>(); System.Threading.Thread.Sleep(5000); string source = driver.PageSource; HtmlParser parser = new HtmlParser(); var doc = parser.ParseDocument(source); var elms = doc.GetElementsByTagName("a"); foreach (var elm in elms) { string className = elm.GetAttribute("class"); if (className == "css-4rbku5 css-18t94o4 css-1dbjc4n r-1loqt21 r-1wbh5a2 r-dnmrzs r-1ny4l3l") { string link = "https://twitter.com" + a.GetAttribute("href"); if (!vs.Any(x => x == link) && !ret.Any(x => x == link)) vs.Add(link); if (vs.Count + ret.Count >= max) break; } } if (vs.Count == 0) return ret; ret.AddRange(vs); if (ret.Count >= max) return ret; var lastLink = driver.FindElementsByTagName("a").Last(); int y = lastLink.Location.Y; string script = string.Format("window.scrollBy(0, {0})", y); driver.ExecuteScript(script); } } } |
ではフォロワー数が多いアカウントに絞って出力することはできないのでしょうか?
実際に上記の方法で取得できたアカウントに実際にアクセスすればフォロワー数が表示されているので取得できるはずです。ほかにも総ツイート数やアカウント名も取得してみましょう。
アカウント名の部分は以下のようになっています。まず <div class=”css-1dbjc4n r-6gpygo r-14gqq1x”> を取得して、そのなかの <div class=”css-901oao r-18jsvk2 r-1tl8opc r-1b6yd1w r-1vr29t4 r-ad9z0x r-bcqeeo r-qvutc0″> となっている部分と <div class=”css-901oao css-bfa6kz r-m0bqgq r-18u37iz r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0″> となっている部分を抜き出せばアカウント名を取得することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<div class="css-1dbjc4n r-6gpygo r-14gqq1x"> <div class="css-1dbjc4n r-1wbh5a2 r-dnmrzs r-1ny4l3l"> <div class="css-1dbjc4n r-1wbh5a2 r-dnmrzs r-1ny4l3l"> <div class="css-1dbjc4n r-1awozwy r-18u37iz r-dnmrzs"> <div dir="auto" class="css-901oao r-18jsvk2 r-1tl8opc r-1b6yd1w r-1vr29t4 r-ad9z0x r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0">【名前】</span> </span> <span class="css-901oao css-16my406 r-18u37iz r-1q142lx r-1tl8opc r-1b6yd1w r-bcqeeo r-qvutc0"></span> </div> </div> <div class="css-1dbjc4n r-18u37iz r-1wbh5a2"> <div dir="ltr" class="css-901oao css-bfa6kz r-m0bqgq r-18u37iz r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0">@XXXXXX</span> </div> </div> </div> </div> </div> |
フォロー数とフォロワー数は以下のようになっています。<div class=”css-901oao css-16my406 r-18jsvk2 r-1tl8opc r-b88u0q r-bcqeeo r-qvutc0″> の部分で最初がフォロー数で二番目がフォロワー数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<div class="css-1dbjc4n r-1mf7evn"> <a href="/furikake555/following" dir="auto" role="link" data-focusable="true" class="css-4rbku5 css-18t94o4 css-901oao r-18jsvk2 r-1loqt21 r-1tl8opc r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-18jsvk2 r-1tl8opc r-b88u0q r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> 1,034 </span> </span> <span class="css-901oao css-16my406 r-m0bqgq r-1tl8opc r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> フォロー中 </span> </span> </a> </div> <div class="css-1dbjc4n"> <a href="/furikake555/followers" dir="auto" role="link" data-focusable="true" class="css-4rbku5 css-18t94o4 css-901oao r-18jsvk2 r-1loqt21 r-1tl8opc r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-18jsvk2 r-1tl8opc r-b88u0q r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> 1,704 </span> </span> <span class="css-901oao css-16my406 r-m0bqgq r-1tl8opc r-bcqeeo r-qvutc0"> <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0"> フォロワー </span> </span> </a> </div> |
総ツイートは以下のようになっています。この部分を抜き出せばよいということになります。
1 2 3 |
<div dir="auto" class="css-901oao css-bfa6kz r-m0bqgq r-1tl8opc r-n6v787 r-16dba41 r-1sf4r6n r-bcqeeo r-qvutc0"> 3.3万 件のツイート </div> |
ではさっそく作成してみましょう。
まずアプリが起動したらChromeDriverをふたつ生成します。ひとつは検索結果からアカウントへのリンクを取得するためのもので、もうひとつはアカウントにアクセスしてフォロワー数などを調べるためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public partial class Form1 : Form { ChromeDriver driver1 = null; ChromeDriver driver2 = null; public Form1() { InitializeComponent(); CreateDrivers(); } void CreateDrivers() { { ChromeDriverService driverService = ChromeDriverService.CreateDefaultService(); driverService.HideCommandPromptWindow = true; ChromeOptions options = new ChromeOptions(); options.AddArgument("--headless"); driver1 = new ChromeDriver(driverService, options); } { ChromeDriverService driverService = ChromeDriverService.CreateDefaultService(); driverService.HideCommandPromptWindow = true; ChromeOptions options = new ChromeOptions(); options.AddArgument("--headless"); driver2 = new ChromeDriver(driverService, options); } } } |
コンストラクタに渡す引数は同じだからと以下のようにしてしまうと、アプリケーションが終了した後もchromedriverのプロセスが残ってしまいます。
1 2 3 4 5 6 7 8 9 10 11 |
void CreateDrivers() { ChromeDriverService driverService = ChromeDriverService.CreateDefaultService(); driverService.HideCommandPromptWindow = true; ChromeOptions options = new ChromeOptions(); options.AddArgument("--headless"); driver1 = new ChromeDriver(driverService, options); driver2 = new ChromeDriver(driverService, options); } |
処理がどこまで進行しているのかわかるようにプログレスバーを表示させています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
public partial class Form1 : Form { List<AccountInfo> GetAccountInfosFromSearchResult(int max) { Invoke((Action)(() => { progressBar1.Maximum = max; progressBar1.Value = 0; })); List<string> accountUrls = new List<string>(); List<AccountInfo> infos = new List<AccountInfo>(); while (true) { List<string> vs = new List<string>(); Invoke((Action)(() => { progressBar2.Maximum = 10; progressBar2.Value = 0; })); // ページを読み込んだとき、下にスクロールさせたときは5秒待機 for (int i = 0; i < 10; i++) { System.Threading.Thread.Sleep(500); Invoke((Action)(() => { progressBar2.Value++; })); } string source = driver1.PageSource; HtmlParser parser = new HtmlParser(); var doc = parser.ParseDocument(source); var elms = doc.GetElementsByTagName("a"); foreach (var elm in elms) { string className = elm.GetAttribute("class"); // アカウントへのリンクを取得する if (className == "css-4rbku5 css-18t94o4 css-1dbjc4n r-1loqt21 r-1wbh5a2 r-dnmrzs r-1ny4l3l") { string link = "https://twitter.com" + elm.GetAttribute("href"); // すでに取得したリンクであれば無視 if (!vs.Any(x => x == link) && !accountUrls.Any(x => x == link)) { vs.Add(link); // アカウント情報を取得する AccountInfo info = GetAccountInfo(link); // フォロワー数が1000人以上であれば取得する if (info.Followers > 1000) { infos.Add(info); Invoke((Action)(() => { progressBar1.Value++; })); } // 最大取得数に達している場合は終了 if (infos.Count >= max) return infos; } } } // 取得できなかった場合は終了 if (vs.Count == 0) return infos; accountUrls.AddRange(vs); // ページを下にスクロールさせる var lastLink = driver1.FindElementsByTagName("a").Last(); int y = lastLink.Location.Y; string script = string.Format("window.scrollBy(0, {0})", y); driver1.ExecuteScript(script); } } } |
GetAccountInfo(string accountUrl)メソッドは各アカウントに実際にアクセスしてアカウント情報を取得するメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public partial class Form1 : Form { AccountInfo GetAccountInfo(string accountUrl) { driver2.Url = accountUrl; Invoke((Action)(() => { progressBar3.Maximum = 10; progressBar3.Value = 0; })); for (int i = 0; i < 10; i++) { System.Threading.Thread.Sleep(500); Invoke((Action)(() => { progressBar3.Value++; })); } string source = driver2.PageSource; HtmlParser parser = new HtmlParser(); var doc = parser.ParseDocument(source); // フォロー数、フォロワー数を取得 var followersElms = doc.GetElementsByClassName("css-901oao css-16my406 r-18jsvk2 r-1tl8opc r-b88u0q r-bcqeeo r-qvutc0"); string following = followersElms[0].TextContent; string followers = followersElms[1].TextContent; // 総ツイート数を取得 var tweetCountElms = doc.GetElementsByClassName("css-901oao css-bfa6kz r-m0bqgq r-1tl8opc r-n6v787 r-16dba41 r-1sf4r6n r-bcqeeo r-qvutc0"); string tweetCount = tweetCountElms[0].TextContent.Replace("件のツイート", ""); tweetCount = tweetCount.Replace(",", ""); tweetCount = tweetCount.Replace(" ", ""); // アカウント名を取得 var accountElms = doc.GetElementsByClassName("css-1dbjc4n r-6gpygo r-14gqq1x"); var nameElms = accountElms[0].GetElementsByClassName("css-901oao r-18jsvk2 r-1tl8opc r-1b6yd1w r-1vr29t4 r-ad9z0x r-bcqeeo r-qvutc0"); string name = nameElms[0].TextContent; var accountElms1 = accountElms[0].GetElementsByClassName("css-901oao css-bfa6kz r-m0bqgq r-18u37iz r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0"); string account = accountElms1[0].TextContent; return new AccountInfo(account, name, tweetCount, following, followers); } } |
実際に取得された情報はAccountInfoクラスに格納されます。10,000を超えると11,000なら「1.1万」と表示されるので数値に変換するためのプロパティを作成しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
public class AccountInfo { public AccountInfo(string account, string name, string tweetCount, string following, string followers) { Account = account; Name = name; _tweetCount = tweetCount; _following = following; _followers = followers; } string _tweetCount = ""; string _following = ""; string _followers = ""; public string Account { get; set; } public string Name { get; set; } public int TweetCount { get { try { string str = _tweetCount.Replace(",", ""); if (str.IndexOf("万") != -1) { str = str.Replace("万", ""); return (int)(double.Parse(str) * 10000); } return int.Parse(str); } catch { return 0; } } } public int Following { get { try { string str = _following.Replace(",", ""); if (str.IndexOf("万") != -1) { str = str.Replace("万", ""); return (int)(double.Parse(str) * 10000); } return int.Parse(str); } catch { return 0; } } } public int Followers { get { try { string str = _followers.Replace(",", ""); if (str.IndexOf("万") != -1) { str = str.Replace("万", ""); return (int)(double.Parse(str) * 10000); } return int.Parse(str); } catch { return 0; } } } } |
検索したい単語を入力してボタンをおすと処理が開始されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public partial class Form1 : Form { private void button1_Click(object sender, EventArgs e) { if (textBox1.Text == "") { MessageBox.Show("検索する文字列を入力してください"); return; } string excelPath = ""; SaveFileDialog dialog = new SaveFileDialog(); dialog.Filter = "Excelファイル(*.xlsx)|*.xlsx"; if (dialog.ShowDialog() == DialogResult.OK) excelPath = dialog.FileName; dialog.Dispose(); if (excelPath == "") return; string str = System.Web.HttpUtility.UrlEncode(textBox1.Text); Task.Run(()=> { driver1.Url = "https://twitter.com/search?q=" + str; List<AccountInfo> infos = GetAccountInfosFromSearchResult(10); SaveExcel(excelPath, infos); MessageBox.Show("終了しました"); }); } } |
以下は取得されたAccountInfoのリストをexcelファイルとして保存するためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
using ClosedXML.Excel; public partial class Form1 : Form { void SaveExcel(string excelPath, List<AccountInfo> infos) { using (var workbook = new XLWorkbook()) { var worksheet = workbook.Worksheets.Add("シート1"); worksheet.Cell(1, "A").Value = "アカウント名"; worksheet.Cell(1, "B").Value = "名前"; worksheet.Cell(1, "C").Value = "ツイート数"; worksheet.Cell(1, "D").Value = "フォロワー数"; int i = 2; foreach (AccountInfo info in infos) { worksheet.Cell(i, "A").Value = info.Account; worksheet.Cell(i, "B").Value = info.Name; worksheet.Cell(i, "C").Value = info.TweetCount; worksheet.Cell(i, "D").Value = info.Followers; i++; } workbook.SaveAs(excelPath); } } } |
最後に終了時の処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Form1 : Form { protected override void OnClosed(EventArgs e) { // DisposeDrivers()メソッドが終了するまで時間がかかる。 // そのあいだウィンドウが表示されたままになるので // this.Visible = false;を実行する this.Visible = false; DisposeDrivers(); base.OnClosed(e); } void DisposeDrivers() { driver1.Quit(); driver1.Dispose(); driver2.Quit(); driver2.Dispose(); } } |