现在有某站点,它的资源主要是 mp3 音频,站点不提供直接批量下载,只能按目录逐级点击,进入某专辑的文件列表,点某文件条目后进入,点击下载按钮转到下载处理页,成功后会接到响应的资源链接,chrome 会打开新标签页播放。
但某专辑动辄有上百个的文件条目,逐一的点专辑列表下载非常繁琐,所以考虑用工具。用工具自动化批量获取文件的方法,大致可归为爬虫技术,涉及到对 web 网页内容的抓取和解析,最终目的是获得文件地址列表。
抓取 web 网页内容的方法和技术有很多,因平时常用 PowerShell 做 Windows 的自动化操作,所以考虑用 PowerShell,具体来说就是用 PowerShehll 的 Invoke-WebRequest 命令。
关于 Invoke-WebRequest
参考微软官方文档,定义如下:
Invoke-webRequest
[-UseBasicParsing]
[-Uri]<Uri>
[-WebSession <WebRequestSection>]
[-SessionVariable <String>]
[-Credential <PSCredential>]
[-UseDefaultCredentials]
[-CertificateThumbprint <String>]
[-Certificate <X509Certificate>]
[-UserAgent <String>]
[-DisableKeepAlive]
[-TimeoutSec <Int32>]
[-Headers <IDictionary>]
[-MaximumRedirection <Int32>]
[-Method <WebRequestMethod>]
[-Proxy <Uri>]
[-ProxyCredential <PSCredential>]
[-ProxyUseDefaultCredentials]
[-Body <Object>]
[-ContentType <String>]
[-TransferEncoding <String>]
[-InFile <String>]
[-OutFile <String>]
[-PassThru]
[<CommonParameters>]
具有如下成员:
void Dispose()
bool Equals(System.Object obj)
int GetHashCode()
type GetType()
string ToString()
Microsoft.PowerShell.Commands.WebCmdletElementCollection AllElements { get; }
System.Net.WebResponse BaseResponse {get;set;}
string Content {get;}
Microsoft.PowerShell.Commands.FormObjectCollection Forms {get;}
System.Collections.Generic.Dictionary[string,string] Headers {get;}
Microsoft.PowerShell.Commands.WebCmdletElementCollection Images {get;}
Microsoft.PowerShell.Commands.WebCmdletElementCollection InputFields {get;} Microsoft.PowerShell.Commands.WebCmdletElementCollection Links {get;}
mshtml.IHTMLDocument2 ParsedHtml {get;}
string RawContent {get;}
long RawContentLength {get;}
System.IO.MemoryStream RawContentStream {get;}
Microsoft.PowerShell.Commands.WebCmdletElementCollection Scripts {get;}
int StatusCode {get;}
string StatusDescription {get;}
Invoke-WebRequest 命令向 web 页或 web 服务发送 HTTP,HTTPS,FTP 和 FILE 请求,解析响应并返回表单、链接、图像和其他 HTML 元素,返回类型为 HtmlWebResponseObject ,具体用法示例可参考官网文档,需要指出的是,该命令可用于一般HTTP请求场景,如保存认证信息的 WebSession 参数,对于大多数互联网应用是够用的,但也存在应付不了的场景,详情见下一篇关于 puppeteer 的介绍。
批量获取下载地址
基于以上对于 Invoke-WebRequest 指令的认识,我们可以写批量获取下载地址的代码了。该资源网站的资源路径格式为 “作者名/专辑名/专辑列表/资源文件”,本文关注说明如何抓取资源地址,所以只从”专辑列表”页面开始抓取。
某资源专辑列表页的地址如下:
$downloadListUrl = "http://www.pingshu8.com/MusicList/mmc_7_4906_1.htm"
直接请求获得下载列表:
$downloadListWebPage = Invoke-WebRequest $downloadListUrl
# list4 是从返回的 web 页中找到的列表所在的 div 元素的类名
$downloadList = $downloadListWebPage.ParsedHtml.body.getElementsByClassName("list4")
到现在获得专辑列表的HTML代码段,$downloadList 对象对应的类型不包括任何方便获取列表项的方法,还好范围已缩减至此,正是需要祭出正则表达式的时候了:
# 从 $downloadList 中解析出地址并转换成集合
$urlPattern = "/down_\d*.html"
# 使用Regex 类匹配出地址并生成集合
$addresses = [Regex]::Matches($downloadList[0].innerHTML,$urlPattern,"IgnoreCase")
$addressList = New-Object Collection.Generic.List[string]
$downloadBaseUrl = "http://www.pingshu8.com"
$addresses | % { $addressList.Add($downloadBaseUrl + $_.Value) }
到现在我们就获取了专辑列表第一页的列表内容,要获得全部列表项,需要逐页访问,可以把上述代码整理如下:
$entryListUrl = "http://www.pingshu8.com/MusicList/mmc_7_4906_1.htm"
$downloadBaseUrl = "http://www.pingshu8.com"
$addressList = New-Object Collections.Generic.List[string]
# 递归获取专辑列表全部地址
function GetAddressList($url)
{
$downloadListWebPage = Invoke-WebRequest $url
$downloadList = $downloadListWebPage.ParsedHtml.body.getElementsByClassName("list4")
$urlPattern = "/down_\d*.html"
$addresses = [Regex]::Matches($downloadList[0].innerHTML,$urlPattern,"IgnoreCase")
$addresses | % { $addressList.Add($downloadBaseUrl + $_.Value) }
# 如果有下一页,存在形如 <a href="/Musiclist/mmc_7_4906_4.htm">末页</a> 的字符,作为递归条件
# "末页"二字在 PS 中可能显示为乱码,造成不匹配,需要转码
$nextPageUrlSectionPattern = @"
<a href=\"\/Musiclist\/[\b,\w]{1,}\.htm\"\>末页</a>
"@
$nextPageSection = $downloadListWebPage.ParsedHtml.body.getElementsByClassName("list5")
$nextPageUrlSection = [Regex]::Matches($nextPageSection[0],$nextPageUrlSectionPattern,"IgnoreCase")
$nextPageUrlPattern = "\/Musiclist\/[\b,\w]{1,}\.htm\"
$nextPageUrl = [Regex]::Matchers($nextPageSection[0],$nextPageUrlPattern,"IgnoreCase")
if ($nextPageUrlSection -ne $null)
{
GetAddressList($nextPageUrl)
}
return
}
至此我们获得了某专辑全部下载地址,但严格来说还只是下载页面地址,所以任务还没完成,因为网站要求用户点击“下载”按钮,chrome 会自动播放返回的真正的MP3文件,这.就引发了下面两个问题:
执行 js 获得链接解析地址。在上面获取的地址还需要解析下载按钮的动作来获得, js 动作代码如下:
function downfile() {
var downurl = "pingshu://cc%252Fbzmtv%255FInc%252Fdownload%252Easp%253Ffid%253D236632akb%253D%253D"; downurl = decodeURIComponent(decodeURIComponent(downurl.substr(10,downurl.length-1))); downurl = downurl.substr(2,downurl.length-7);
}
解析出的地址 downurl 形如 “/bzmtv_Inc/download.asp?fid=236632”,使用动态页面提供真正的资源地址,这里问题是如何在 PS 中执行 js 代码。
如何下载到资源。上面动态页面直接用 Invoke-WebRequest 访问,返回的响应没有任何有用的内容,怀疑是网站使用了反爬虫策略。
第1个问题可以用第三方类库解决(如 jint),但第2个问题,网站用了哪种反爬虫策略,事实是 PS 不是专门用来做爬虫的,并且没有专门的模块来处理这方面问题,如果一定要用 PS 也不是不可以,会耗费大量的时间精力,但问题是技术本身就是工具,目的是让使用者更方便,所以这么做是得不偿失的,解决问题的方法是另辟蹊径。
解决上述第2个问题的方法放到另一篇去讲,顺便聊一聊爬虫。
权版信息
本文永久链接:https://1983cc.github.io/2017/12/26/用PowerShell批量下载某站点的文件/