AF的博客

用PowerShell批量下载某站点的资源文件

现在有某站点,它的资源主要是 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文件,这.就引发了下面两个问题:

  1. 执行 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 代码。

  2. 如何下载到资源。上面动态页面直接用 Invoke-WebRequest 访问,返回的响应没有任何有用的内容,怀疑是网站使用了反爬虫策略。

第1个问题可以用第三方类库解决(如 jint),但第2个问题,网站用了哪种反爬虫策略,事实是 PS 不是专门用来做爬虫的,并且没有专门的模块来处理这方面问题,如果一定要用 PS 也不是不可以,会耗费大量的时间精力,但问题是技术本身就是工具,目的是让使用者更方便,所以这么做是得不偿失的,解决问题的方法是另辟蹊径。

解决上述第2个问题的方法放到另一篇去讲,顺便聊一聊爬虫。


权版信息
本文永久链接:https://1983cc.github.io/2017/12/26/用PowerShell批量下载某站点的文件/