本篇以 C# 程式碼說明如何搜尋 Youtube 歌曲及下載 mp3/mp4 格式檔
爬蟲程式
爬蟲程式並不是 Python 的專利,其實 Java, C# 等程式語言都支援。使用 C# 有個方便之處,就是可以直接編譯成 .exe 檔,讓使用者按二下就可以直接執行。這可比 Python 方便多了。
安裝Selenium 套件
請進入 “工具/NuGet套件管理員/管理方案的NuGet套件”,然後在瀏覽頁籤中搜尋 Selenium,安裝 Selenium.WebDriver 及 DotNetSeleniumExtras.WaitHelpers 二個套件。
請注意 Selenium.Support 已被淘汰掉了,改用 DotNetSeleniumExtras.WaitHelpers。
下載 WebDriver.exe
網路上的教學都說需先下載 chromedriver.exe檔,但本人測試後是不需要安裝 chromedriver 的。因為 Selenium.WebDriver 套件會自動下載與 Chrome 相同版本的 chromedriver.exe,如此也省去了 Chrome 瀏覽器版本更新的問題。
開啟Browser
開啟 Browser 會花費蠻久的時間(1~2秒),所以必需將開啟 browser 的功能寫在新的執行緒上。本例使用 async Task 來執行。但建構子 MainWindow 是不淮加入 async 的,所以必需把開啟 browser 寫在 Window_Loaded 方法中,如下代碼
public MainWindow() { InitializeComponent(); disableUI(); } private async void Window_Loaded(object sender, RoutedEventArgs e) { lblPath.Content = path.Replace("_", "__"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } //底下的版本取得方式,會在發佈後無法使用,造成程式閃退 //lblVersion.Content= string.Format("Version : {0}", // FileVersionInfo.GetVersionInfo( // Assembly.GetExecutingAssembly().Location // ).FileVersion.ToString()); //底下才是取得版本的正確方式 lblVersion.Content = string.Format( "Version : {0}", Assembly.GetExecutingAssembly().GetName().Version.ToString() ); await Browser(); enableUI(); } private async Task Browser() { await Task.Run(() => { ChromeOptions options = new ChromeOptions(); ChromeDriverService service = ChromeDriverService.CreateDefaultService(); service.SuppressInitialDiagnosticInformation = true; service.EnableVerboseLogging = false; service.HideCommandPromptWindow = true; options.AddArgument("--disable-logging"); options.AddArgument("--output=/dev/null"); options.AddArgument("--headless"); browser = new ChromeDriver(service, options); }); }
搜尋歌曲
搜尋歌曲需使用 Selenium 操作 browser, 代碼如下
private async void btnSearch_Click(object sender, RoutedEventArgs e) { lblStatus.Content = "Searching...."; disableUI(); lsSong.Items.Clear(); String url = string.Format( "https://www.youtube.com/results?search_query={0}", txtSong.Text ); await Search(url); enableUI(); lblStatus.Content = ""; foreach (var item in dic) { StackPanel panel = new StackPanel(); panel.Orientation = Orientation.Horizontal; panel.Children.Add(new CheckBox()); TextBox txt = new TextBox(); txt.Text = item.Value; panel.Children.Add(txt); lsSong.Items.Add(panel); } } private async Task Search(string url) { await Task.Run(() => { dic.Clear(); browser.Navigate().GoToUrl(url); WebDriverWait wait = new WebDriverWait( browser, TimeSpan.FromSeconds(10) ); var element = wait.Until( SeleniumExtras.WaitHelpers.ExpectedConditions.ElementIsVisible( By.Id("video-title") ) ); var tags = browser.FindElements(By.TagName("a")); foreach (var tag in tags) { string href = tag.GetAttribute("href"); var title = tag.GetAttribute("title"); if (title != "" && href != null && href.Contains("watch") && !dic.ContainsKey(href)) { dic.Add(href, string.Format("{0} url={1}", title, href)); } } }); }
YoutubeExplode
C# 支援 YouTube 影片下載的套件不多,而且大都不能用了,比如 VideoLibrary 下載高清影片時沒有聲音,sharpGrabber 則無法使用。
本人測試目前最好用的套件,就屬 YoutubeExplode 這個套件,請由 NuGet 搜尋並安裝。
官方文件說明網址如下 : https://github.com/Tyrrrz/YoutubeExplode
下載 影片/音樂 的代碼如下
private async void btnDownload_Click(object sender, RoutedEventArgs e) { List titles = new List(); for (int i = 0; i < lsSong.Items.Count; i++) { var item = lsSong.Items[i] as StackPanel; var chk = item.Children[0] as CheckBox; var txt = item.Children[1] as TextBox; if (chk.IsChecked is true) { titles.Add(txt.Text); } } var style = 0; if (rbMp3.IsChecked == true) style = 0; else style = 1; foreach (var title in titles) { var ts = title.Split(" url="); var file = ts[0]; var url = ts[1]; lblStatus.Content = string.Format("Download {0}...", file); await Download(url, style, path); } lblStatus.Content = string.Format("Download suscessful"); } private async Task Download(string url, int style, string path) { await Task.Run(async () => { var youtube = new YoutubeClient(); var video = await youtube.Videos.GetAsync(url); var author = video.Author.ChannelTitle; var streamManifest = await youtube.Videos.Streams.GetManifestAsync(url); var title = video.Title .Replace("\\", "") .Replace("/", "") .Replace(":", "") .Replace("*", "") .Replace("?", "") .Replace("\"", "") .Replace("<", "") .Replace(">", "") .Replace("|", "") .Replace(" ", ""); IStreamInfo streamInfo=null; if (style == 0) { streamInfo = streamManifest.GetAudioOnlyStreams() .GetWithHighestBitrate(); } else { streamInfo = streamManifest.GetMuxedStreams() .GetWithHighestVideoQuality(); } //var stream = await youtube.Videos.Streams.GetAsync(streamInfo); await youtube.Videos.Streams.DownloadAsync( streamInfo, $"{path}\\{title}.{streamInfo.Container}" ); }); }
UI 版面設計
UI 的畫面設計如下代碼
<Window x:Class="Youtube.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Youtube"
mc:Ignorable="d"
Loaded="Window_Loaded"
Title="MahalYT" Height="450" Width="800" WindowState="Maximized">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition/>
<RowDefinition Height="28"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Background="#e0e0e0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="300"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Name="lblVersion"/>
<Grid Grid.Column="1" Background="#a0a0ff">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Song"/>
<TextBox Grid.Column="1" VerticalContentAlignment="Center" Name="txtSong"/>
<Button Grid.Column="2" Content="Search" Name="btnSearch" Click="btnSearch_Click"/>
</Grid>
<Label Grid.Column="2" Content="Author : Thomas (mahaljsp@gamil.com)" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid>
<ListBox Grid.Row="1" x:Name="lsSong"/>
<Grid Grid.Row="2" Margin="0,2,0,1" Background="#E0E0E0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" Background="#80ff80" >
<RadioButton GroupName="rbStyle" Name="rbMp3" Margin="10,0,10,0">mp3</RadioButton>
<RadioButton GroupName="rbStyle" IsChecked="True" Name="rbMp4" Margin="10,0,10,0">mp4</RadioButton>
<Button Name="btnDownload" Width="80" Margin="20,0,0,0" Click="btnDownload_Click" >Download</Button>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Background="#f0f0f0">
<Label Width="200" Name="lblPath"/>
<Button Name="btnPath" Width="100" Click="btnPath_Click">Path</Button>
</StackPanel>
</Grid>
<Grid Grid.Row="3" Margin="0,1,0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
<ColumnDefinition Width="84"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="URL" Background="#E0E0E0"/>
<TextBox Grid.Column="1" Text="" TextWrapping="Wrap" VerticalContentAlignment="Center" Name="txtUrl"/>
<Button Grid.Column="2" Content="URL Download" Name="btnUrlDownload" Click="btnUrlDownload_Click" Grid.ColumnSpan="2"/>
</Grid>
<Label Grid.Row="4" Name="lblStatus" Background="#d5d5d5" HorizontalContentAlignment="Center" />
</Grid>
</Window>
使用者控制項
顯示在 ScrollViewer 裏的 item 蠻複雜的,所以就請使用者控制項先制定其樣式,再由主程式產生 item 並加入 ScrollViewer 中。
請在專案 Youtube 處按右鍵/加入/新增項目/使用者控制項(WPF),名稱鍵入 SearchItem。
SearchItem.xaml 詳細內容如下
<UserControl x:Class="Youtube.SearchItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Youtube"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border BorderBrush="Black" BorderThickness="0.5" Height="Auto" Margin="2,1,2,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Name="item" Background="#c0c0c0" VerticalContentAlignment="Center" />
<CheckBox Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Name="chk"/>
<StackPanel Grid.Column="2" VerticalAlignment="Center" Orientation="Vertical" HorizontalAlignment="Stretch">
<TextBox Name="txtTitle" Background="#f9f9f9" IsReadOnly="True" Height="30" VerticalContentAlignment="Center" FontSize="14" BorderThickness="0"/>
<TextBox Name="txtUrl" Background="#eeeeee" IsReadOnly="True" Height="30" VerticalContentAlignment="Center" FontSize="14" BorderThickness="0"/>
</StackPanel>
</Grid>
</Border>
</UserControl>
完整程式碼
底下是本範例的完整程式碼
using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using YoutubeExplode; using YoutubeExplode.Videos.Streams; namespace Youtube { ///<summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { IWebDriver browser; Dictionary<String, String> dicts = new Dictionary<String, String>(); string path = "c:\\youtube_download"; public MainWindow() { InitializeComponent(); disableUI(); } private async void Window_Loaded(object sender, RoutedEventArgs e) { //txtStatus.Text=$"MahalYT Install Path : {AppDomain.CurrentDomain.BaseDirectory}"; if (File.Exists("previous.json")) { var jsonString = File.ReadAllText("previous.json"); PathInfo info = JsonSerializer.Deserialize<PathInfo>(jsonString); path = info.infoPath; } if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } lblPath.Content = path.Replace("_", "__"); //底下的版本取得方式,會在發佈後無法使用,造成程式閃退 //lblVersion.Content= string.Format("Version : {0}", // FileVersionInfo.GetVersionInfo( // Assembly.GetExecutingAssembly().Location // ).FileVersion.ToString()); //底下才是取得版本的正確方式 lblVersion.Content = string.Format( "Version : {0}", Assembly.GetExecutingAssembly().GetName().Version.ToString() ); await Browser(); enableUI(); } private async Task Browser() { await Task.Run(() => { ChromeOptions options = new ChromeOptions(); ChromeDriverService service = ChromeDriverService.CreateDefaultService(); service.SuppressInitialDiagnosticInformation = true; service.EnableVerboseLogging = false; service.HideCommandPromptWindow = true; options.AddArgument("--disable-logging"); options.AddArgument("--output=/dev/null"); options.AddArgument("--headless"); browser = new ChromeDriver(service, options); }); } private async void btnSearch_Click(object sender, RoutedEventArgs e) { if (txtSong.Text == "") { MessageBox.Show("Please input Song/Singer", "MahalYT", MessageBoxButton.OK, MessageBoxImage.Warning); return; } txtStatus.Text = "Searching...."; disableUI(); //lsSong.Items.Clear(); lsSong.Children.Clear(); String url = string.Format( "https://www.youtube.com/results?search_query={0}", txtSong.Text ); chAll.IsChecked = false; chAll.IsEnabled = false; await Search(url); enableUI(); txtStatus.Text = ""; var index = 0; foreach (var dict in dicts) { var values = dict.Value.Split(" url="); SearchItem item = new SearchItem(); item.item.Content = index + 1; item.txtTitle.Text = values[0]; item.txtUrl.Text = values[1]; lsSong.Children.Add(item); index++; } if (lsSong.Children.Count > 0) chAll.IsEnabled = true; } private async Task Search(string url) { await Task.Run(() => { dicts.Clear(); browser.Navigate().GoToUrl(url); WebDriverWait wait = new WebDriverWait( browser, TimeSpan.FromSeconds(10) ); var element = wait.Until( SeleniumExtras.WaitHelpers.ExpectedConditions.ElementIsVisible( By.Id("video-title") ) ); var tags = browser.FindElements(By.TagName("a")); foreach (var tag in tags) { string href = tag.GetAttribute("href"); var title = tag.GetAttribute("title"); if (title != "" && href != null && href.Contains("watch") && !dicts.ContainsKey(href)) { dicts.Add(href, string.Format("{0} url={1}", title, href)); } } }); } private async void btnDownload_Click(object sender, RoutedEventArgs e) { if (lsSong.Children.Count == 0) { MessageBox.Show("Please Search Song/Singer first", "MahalYT", MessageBoxButton.OK, MessageBoxImage.Warning); return; } int style = 1; if (rbMp3.IsChecked == true) style = 0; chAll.IsEnabled = false; disableUI(); int downloadCount = 0; foreach (SearchItem item in lsSong.Children) { if (item.chk.IsChecked is true) { txtStatus.Text = string.Format("Download {0}...", item.txtTitle.Text); await Download(item.txtUrl.Text, style, path); downloadCount++; } } if (downloadCount > 0) txtStatus.Text = string.Format("Download suscessful"); enableUI(); chAll.IsEnabled = true; } private async Task Download(string url, int style, string path) { await Task.Run(async () => { var youtube = new YoutubeClient(); var video = await youtube.Videos.GetAsync(url); var author = video.Author.ChannelTitle; var streamManifest = await youtube.Videos.Streams.GetManifestAsync(url); var title = video.Title .Replace("\\", "") .Replace("/", "") .Replace(":", "") .Replace("*", "") .Replace("?", "") .Replace("\"", "") .Replace("<", "") .Replace(">", "") .Replace("|", "") .Replace(" ", ""); IStreamInfo streamInfo = null; if (style == 0) { streamInfo = streamManifest.GetAudioOnlyStreams() .GetWithHighestBitrate(); } else { streamInfo = streamManifest.GetMuxedStreams() .GetWithHighestVideoQuality(); } //var stream = await youtube.Videos.Streams.GetAsync(streamInfo); await youtube.Videos.Streams.DownloadAsync( streamInfo, $"{path}\\{title}.{streamInfo.Container}" ); }); } private async void btnUrlDownload_Click(object sender, RoutedEventArgs e) { if (txtUrl.Text == "") { MessageBox.Show("Please input URL", "MahalYT", MessageBoxButton.OK, MessageBoxImage.Warning); return; } try { txtStatus.Text = string.Format("Download {0}...", txtUrl.Text); int style = 0; if (rbMp3.IsChecked == true) style = 0; else style = 1; await Download(txtUrl.Text, style, path); txtStatus.Text = string.Format("Download suscessful"); } catch { MessageBox.Show("YouTube address error", "Download Fail", MessageBoxButton.OK, MessageBoxImage.Error); } txtUrl.Text = ""; } private void btnPath_Click(object sender, RoutedEventArgs e) { using (var dialog = new System.Windows.Forms.FolderBrowserDialog()) { if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { path = dialog.SelectedPath; var info = new PathInfo { infoPath = path }; File.WriteAllText("previous.json", JsonSerializer.Serialize(info)); lblPath.Content = path.Replace("_", "__"); } } } private void disableUI() { btnDownload.IsEnabled = false; btnPath.IsEnabled = false; btnSearch.IsEnabled = false; btnUrlDownload.IsEnabled = false; } private void enableUI() { btnDownload.IsEnabled = true; btnPath.IsEnabled = true; btnSearch.IsEnabled = true; btnUrlDownload.IsEnabled = true; } private void chAll_Click(object sender, RoutedEventArgs e) { var chk = false; if (chAll.IsChecked == true) chk = true; foreach (SearchItem item in lsSong.Children) { item.chk.IsChecked = chk; } } } class PathInfo { public string infoPath { get; set; } } }
Icon 注意事項
Icon 圖示是這個專案的代表,此 Icon 也會顯示在桌面的快捷鍵上。
請由右方方案總管的專案按右鍵/屬性/應用程式,在圖示選擇 .ico 檔,此檔會被 copy 到專案之下。然後記得要在方案總管的 .icon 檔點一下,下方檔案屬性 “複製到輸出目錄” 要選擇 “永遠複製”。如果沒有作此步驟,使用者在執行 setup.exe時,會出現 “缺少需要的套件” 之類的錯誤。
發佈專案
專案完成後,就是要發佈安裝檔給人下載安裝並執行,如何發佈,有許多要注意的事項。
請從建置/發佈/ClickOnce開始,發佈位置使用預設的 bin\publish。安裝位置請選擇從網站,指定 URL 如http://mahaljsp.asuscomm.com/project/mahalyt。
設定/選項/資訊清單,請勾選 “建立桌面捷徑”。然後也是在設定/更新設定/更新位置輸入上述的 URL : http://mahaljsp.asuscomm.com/project/mahalyt,指定此應用程式所需的最低版本請輸入比發佈版本少一號。
然後在最後的設定/目標執行階段,選擇win-x64,然後檔案發行選項要勾選產生單一檔案。如果產生單一檔案沒有勾,則安裝後是無法執行的。
最後再按發佈,就會在專案下的 bin下產生 publish 目錄。再使用 filezilla 將 publish 下的所有檔案目錄都 copy 到網站的 \project\mahalyt 下。
執行檔下載
本範例已編譯成執行檔,在執行此應用程式前,需先下載 .net 5.0 sdk framework,請到微軟官網下載並安裝
https://dotnet.microsoft.com/zh-cn/download/dotnet/5.0
然後請到本站 http://mahaljsp.asuscomm.com/project/mahalyt/ 下載並執行 setup.exe 安裝檔。
下載時瀏覽器會警告此程式 “可能不安全”,請按 “保留”。請放心,本程式絕對沒有病毒,瀏覽器只要是下載 .exe 檔都會提出這樣的警告。
安裝時,請選擇 “其他資訊”,再選擇 “仍要執行”
安裝完就會自動開啟本程式,桌面上也會自動產生快捷鍵 “MahalYT”。