资源下载的终极利器-资源轻松简单下载-资源万能下载法
layout: post
category: “program”
title: “資源下載的終極利器-資源輕松簡單下載-資源萬能下載法”
tags: [c++]
- Content
{:toc}
緣起
? 經常會有朋友問起:某FM的音頻文件怎么下載?某網站音效素材mp3怎么下載?等等之類。
? 于是便介紹了一下如何使用谷歌瀏覽器配合貓爪插件快速下載音頻的方法,但是無奈電腦小白太多,很多操作配置并不會,有這功夫倒不如直接幫忙下載好了文件發與TA便是。
后來想了想,何不寫個足夠傻瓜的工具?實現一鍵點擊下載。
設想
- 編寫一個工具軟件,要足夠傻瓜便捷,一鍵下載。
- 該軟件內置一個chrome內核的瀏覽器。
- 在瀏覽器層面攔截資源訪問,判斷一下,如果是需要的多媒體資源,進行自動下載。
- 不從網站的協議、接口、加解密入手,實現以不變應萬變的萬能方法下載資源。
- 多媒體文件還可以進行人性化命名,也就是網頁瀏覽頁面,點擊自動下載且自動命名好。這個功能點想法很大膽,但是效果很驚艷,也是做到最后面才突發奇想的。
實現
? 想法很好,但是平時時間比較零碎,所以采用敏捷方法,使用csharp開發。好在現在Visual Studio已經到了2019,.NET已經到了5,工具和語言都很方便趁手,寫起代碼來也算是輕松愉快地進行。
界面構想1:主界面就打算設計成一個簡單的瀏覽器窗口,訪問資源網站,看到哪個音頻點擊哪個即可下載,足夠傻瓜便捷。底部一個狀態欄用來顯示日志信息,同時可以添加一些功能按鈕,用來拓展功能。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-xvpZv0LF-1626272990711)(…/…/images/2021/simpleDown1.png)]
? 這里使用的是winform,沒有使用WPF,為的還是快捷,拖放控件快速搭積木實現原型即可。
? 使用VisualStudio的NuGet功能快速添加cefsharp模塊,這個就是基于chrome引擎的瀏覽器模塊,后面簡稱cef(或cef瀏覽器)。選擇x64 CPU,其他模式暫不考慮,先能用起來再說。
參考:Using CefSharp to capture Resource Response Data (body) - Stack Overflow,實現在瀏覽器層面的資源訪問攔截,完整代碼如下:
using CefSharp; using CefSharp.Handler;namespace ResDown {public delegate bool Callback(MessageType messageType, object obj);public class ResourceRequestHandlerExt : ResourceRequestHandler {private string userAgent;public Callback mCallback;public ResourceRequestHandlerExt(string userAgent, Callback callback) {this.userAgent = userAgent;this.mCallback = callback;}protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {var headers = request.Headers;headers["User-Agent"] = userAgent;request.Headers = headers;string resUrl = request.Url;if (request.ResourceType == CefSharp.ResourceType.Media && star.settings.IsAutoDownMedia) {Settings.mediaResourceList.push(resUrl);mCallback?.Invoke(MessageType.on_got_media, null);} else if (request.ResourceType == CefSharp.ResourceType.Image && star.settings.IsAutoDownImage) {Settings.imageResourceList.push(resUrl);mCallback?.Invoke(MessageType.on_got_image, null);}return base.OnBeforeResourceLoad(chromiumWebBrowser, browser, frame, request, callback);}} }? 代碼非常簡短,就是在瀏覽器訪問資源前判斷下資源類型(這里判斷是圖片或者是多媒體資源),把資源URL回調出去(傳給了主進程再進行下載)。這里User-Agent做了可配置,實際上也可以不用額外設置。是否下載圖片或者多媒體也都設計成了可配置,這塊是明智之舉,實際上工具能力泛化了,可以下載圖片使用,也可以下載音頻視頻。
至此,流程基本跑通,想法基本可靠。下面一個難題是,每次點擊下載的資源文件命名問題。以何種規則命名呢?如果以URL路徑中的文件名作為本地文件名,勢必太亂,而且資源文件多起來之后很難分別是哪個跟哪個,很不友好。后來就提出了大膽設想:網站上的資源都是有名稱的,供網友查看,那能不能把網頁上的名稱給利用起來,在使用者點擊的時候,先把這個資源名稱獲取到后,再自動下載呢?其實是可以的。
首先想到的是利用 JavaScript 腳本獲取鼠標點擊的資源在網頁中的元素,然后隨著這個被點擊的元素順藤摸瓜找到資源名稱。一般來說點擊的元素的名稱都不是資源的名稱,但是可以預見,顯示資源名稱的元素離被點擊元素肯定不遠,做個簡單的規則查找即可。
本人對JavaScript并不熟悉,也不喜歡這個蹩腳的語言,后面寫的代碼都是臨時搜索拼湊整合的,將就看下。
先在chrome的控制臺中做調試分析,發現確實可以,參考:「JavaScript獲取網頁點擊時候的元素信息」。于是更進一步,編寫查找資源名稱的代碼,如下:
//detectResName.jsvar findSubStr = function (s, pre, tail) {var p1 = 0;var preLen = 0;var p2 = s.length;if (pre != null) {p1 = s.indexOf(pre);if (p1 == -1) p1 = 0;else preLen = pre.length;}if (tail != null) {p2 = s.indexOf(tail, p1);if (p2 == -1) p2 = s.length;}s = s.substr(p1 + preLen, p2 - p1 - preLen);return s; }document.onclick = function (evt) {var evt = evt ? evt : window.event;var e = evt.srcElement || evt.target;var id = e.id;var className = e.className;var tagName = e.tagName;var text;console.log('\r\n----\r\n[tag]: ' + tagName + '\t[class]: ' + className + '\t[id]: ' + id);if (tagName == 'LI') {text = e.outerText;}else if(tagName == 'IMG') {console.log('img: ' + e.src);}// 根據域名添加規則解析var url = document.URL;var parent = null;var tryCount = 5;if (url.includes('51miz.com/')) {tryCount = 3;}else if (url.includes('www.htqyy.com')){text = document.getElementsByTagName('h1')[0].outerText;console.log('text: ' + text);console.setResName(text);return;}else if (url.includes('tukuppt.com')){tryCount = 1;}if (text == undefined || text == '') {for (var i = 0; i < tryCount; i++) {e = e.parentElement;tagName = e.tagName;console.log('[parent tag]: ' + tagName + '\t[class]: ' + e.className + '\t[text]: ' + e.outerText);if (tagName == 'LI') { break; }}}parent = e;text = e.outerText;// 根據域名添加規則解析if (url.includes('aigei.com')) {text = findSubStr(text, 'VIP\n', '\n');} else if (url.includes('tukuppt.com')) {text = findSubStr(text, null, '00:');} else if (url.includes('xxxxxx.com')) {} else {// 自動解析,取子元素中outerText最長的那個var maxLen = 0;var len = 0;var index = 0;var s;var pos;for (var i = 0; i < parent.childElementCount; i++) {s = parent.children[i].outerText;if (s.length < 5 || s.includes(':') || s.includes('-')) {// 可能是日期時間continue;}len = s.length;//console.log('child: ' + s);if (len > maxLen) {maxLen = len;index = i;}}text = parent.children[index].outerText;}console.log('text: ' + text);console.setResName(text); }? 代碼寫的很不滿意,但也無后續優化計劃,如果有擅長JavaScript的朋友可以提供更優美的代碼。大概思路是:先找到網頁中被點擊的元素,然后一直向上找父節點,一直找到節點的 tagName 為 LI ,認為是找到了資源列表中被點擊的資源所在,然后再遍歷該父節點的子元素取 outerText 屬性。然后結合一定規則取出資源名稱,如果規則比較復雜的,直接根據網站的URL添加粗暴直接的解析規則。
以上代碼做了如下資源網站的兼容測試:
http://www.htqyy.com/ https://www.tukuppt.com/yinxiao/ https://www.aigei.com/sound/class/environment https://www.51miz.com/so-sound/220452.html https://www.qingting.fm/channels/332644/ https://www.ximalaya.com/youshengshu/4756811/主要類型為:
- 列表模式
- 非列表模式
在chrome瀏覽器中實驗完畢了,就可以集成到自己的軟件中來。那么原來的界面設計可能就不夠滿足了,這個界面如何設計?既不影響原有的簡單使用方式,又要增加強勁的功能。
界面構想2:
這個后來直接借鑒了谷歌瀏覽器的開發者模式界面思路,直接在原界面右側設計一個側邊欄,作為軟件的黑客工具欄窗口。該窗口可以通過底部狀態欄的黑客(帽子圖標)按鈕隱藏和現實,隱藏時網頁窗口布局自動擴寬,對原有界面影響不大。
工具窗口顯示時,可以選擇啟用的JavaScript腳本列表,這里預置了三個(可拓展模式,以后相加可以隨時添加并啟用):
- 空:選擇時,網頁加載后不加載自定義的JavaScript腳本。
- detectElement.js:選擇時,網頁加載后,在頁面上隨便點擊即可顯示元素信息,代碼內容不在額外貼出,參考:「JavaScript獲取網頁點擊時候的元素信息」。
- detectResName.js:選擇時,網頁加載后,在頁面上隨便點擊即可顯示元素信息,同時調用一個導出的JS函數,傳遞獲取到的資源名稱。
注意上面的JavaScript代碼中,有一個接口函數:console.setResName,它不是原生就有的函數,而是封裝導出的,在c#中導出JavaScript接口函數的方法如下:
先聲明一個類型:
namespace ResDown {class ClassForJS {public delegate void OnWebLog(string msg);private static OnWebLog callback = null;public static void setCallback(OnWebLog log) {callback = log;}public void log(string msg) {if (callback != null) {callback(msg);}}public void log(string msg, int code) {if (callback != null) {callback(msg);}}public void setResName(string name) {Settings.resName = name.Trim();}} }再創建一個對象,并注冊:
private ClassForJS objForJS = new(); ClassForJS.setCallback(OnWebLog);browser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true; browser.JavascriptObjectRepository.Register("console", objForJS, isAsync: true, options: BindingOptions.DefaultBinder);/// <summary> /// 網頁日志輸出 /// </summary> /// <param name="msg"></param> private void OnWebLog(string msg) {mContext.Post((o) => {if (this.txtWebLog.Lines.Length > 9999) {this.txtWebLog.Clear();}this.txtWebLog.Text += o + "\r\n";if (this.txtWebLog.Visible) {this.txtWebLog.SelectionStart = this.txtWebLog.TextLength;this.txtWebLog.ScrollToCaret();}}, msg); }上面討了個巧,直接把對象注冊為了console,通過導出一個log接口函數就算了接管了日志輸出。再額外導出一個接口函數 setResName,用來接收獲取到的資源文件名即可。這樣實現的好處是,方便調試JavaScript腳本,一者軟件可以直接攔截并顯示日志;二是即使把JavaScript腳本拿到谷歌控制臺里執行,也只需注釋一行代碼即可,大部分代碼都是直接復用的。
效果
軟件最終的實現效果是:
- 軟件啟動后,隱藏黑客工具欄窗口,默認加載「detectResName.js」腳本,默認只下載多媒體資源。這里為了方便演示,把工具窗口打開了。
- 瀏覽器欄有歷史訪問列表功能,可以在歷史里選擇URL,同時有自動完成功能。
- 訪問某資源網站后,想下載哪個資源,點擊播放即可,不用聽完。這個時候腳本會執行,自動獲取到資源名稱,傳遞給csharp代碼,然后下載資源的時候就使用這個資源文件名作為保存的文件名稱。
- 全部異步,可以隨便點擊哪個資源名稱均可,資源文件保存不會串名。對于多集的有聲小說,也可以按照每集的名稱下載。
- 那些需要登錄或者需要會員的資源網站,均無須購買均可下載,也即只要能試聽就能下載,既強大又便捷。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gKYuKqtb-1626272990720)(…/…/images/2021/simpleDown2.png)]
其他能力:
- 360doc這種禁止復制網頁文本的,設計了一個禁用或啟用頁面JavaScript代碼的開關(軟件底部顯示JS的按鈕),關閉后,直接禁用網頁JavaScript的執行,就可以直接復制網頁里的文本,爽歪歪。
總結
? 以上,技術含量不高,權當自己無聊「娛樂」了一把,感謝C#、VisualStudio、chrome。
? 但該方法其實是萬能方法,任資源網站怎么變化,怎么加密,都繞不過去。即使通過URL直接下載的是加密的,其實cef里還有接口可以用,大概思路就是:網頁上的資源總要渲染,總要讓人聽到看到吧,那就直接把渲染好的內存數據保存下來就好,這塊實現起來可能會花點時間,我就沒有寫了,但是是可以突破的。相關的入手點,可以參考這幾個關鍵詞:OnResourceLoadComplete、MemoryStreamResponseFilter、GetResourceResponseFilter。
總結
以上是生活随笔為你收集整理的资源下载的终极利器-资源轻松简单下载-资源万能下载法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【开发工具之Spring Tool Su
- 下一篇: 项目管理思考——我适合做项目经理吗