01

前言

最近因为 Zigma 帮我写了个推广 Catcher 小程序软文的原因,答应了他帮他爬了一个盗版音频网站的整套 《李淼谈奇案》 。

在制作爬虫脚本的过程中,也是遇到了一些有趣的问题,所以特此写了这篇 Blog 用于记录脚本的整一个实现与问题解决。

02

实现思路

整体的实现思路其实并不复杂,获取整个项目的列表,解析每一项的下载链接,然后分别下载保存起来。

03

获取项目列表

首先,在查看选集页的网页源码可以发现,所有子项目都是用  标签来显示的,所以利用这一规律加上正则,便可以获得每一个子项目的指向链接。

r, _ := http.Get("http://xxxx/xxxx")defer r.Body.Close()b, _ := ioutil.ReadAll(r.Body)re := regexp.MustCompile("href=\"/play/(.*)\" target=\"_blank\"")rlist := re.FindAllSubmatch(b, -1)for _, i := range rlist {     fmt.Println(i[1])}

简单的 Golang 正则介绍

Golang 的 Regexp 对象可以通过以下两种方法构建,

// 当构建失败会返回 errre, err := regexp.Compile(`\s+`)

// 不返回 errre := regexp.MustCompile(`\s+`)

个人更喜欢后者,因为有时候不需要多次使用 Regexp 对象的时候,可以直接把表达式接在 FindSubmatch 等语句前面(可以省一行代码)。

然后,常用的语句有以下,

// 检测是否存在match, _ := regexp.MatchString("H(.*)d", "Hello World")fmt.Println(match) //true// 利用 Regexp 对象r, _ := regexp.Compile("H(.*)d")fmt.Println(r.MatchString("Hello World")) //true

// 替换字符// 三个修饰词 `Literal` 和 `Func`r, _ = regexp.Compile("W(.*?)d")fmt.Println(r.ReplaceAllString("Hello World! World", "html")) // Hello html! html// 利用匿名函数对 Replace 返回值进行处理fmt.Println(r.ReplaceAllStringFunc("Hello World! World",  func(b string) string {    fmt.Println(b)    return b + b  }))// Hello WorldWorld! WorldWorld// Literal 修饰符可以将 `分组引用符` 当普通字符处理fmt.Println(r.ReplaceAllString("Hello World! World", "${1}ooo")) //Hello orlooo! orlooofmt.Println(r.ReplaceAllLiteralString("Hello World! World", "${1}ooo")) //Hello ${1}ooo! ${1}ooo

// 匹配字符// 三个修饰词 `All` 和 `Submatch` 以及 `Index`// Find 组合以上修饰词就可以组成不同效果r, _ = regexp.Compile("W(.*?)d")// 最基础的版本是这个 只匹配一次且不能分组fmt.Println(r.FindString("Hello World! World")) // World// 变种 只匹配一次 返回子匹配项fmt.Println(r.FindStringSubmatch("Hello World! World")) // [World orl]// 变种 全局匹配 且返回匹配项 第二个参数可指定匹配次数fmt.Println(r.FindAllStringSubmatch("Hello World! World", -1)) // [[World orl] [World orl]]// 变种 返回开始与结束索引而非字符fmt.Println(r.FindAllStringSubmatchIndex("Hello World! World", -1)) // [[6 11 7 10] [13 18 14 17]]

// 分割字符串r = regexp.MustCompile("W")fmt.Println(r.Split("Hello World! World", -1)) //[Hello orld! orld]

// 以及一个通用修饰词 `String` 当去掉此修饰词时输入与输入会变成 []byte 类型r, _ = regexp.Compile("W(.*?)d")fmt.Println(r.FindStringSubmatch("Hello World! World")) //[World orl]fmt.Println(r.FindSubmatch([]byte("Hello World! World"))) //[[87 111 114 108 100] [111 114 108]]

04

解析下载链接

从 03 中我们获得了每个项目的链接,接下来就要逐个解析下载链接了。

通常来讲网站本身的资源获取分成两种,如果网站本身是 CSR(客户端渲染)形式的,那么资源通常都是以 RESTful API 然后 JSON 形式直接返回到前端的,这时候就比较简单,只需要在开发者工具里找到他的 API 然后直接处理就 VANS 了,有时候会做一些加密或者验证处理,但是只要是H5肯定都是藏不住的,费点心思去 JS文件里找到密钥即可;如果是 SSR(服务器渲染) 形式的,那么这时候音频的链接多半都是在 html 或者 js 文件里,可能还会有一些混淆手段,处理起来就比前者复杂,需要灵活的利用正则达到目标。

而这次需要爬取的网站属于后者,在一番查看后,发现了音频地址的来源

但直接根据上述的规律对项目每项进行解析下载链接,会发现有一些下载链接明显是错误的或者是空值。

r := regexp.MustCompile(`\$\(this\)\.jPlayer\("setMedia", \{\s*mp3:'(.*?)'(\+murl)?`)url = r.FindSubmatch(b)

再次刷新该页面后发现是由于网站开发在这做了点小手脚;如下图,他随机生成了 murlxxxxxxx 与 urlxxxxxxx 字符,有时这些字符为空音频链接直接在 JPlayer函数 里面,有时候又是将这两个字符拼起来的值放进 JPlayer 函数里。

其实吧,u1s1 这个方法还是挺聪明的,因为正则或者其它爬虫方案( 比如Python 的 Beautiful Soup)本身都是直接将整个网页当作纯字符串来解析,这样的作法无疑是给爬虫增加了一些游戏难度。

知道了他的诡计,便可以写出应对的方法了。

re := regexp.MustCompile(`(?m)^url\d+ \= '(.*?)'(\+murl)?`)url := re.FindSubmatch(b)if string(url[1]) == "" {  re = regexp.MustCompile(`\$\(this\)\.jPlayer\("setMedia", \{\s*mp3:'(.*?)'(\+murl)?`)  url = re.FindSubmatch(b)}urlContent := string(url[1])// 这里发现他有几集音频资源是直接用的蜻蜓FM的,所以做了个分支处理if m, _ := regexp.MatchString(`(\.m4a|\.mp3)$`, urlContent); !m {  if m, _ := regexp.MatchString(`qingting`, urlContent); m {    urlContent = urlContent + ".m4a"  } else {    urlContent = urlContent + ".mp3"  }}if m, _ := regexp.MatchString(`qingting`, urlContent); !m {  urlContent = "http://xxxxxxx/xxxxxxxxx" + urlContent}re, _ = regexp.Compile("正在播放:(.*)") //顺便把节目标题爬下来等会用来当标题name := re.FindSubmatch(b)

大功告成,只剩最后一步了。

05

利用Goroutine实现并发爬取

Golang 的协程是一个很牛批的东西,不仅是因为他的简单易用。

像是 Node 或者 Python 的 coroutine ,他们只是并发而非并行,也就是说当你卡死在一个 coroutine 上的时候是无法切换到别的 coroutine 上的,也就会导致卡死。但 Goroutine 却可以实现并行,也就是你的主线程卡住的时候,协程一样可以继续运行。(菜比理解,大佬勿喷)

然后,关于 Goroutine 的扩展知识有很多,比如 sync.waitGroup 以及读写锁啥的,这里由于用不到且篇幅有限就不铺开来介绍了,这里主要介绍一下如何创建一个协程以及 通过管道(channel)进行协程间通信。

创建协程

package main

import (  "fmt")

func main() {  // 创建了10个协程  for i := 0; i < 10; i++ {    go goroutineD(i)  }  // 需要堵住主线程,否则会由于主线程退出而导致协程来不及执行  select {}}

func goroutineD(i int) {  fmt.Println(i)}
PS D:\学习\李淼\333> go run main.go91276868345fatal error: all goroutines are asleep - deadlock!

Go 的协程创建非常简单,只需要在函数面前加个 go 即可。需要注意的是,不像是node 中的异步,

function s() {    setTimeout(() => {        console.log("一秒后")    }, 1000);}

function main() {    s()    return "结束"}

console.log(main())
PS D:\学习\李淼> node d.js结束一秒后

go 的协程是需要堵住主线程的,否则就会出现主线程结束导致整个进程终止,但协程还来不及执行完的情况。除了上述直接整个 select {} ,还有很多方法实现堵住主线程以等待 go 的协程执行完的方法,也包括下面介绍的 管道。

管道(channel)

// 声明一个用于传输 int 类型数据的管道并返回其指针(无缓存)char := make(chan int)// 缓存为一char = make(chan int, 1)

管道的概念,个人理解就类似,主线程和协程就像是两个房间,而管道就是在这两房间中间凿开了个洞,而缓存就指的是你这个洞的宽度。如果这个洞是没有宽度的,则必须要主线程房间里的人站在那接着,协程才能传东西过来,不然就会掉到地上。而缓存就是给了一个存在洞里的空间,协程房间里的人可以把东西放进管子里,然后主线程的人过一会再拿走,就比如以下的例子便可以看出二者的区别;

char := make(chan int, 1)char 1fmt.Println(char) // 1

char = make(chan int)char 1fmt.Println(char) // fatal error

其中箭头是 channel 中个人觉得很棒的设计,很象形的表现了放入和取出的的操作。根据 channel 的特性,上面的输出 0-9 字符的代码便可以写成这样,

package main

import (  "fmt")

func main() {  char := make(chan int)  for i := 0; i < 10; i++ {    go goroutineD(i, char)  }  for i := 0; i < 10; i++ {    fmt.Println(  }}

func goroutineD(i int, char chan int) {  char }

根据以上的知识,并发的去爬音频便非常简单了,

  • 在主线程中获取项目的列表,并创建多个协程让他去各自执行接下来的事务

// rlist 是一个由所有项目构成的数组page := make(chan string, 4)for _, i := range rlist {  go getMediaUrl(i[1], page)}for i := 0; i < len(rlist); i++ {  fmt.Println("下载完成 ", }
  • 解析下载链接并传给下载保存的函数,在函数结束后告诉管道

func getMediaUrl(i string, page chan string) {  r, err := http.Get(i)  if err != nil {    // 他服务器有点屑,所以做一个重试处理    go getMediaUrl(i, page)  } else {    defer r.Body.Close()    b, _ := ioutil.ReadAll(r.Body)    re := regexp.MustCompile(`(?m)^url\d+ \= '(.*?)'(\+murl)?`)    url := re.FindSubmatch(b)    if string(url[1]) == "" {      re = regexp.MustCompile(`\$\(this\)\.jPlayer\("setMedia", \{\s*mp3:'(.*?)'(\+murl)?`)      url = re.FindSubmatch(b)    }    urlContent := string(url[1])    if m, _ := regexp.MatchString(`(\.m4a|\.mp3)$`, urlContent); !m {      if m, _ := regexp.MatchString(`qingting`, urlContent); m {        urlContent = urlContent + ".m4a"      } else {        urlContent = urlContent + ".mp3"      }    }    if m, _ := regexp.MatchString(`qingting`, urlContent); !m {      urlContent = "http://xxxx/xxxxxxx" + urlContent    }    re, _ = regexp.Compile("正在播放:(.*)")    name := re.FindSubmatch(b)    SpiderPage(urlContent, string(name[1]))    page string(name[1])  }}
  • 检测音频文件是否存在,不在则下载并保存

func SpiderPage(url string, name string) {  _, err := os.Stat("./李淼谈奇案/" + name + ".mp3")  if os.IsNotExist(err) {    r, err := http.Get(url)    if err != nil {      fmt.Println("下载失败" + name)    } else {      defer r.Body.Close()      path := "./李淼谈奇案/" + name + ".mp3"      f, _ := os.Create(path)      defer f.Close()      b, _ := ioutil.ReadAll(r.Body)      f.Write(b)    }  }}

06

结束语

至此,整个脚本便算是做完了,第一次正经写学习笔记,可能会废话有点多?如果需要源码的话,可以在公众号后台回复「李淼谈奇案」获得整个源码。(特别声明!只是为了学习交流,整套音频听都没敢听爬下来立马就删了!)

最后嘴一句,这网站服务器带宽是真的不咋地,下载速度才十几k,并发的爬都得爬半天。

END

python中data.find_all爬取网站为空列表_利用Golang快速爬取盗版网站的整套音频相关推荐

  1. 在python中创建一个具有特定大小的空列表

    本文翻译自:Create an empty list in python with certain size I want to create an empty list (or whatever i ...

  2. python中data.find_all爬取网站为空列表_Python网络爬虫之Scrapy 框架-分布式【第二十九节】...

    1. 介绍scrapy-redis框架 scrapy-redis 一个三方的基于redis的分布式爬虫框架,配合scrapy使用,让爬虫具有了分布式爬取的功能. github地址: https://g ...

  3. python为啥爬取数据会有重复_利用Python来爬取“吃鸡”数据,为什么别人能吃鸡?...

    原标题:利用Python来爬取"吃鸡"数据,为什么别人能吃鸡? 首先,神装镇楼 背景 最近老板爱上了吃鸡(手游:全军出击),经常拉着我们开黑,只能放弃午休的时间,陪老板在沙漠里奔波 ...

  4. Python中ArcPy实现对大量长时间序列栅格遥感影像批量逐像元求取像素平均值

      本文介绍基于Python中ArcPy模块,对大量长时间序列栅格遥感影像文件的每一个像元进行多时序平均值的求取.   在遥感应用中,我们经常需要对某一景遥感影像中的全部像元的像素值进行平均值求取-- ...

  5. python中data是什么意思_C++中cv::Mat中的data属性对应在python中是什么

    1, 因为我要使用一个dll,看C++的代码,是这样调用的 using namespace cv; m_image_mat = imread ( full_file_name ); data = m_ ...

  6. python中的序列类型数据结构元素的切片操作_浅析python中的分片与截断序列

    浅析python中的分片与截断序列 序列概念 在分片规则里list.tuple.str(字符串)都可以称为序列,都可以按规则进行切片操作 切片操作 注意切片的下标0代表顺序的第一个元素,-1代表倒序的 ...

  7. python中打开文件时只允许写入的模式是_详解python中各种文件打开模式

    在python中,总的来说有三种大的模式打开文件,分别是:a, w, r 当以a模式打开时,只能写文件,而且是在文件末尾添加内容. 当以a+模式打开时,可以写文件,也可读文件,可是在读文件的时候,会发 ...

  8. python中打开文件时只允许写入的模式是_在open函数中访问模式参数使用()表示打开一个文件只用于写入。(4.0分)_学小易找答案...

    [单选题]溢流坝属于( ) [单选题]在重力坝的底部沿坝轴线方向设置大尺寸的空腔,即为( ) [单选题]模式()的用途是打开一个文件用于追加.如果该文件已存在,文件指针将会放在文件的结尾.(4.0分) ...

  9. 在python中类型属于对象变量是没有类型的_如何理解python对象有类型,变量无类型...

    在Python中,有这样一句话是非常重要的:对象有类型,变量无类型.怎么理解呢? 首先,5.6都是整数,Python中为它们取了一个名字,叫做"整数"类型的对象(或者数据),也可以 ...

最新文章

  1. 设计模式之状态模块加观察者模式
  2. 和lock一起学beego 博客系统开发为例(五)
  3. WPF DataGrid横向显示
  4. ARC中block块作为属性的使用笔记
  5. 一个4体低位交叉的存储器_GD25Q16CSIG|NRAM存储器的原理及优势是什么?
  6. Redis 集合处理
  7. python 逆向生成正则表达式_用Python中的正则表达式生成lis
  8. 激光slam_机器人主流定位技术,激光SLAM与视觉SLAM谁更胜一筹
  9. html5单击修改背景色,用获取节点的方式实现点击按钮改变标签背景颜色的问题...
  10. HDU 5045 状压DP 上海网赛
  11. ios 模拟器添加经纬度_iOS 微信双开来了,但我不建议你使用
  12. 一个编辑的黑洞项目:编程日历背后的 “鬼级操作”
  13. 实现一周之内自动登录的 cookie和session还有localStorage的存储机制
  14. 解决win7语言栏消失问题
  15. 基于matlab的频率特性测试仪,基于虚拟仪器的网络频率特性测试仪
  16. 【生活】教你有效戒糖
  17. hadoop SWAP交换空间
  18. python3使用opencv读取raw格式图片并保存为bmp格式图片
  19. html5绘制五环,浅析HTML5的Canvas——案例绘制
  20. 网页Unity3d游戏全离线玩的高招!

热门文章

  1. 基于观察者模式——创建显示天气数据
  2. 【树状数组 思维题】luoguP3616 富金森林公园
  3. Vijos——T 1016 北京2008的挂钟 || 洛谷—— P1213 时钟
  4. asp.net中用LinkButton取到gridview中当前行的ID值
  5. Cut Curve randomly
  6. [转载] java简易爬虫Crawler
  7. 图标缩排和悬浮突显的简单实现
  8. 2018年9月28号-10月9号
  9. Weblogic的安装与卸载
  10. android中shape的属性