Contents
年ごとにわけてチャンネルの全動画のurlを取得する
前回作成したアプリはアップロード件数が多いと取得することができませんでした。そこでYouTubeにはチャンネル内の検索機能があること、検索するときに「after:2020-01-01 before:2020-06-30」とやれば動画がアップロードされた時期を限定して検索することができることを利用して、期間を区切って動画urlを取得します。1年単位であればそんなに大量の動画をアップすることができないので、なんとかなりそうです。
これは私のチャンネルをafter:2021-01-01 before:2021-08-31で検索したときのurlです。
https://www.youtube.com/channel/UCp5XB6e_VOKA0q6KENw-hlQ/search?query=after%3A2021-01-01%20before%3A2021-08-31
YouTubeは設立が2005年2月ですから
https://www.youtube.com/channel/XXXXXX/search?query=after%3A2005-01-01%20before%3A2005-12-31
https://www.youtube.com/channel/XXXXXX/search?query=after%3A2006-01-01%20before%3A2006-12-31
https://www.youtube.com/channel/XXXXXX/search?query=after%3A2007-01-01%20before%3A2007-12-31
とやっていけば全部取得できそうです。
チャンネルのurlには複数のタイプがある
ところがそんな簡単な問題ではないようです。チャンネルのUrlがhttps://www.youtube.com/channel/XXXXXX/ではなく別のタイプのものがあります。それでもチャンネルのUrlの最後に/search?query=after%3A2019-01-01%20before%3A2019-12-31をつければ検索結果は得られます。それではやっていきましょう。
調査対象としてテキストボックスに入力するのはチャンネルのurlです。このあとに/search?query=after%3A20XX-01-01%20before%3A20XX-12-31を追加して結果を調べさせます。
XPathで動画urlをゲット?
取得する要素を指定する場合はXPathを使います。HTMLを調べた結果、前回の記事同様、
1 2 |
//*[@id="video-title"] // 前回と同じ |
でよいことがわかりました。
それと同時に前回の方法ではXPathを指定することで取得された要素から
前回との相違点について
1 2 3 4 5 6 7 |
// アンカーテキストを取得する JSHandle jsHandle1 = await elementHandle.GetPropertyAsync("textContent"); string videoTitle = jsHandle1.ToString().Substring(9); // リンクを取得する JSHandle jsHandle2 = await elementHandle.GetPropertyAsync("href"); string videoUrl = jsHandle2.ToString().Substring(9); |
とすればよかったのですが、今回はその方法ではリンクは取得できるのですが、アンカーテキストは取得できません。よくよくHTMLソースを調べてみると以下のようになっていました。でもこれならGetPropertyAsync(“textContent”)ではなく、GetPropertyAsync(“title”)とすれば動画タイトルは取得できますよね。
1 2 3 4 5 6 |
<a id="video-title" class="yt-simple-endpoint style-scope ytd-video-renderer" title="動画のタイトル" href="動画へのリンク" aria-label="動画のタイトル 作成者: チャンネル名 どのくらい前にアップされたか? 動画の再生時間 動画の視聴回数"> <yt-icon id="inline-title-icon" class="style-scope ytd-video-renderer" hidden=""></yt-icon> <yt-formatted-string class="style-scope ytd-video-renderer" aria-label="動画のタイトル 作成者: チャンネル名 どのくらい前にアップされたか? 動画の再生時間 動画の視聴回数"> 動画のタイトル </yt-formatted-string> </a> |
あとはコードを書くだけです。
コードを書く
前回のプログラムではForm1のコンストラクタのなかでヘッドレスブラウザを起動させていましたが、この処理は前回と完全に同じなので省略します。あと取得されたデータをExcelファイルとして保存するところも完全に同じなので詳しくは前回の記事を参照してください。
UIの変更
前回の記事と異なる部分を示します。といってもほとんど同じです。ひとつのチャンネルから情報を得るために複数の検索処理をしなければならないので、プログレスバーをもうひとつ追加しました。
以前のプログレスバーにはその年ごとの取得した要素から動画タイトルと動画urlを取得する処理がどれだけ完了したか、ふたつめのプログレスバーには2005年にYouTubeが発足してから今年までのうち、どれだけ情報を取得できたかを示しています。
時間がかかる処理はどれだけ処理が進んでいるのかをプログレスバーなどをつかって表示したほうが、使う側もどれくらい待てばいいかわかるので新設設計というものです。
変更点はちょっとだけ
それでは検索結果を取得するためのボタンを押したときの処理を示します。
変更点は少ししかありません。大きな変更といえば、チャンネルurlから検索結果を表示させるurlを取得している部分です。それ以外の変更はプログレスバーをふたつにしたのと取得されるのは動画のなかに再生リストが混在するのでそれを取り除く処理をしているだけです。
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 |
public partial class Form1 : Form { int TotalVideoCount = 0; private async void GetVideoUrlButton_Click(object sender, EventArgs e) { TotalVideoCount = 0; string channelUrl = textBox1.Text; if (channelUrl == "") { MessageBox.Show("Urlを入力してください"); return; } string excelFilePath = GetSaveExelFilePath(); if (excelFilePath == "") return; startButton.Enabled = false; List<Data> datas = new List<Data>(); // 2005年から今年まで1年単位でデータを取得する DateTime now = DateTime.Now; // プログレスバーを初期化する Invoke((Action)(() => { progressBar2.Maximum = now.Year - 2005 + 1; progressBar2.Value = 0; })); for (int year = 2005; year <= now.Year; year++) { // 現在、何年分の情報を取得しているか表示する Invoke((Action)(() => { label1.Text = year + " 年の情報を取得中"; })); // チャンネルurlを利用してチャンネル内の各年の動画を検索する string channelUrlSearch = channelUrl + String.Format("/search?query=after%3A{0}-01-01%20before%3A{0}-12-31", year); channelUrlSearch = channelUrlSearch.Replace("//search", "/search"); ElementHandle[] elements = await GetElementsWithScroll(channelUrlSearch); List<Data> datas0 = await GetDataFromElements(elements); datas.AddRange(datas0); Invoke((Action)(() => { if(progressBar2.Maximum >= year - 2005 + 1) progressBar2.Value = year - 2005 + 1; })); } // あとは保存するだけといきたいところだが、取得されるのは動画と再生リストが混在している。 // HTMLをみてみるとこの方法では再生リストはurlが取得できないので動画urlが存在しないものは再生リストとして考える // "https://"は動画urlが取得されているなら必ず見つかる文字列 datas = datas.Where(x => x.VideoUrl.IndexOf("https://") != -1).ToList(); SaveExcel(excelFilePath, datas); MessageBox.Show("完了しました"); startButton.Enabled = true; return; } } |
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 |
public partial class Form1 : Form { private async Task<ElementHandle[]> GetElementsWithScroll(string channelUrlSearch) { // キャンセルボタンがおされたらtrueになる isStop = false; await page1.GoToAsync(channelUrlSearch); await System.Threading.Tasks.Task.Delay(5000); Invoke((Action)(() => { progressBar1.Maximum = 100; progressBar1.Value = 0; })); int i = 0; int oldCount = 0; ElementHandle[] elements = null; // 一気に下までスクロール while (true) { try { i++; await page1.Mouse.WheelAsync(0, 2000 * i); await Task.Delay(2000); // XPathで指定された要素を取得する elements = await page1.XPathAsync("//*[@id=\"video-title\"]"); Invoke((Action)(() => { // XPathで指定された要素で現段階で取得された数を表示する labelStatus.Text = elements.Length.ToString() + "件取得"; })); // もしキャンセルボタンがおされていたらループを抜ける if (isStop) break; if (oldCount == elements.Length) break; oldCount = elements.Length; } catch { // 例外発生のときはループを抜ける MessageBox.Show("例外発生!"); break; } } return elements; } } |
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 |
public partial class Form1 : Form { async Task<List<Data>> GetDataFromElements(ElementHandle[] elements) { // プログレスバーを初期化する Invoke((Action)(() => { progressBar1.Maximum = elements.Length; progressBar1.Value = 0; })); // 返却するデータのリストを作成する List<Data> datas = new List<Data>(); // ループは何回まわったか? int i = 0; foreach (ElementHandle elementHandle in elements) { // アンカーテキストを取得する JSHandle jsHandle1 = await elementHandle.GetPropertyAsync("title"); string videoTitle = jsHandle1.ToString().Substring(9); // リンクを取得する JSHandle jsHandle2 = await elementHandle.GetPropertyAsync("href"); string videoUrl = jsHandle2.ToString().Substring(9); // Dataオブジェクトを生成してデータを格納し、リストに追加する Data data = new Data() { VideoTitle = videoTitle, VideoUrl = videoUrl }; datas.Add(data); // プログレスバーのValueを増やして処理が進行している様子を可視化する Invoke((Action)(() => { progressBar1.Value = i; i++; TotalVideoCount++; label2.Text = "取得総数:" + TotalVideoCount.ToString(); })); } // 終わったらプログレスバーを満タンの状態にする progressBar1.Value = progressBar1.Maximum; return datas; } } |