【项目简述】
抓取某学院视频网站的系列课程,相比于上一篇,这一次多了不少实用性。
【前提】
必须要有该网站的VIP账户,主要是Cookies要使用,否则无法获取一部分受限视频。当然有不少免费获取VIP账户的方法可以自行搜索。
【视频网站的特点】
1)这类网站一般都有不同程度的反扒措施,某学院采用的是动态缓存,即是有权限的会员打开网页才能获取该视频的Url,这个Url有一定时效性,不适合大量抓取Url后再下载(等全抓完前面的已失效了)。但是边抓取Url边下载是没有问题的,这是较容易爬取的一类。网页的分析和以前并无分别,用F12搞定。
2)由于目标文件数量多,文件的组织比较重要,url中的文件名并不适合于文件保存(人类无法理解。。。)。这里采用最符合阅读习惯的方式对于不同层级建目录,文件名直接采用页面标题命名。另外网页上有些层级的内容未带序号,为避免下载以后排序混乱的问题,需要在前面追加序号。
3)由于目录和文件名来自网页,就会有相对于操作系统的非法字符问题,比如windows下 \/:*?"<>| 这些字符是不能出现在目录文件中的。另外还有不少其他非法文字。
4)视频文件爬取比较耗时,不要期望一次性成功,所以必须要有重复检测,即遇到已爬取的文件直接跳过。
先上核心代码
for( i <- 1 to 5; e1 <- fCrawl(url.format(i)).select("div.lesson-card"); path1 = e1.select("h2").text.stripIllegalChar; (e2,j) <- fCrawl(e1.select("div.text a").attr("href")).select("div.lesson-item").zipWithIndex; path2 = j + 1 + "_" +e2.select("dt.title").text.stripIllegalChar; e3 <- fCrawl(e2.select("a").attr("href")).select("dl.lessonvideo-list a")){ val file = e3.text.stripIllegalChar+".mp4" val path = BASE_PATH + "/" + path1 + "/" + path2 if (!(new java.io.File(path+"/"+file)).exists){ fDownload(path, file, fCrawl(e3.attr("href")).select("source").attr("src")) println(s"$path\t$file\t下载成功!") }else{ println(s"$path\t$file\t已下载!") } }
是的,你没有看错,得益于Scala强大的表现力,核心代码就只有这10几行。当然还没有把一些辅助型代码算在内。
说明:
- 首先是Scala的for语句实际上是一个可用于多层次迭代的语法糖,上面的语句实际上产生了四层迭代。第一层 1 to 5 是对分页的迭代,后面每一层进入一次页面抓取,最后循环体内的fcrawl就可以抓取到视频Url了。类似如下结构:
(1 to 5)
.foreach(i => fCrawl(url.format(i)).select("div.lesson-card") )
.foreach( e1 => fCrawl(e1.select("div.text a").attr("href")).select("div.lesson-item").zipWithIndex)
.foreach( (e2,j) => fCrawl(e2.select("a").attr("href")).select("dl.lessonvideo-list a")))))
- e2级中,zipWithIndex这个方法是用来给迭代对象添加序号的,由于Scala的序号从0开始,所以使用的时候还需要加1。
- 熟悉Scala的朋友一定会问,stripIllegalChar是什么鬼,居然用在了String类型上。这个其实是Scala的黑科技之一隐式类,很多时候,它能让代码更加优雅。
真相在这里
implicit class StringImprovement(val s : String){ // 删除文件名中的非法字符 def stripIllegalChar = s.replaceAll("???", "").replaceAll("\\\\|/|:|\\*|\\?|<|>|\\||\"", "") }
现在String类型的对象就可以使用stripIllegalChar方法去删除那些非法字符了。???是特指一些无法显示的非法字符,这个只能遇到的时候,现去添加,由于该程序有重复文件判断,所以每次异常修改后,直接再次执行就好了。
fCrawl,fDownload这两个方法就是简单递归自身保证Url抓取和文件下载的成功。
def fCrawl(url: String): Document = Try(Jsoup.connect(url).timeout(0).cookies(cookies).get()) match { case Failure(e) => println(e.getMessage); sleep(10000); fCrawl(url) case Success(d) => d } def fDownload(path: String, file: String, url: String): Unit = Try(JavaHelper.download(path, file, url)) match { case Failure(e) => println(e.getMessage); sleep(10000); JavaHelper.download(path, file, url) case Success(_) => }
异常的时候三步:异常信息打印,暂停10秒,再次执行。
最后是JavaHelper.download方法,由于jsoup下载文件的能力较差,自己写了一个java方法如下(实际运行下来发现还蛮健壮,极少出错):
public static void download(String path, String fileName, String fileUrl) throws IOException {
File pathFile = new File(path);
if (!pathFile.exists()) {
pathFile.mkdirs();
}
URL url = new URL(fileUrl);
InputStream inStream = url.openConnection().getInputStream();
FileOutputStream fs = new FileOutputStream(path + "/" + fileName);
byte[] buffer = new byte[1204];
int byteread = 0;
while ((byteread = inStream.read(buffer)) != -1) {
fs.write(buffer, 0, byteread);
}
fs.close();
}
其他代码
// Url源 val url = "http://ke.jikexueyuan.com/xilie/?page=%d" // 存储路径 val BASE_PATH = "G:/video/Series" def sleep(i: Long) = Thread.sleep(i) // 这里设置你的vip账户的cookies信息,用F12你应该懂的 val cookies = new java.util.HashMap[String,String] cookies.put("uname", "???") cookies.put("authcode", "???")
经过累计时间差不多1天的半值守运行,成功下载了带有良好目录结构的3000+视频文件。
小结: Scala的很多特性(如类型推断,隐式转换,众多语法糖等等),让它变成一门极高“信噪比”的语言。显然,更少的代码,会让维护都变得更容易。但同时,灵活掌握这些特性也有一定难度,我想这也许是它始终无法普及的原因吧。