チャットワークのメッセージをSeleniumで取得するプログラムをつくります。まずログインしないといけないのですが、Seleniumだとなかなか画像認証がとおりません。そこで以下の方法をとります。
最初は普通にアクセスしてログインする
セッション情報を保持しておいてログイン状態を維持する
これだと次回Seleniumでアクセスしてもログインされた状態でアクセスできます。
あとはHTMLの解析だけです。
通常のメッセージは以下のようになっています。
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 |
<div id="_messageId1312378079217209344" class="_message sc-fONwsr WrddL" data-rid="190330574" data-mid="1312378079217209344" data-deleted="0"> <div class="sc-ipXKqB cqZxZd"> <div class="sc-hwwEjo _speaker khZkSh"> <div class="_avatarHoverTip _avatarClickTip sc-gzOgki bItvSJ" data-aid="2678138"><img src="https://appdata.chatwork.com/avatar/ico_default_orange.png" alt="発言者名" class="sc-iyvyFf bUHyuu"> </div> </div> <div class="sc-hmXxxW hsWqGy"> <div class="sc-jtRfpW caKNTc"> <p class="sc-kTUwUJ _speakerName cppzHn">発言者名</p> <p class="sc-gxMtzJ eeIuYx"></p> </div> <pre class="sc-btzYZH hcMuxa"> <div contenteditable="false" data-cwtag="[To:4812170]" class="sc-hmzhuo byLGUC"> <div class="chatTimeLineTo sc-ktHwxA bBgBqK"> <svg viewBox="0 0 10 10" width="14" height="14" class="sc-cIShpX gaakfv"> <use fill-rule="evenodd" xlink:href="#icon_chatTimeLineTo"></use> </svg> </div> <img alt="" src="https://appdata.chatwork.com/avatar/ico_default_orange.png" data-aid="4812170" class="sc-TOsTZ dEnuMG _avatarHoverTip _avatarClickTip avatarClickTip _avatarAid4812170"> </div> メッセージ本文がここに入ります。 メッセージに改行があっても改行タグはありません。 </pre> </div> <div class="_timeStamp sc-cqCuEk ecMjzx" data-tm="1589368916"> 2020年5月13日 20:21 </div> </div> </div> |
もしファイルも一緒に送信するとpreタグのなかがこのようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<pre class="sc-btzYZH hcMuxa"> メッセージ本文がここに入ります。 メッセージに改行があっても改行タグはありません。 <div data-cwopen="[info]" data-cwclose="[/info]" class="chatInfo sc-fBuWsC kGGSff"> <div data-cwopen="[title]" data-cwclose="[/title]" class="sc-fMiknA EZNNc"> <div class="sc-dVhcbM hGnmfY"> <span class="sc-bdVaJa vKQHu"> <svg viewBox="0 0 10 10" width="16" height="16" class="sc-bwzfXH jhCIfQ" aria-hidden="true"> <use fill-rule="evenodd" xlink:href="#icon_info"></use> </svg> </span> </div> <span data-cwtag="[dtext:file_uploaded]">ファイルをアップロードしました。</span> </div> <div data-cwopen="[download:593633889]" data-cwclose="[/download]"> <a href="gateway/download_file.php?bin=1&file_id=593633889&preview=0" tabindex="-1" target="_downloadFrame" rel="noopener">ファイル名.xlsx (33.33 KB)</a> </div> </div> </pre> |
そこで発言者と発言内容、添付ファイルがあるならこれも取得するというのであれば以下のようになります。
class=”_message”を取得。そのなかのclass=”_speakerName”を調べれば発言者がわかります。preを調べれば発言内容もわかります。pre内にa タグが存在し、その href属性に gateway/download_file.php? が含まれ、target属性が”_downloadFrame”であればそれが添付ファイルです。添付ファイルのurlは https://www.chatwork.com/ + gateway/download_file.php?~ です。
発言時刻ですが、同じ日に複数回メッセージを送ると日付が省略されています。
1 2 |
<div class="_timeStamp sc-cqCuEk ecMjzx" data-tm="1601347143">2020年9月29日 11:39</div> <div class="_timeStamp sc-cqCuEk ecMjzx" data-tm="1601347374">11:42</div> |
しかしdata-tm=の部分を調べればここから日付も含めて時刻がわかります。この部分には1970年1月1日0字0分0秒からの経過秒が書かれていることがわかります。日本時間と世界標準時とのあいだに9時間のズレがあります。
data-tm属性を取得して、
1 2 3 4 |
double d = double.Parse(datatm); // datatm は data-tm属性の文字列 DateTime date = new DateTime(1970, 1, 1); date = date.AddHours(9); timeStamp = date.AddSeconds(d).ToString("yyyy年MM月dd日 HH時mm分ss秒"); |
これで取得できます。
それから同じ人が連続してメッセージを送ったときに発言者は取得できません。このときは前の人と同じとして処理をおこないます。
でははじめましょう。デザイナで以下のようなものを作ります。
そしてNuGetでSelenium.WebDriverとSelenium.Support、そして操作したいブラウザのDriverをインストールします。今回はChromeを操作するのでSelenium.Chrome.WebDriverをインストールします。それからHTMLの解析でAngleSharp、Excelファイルを操作するためにClosedXMLを使います。AngleSharpとClosedXMLもNuGetでインストールしておいてください。
アプリが開始されるとChromeが起動します。事前にChromeでChatWorkにログインしておけば自動的にログインされます。
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 |
using OpenQA.Selenium.Chrome; public partial class Form1 : Form { ChromeDriver driver = null; public Form1() { InitializeComponent(); StartSeleniumChrome(); } void StartSeleniumChrome() { ChromeDriverService driverService = ChromeDriverService.CreateDefaultService(); driverService.HideCommandPromptWindow = true; // 保存されているセッション情報でログイン状態を維持できるようにする ChromeOptions options = new ChromeOptions(); string PROFILE_PATH = @"C:\Users\kj\AppData\Local\Google\Chrome\User Data"; options.AddArgument("user-data-dir=" + PROFILE_PATH); driver = new ChromeDriver(driverService, options); driver.Url = "https://www.chatwork.com/login.php"; } } |
ボタンが押されたらファイルを保存する場所を選択させ、ChatWorkのHTMLを解析してデータを取得し、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 |
public partial class Form1 : Form { private void button1_Click(object sender, EventArgs e) { progressBar1.Value = 0; progressBar2.Value = 0; SaveFileDialog dialog = new SaveFileDialog(); dialog.Filter = "Excelファイル(*.xlsx)|*.xlsx"; DialogResult dr = dialog.ShowDialog(); if (dr != DialogResult.OK) { dialog.Dispose(); return; } string excelFilePath = dialog.FileName; dialog.Dispose(); List<Data> datas = GetMessageList((int)numericUpDown1.Value); if (datas.Count == 0) return; SaveExcelFile(excelFilePath, datas); } } |
ChatWorkのHTMLを解析してデータを取得するGetMessageList(int max)メソッドを示します。HTMLの解析にAngleSharpを使用しています。
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
using AngleSharp.Dom; using AngleSharp.Html.Parser; using AngleSharp.Html.Dom; public class Data { public string SpeakerName = ""; public string Message = ""; public string TimeStamp = ""; public List<string> FileUrls = null; } public partial class Form1 : Form { List<Data> GetMessageList(int max) { progressBar1.Maximum = max; List<Data> Datas = new List<Data>(); List<string> ids = new List<string>(); string preSpeaker = ""; while (true) { string html = driver.PageSource; HtmlParser parser = new HtmlParser(); IHtmlDocument doc = parser.ParseDocument(html); var messageElms = doc.GetElementsByClassName("_message"); List<Data> datas = new List<Data>(); foreach (var messageElm in messageElms) { if (ids.Any(x => x == messageElm.Id)) continue; ids.Add(messageElm.Id); string speakerName = ""; var speakerNameElms = messageElm.GetElementsByClassName("_speakerName"); if (speakerNameElms != null && speakerNameElms.Count() > 0) speakerName = speakerNameElms[0].TextContent; if (speakerName == "") speakerName = preSpeaker; preSpeaker = speakerName; string message = ""; List<string> fileUrls = new List<string>(); var messagePre = messageElm.GetElementsByTagName("pre"); if (messagePre != null && messagePre.Count() > 0) { message = messagePre[0].TextContent; var aElms = messagePre[0].GetElementsByTagName("a"); foreach (IElement elm in aElms) { string href = elm.GetAttribute("href"); if (href == null) continue; if (href.IndexOf("gateway/download_file.php?") != -1) { string fileUrl = "https://www.chatwork.com/" + href; fileUrl = fileUrl.Replace("&", "&"); fileUrl = fileUrl.Replace("&preview=0", ""); fileUrls.Add(fileUrl); } } } string timeStamp = ""; var timeStampElms = messageElm.GetElementsByClassName("_timeStamp"); if (timeStampElms != null && timeStampElms.Count() > 0) { string datatm = timeStampElms[0].GetAttribute("data-tm"); if (datatm != null) { try { double d = double.Parse(datatm); DateTime date = new DateTime(1970, 1, 1); date = date.AddHours(9); timeStamp = date.AddSeconds(d).ToString("yyyy年MM月dd日 HH時mm分ss秒"); } catch { } } } if (message == "") continue; Data data = new Data() { SpeakerName = speakerName, Message = message, TimeStamp = timeStamp, FileUrls = fileUrls, }; datas.Add(data); // プログレスバーで進行状況がわかるようにする progressBar1.Value++; if (Datas.Count + datas.Count >= max) { Datas.InsertRange(0, datas); return Datas; } } Datas.InsertRange(0, datas); var messageElms1 = driver.FindElementsByClassName("_message"); int firstY = messageElms1.First().Location.Y; // 一番最初の div class = "_message" の座標がマイナスであればまだ上にスクロールさせなければならない // プラスの値なら終了 if (firstY > 0) { progressBar1.Value = max; return Datas; } // スクロールさせるときウィンドウをスクロールさせるのではなく、 // div class = "sc-gHboQg"をスクロールさせる string script = "let elm = document.getElementsByClassName(\"sc-gHboQg\")[0];elm.scrollTop = -10;"; driver.ExecuteScript(script); // スリープしているときはプログレスバーを動かす progressBar2.Value = 0; progressBar2.Maximum = 30; for (int i = 0; i < 30; i++) { System.Threading.Thread.Sleep(100); progressBar2.Value++; } } } } |
データを取得できたらExcelファイルとして保存します。ClosedXMLを使用しています。
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 |
using ClosedXML.Excel; public partial class Form1 : Form { void SaveExcelFile(string filePath, List<Data> datas) { 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 (Data data in datas) { worksheet.Cell(i, "A").Value = data.TimeStamp; worksheet.Cell(i, "B").Value = data.SpeakerName; worksheet.Cell(i, "C").Value = data.Message; string str = ""; foreach (string fileUrl in data.FileUrls) str += fileUrl + "\n"; worksheet.Cell(i, "D").Value = str; i++; } workbook.SaveAs(filePath); } } } |
終了するときにChromeDriverをdisposeする必要があります。これをわすれるとChromeDriverはいつまで立っても終了しません。なにげなくタスクマネージャーをみてみると大変なことになっていました。
ChromeDriverをDisposeするときに時間がかかります。そのあいだFormは閉じないのでユーザーとしては「あれっ」と思うことになります。そこでthis.Visible = false;のあとChromeDriverをDisposeする処理をおこなっています。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { protected override void OnClosed(EventArgs e) { this.Visible = false; driver.Quit(); driver.Dispose(); base.OnClosed(e); } } |