前言
闲来无事的时候,偶尔也会看看漫画,但是鹅厂的操作大家都懂,想看最新的你就得给钱,本着白嫖精神,我找到了扑飞漫画 ,但是这网页的阅读体验一言难尽,他家的APP也是,动不动就加载失败,一等一半天。思来想去,还是弄个爬虫把图片都爬下来,然后想法弄到kindle里面岂不美哉。因为不会Python,所以只好用GO来写了,虽然没写过,但是可以现学嘛。
初识爬虫
网上找了下资料,go的写爬虫也太简单了吧,几行代码就搞定了,比如下面这样,几行代码就把整个页面拿到了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "io/ioutil" "net/http" ) func main () { resp, err := http.Get("https://www.baidu.html" ) if err != nil { fmt.Println("http get error" , err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println("read error" , err) return } fmt.Println(string (body)) }
得知爬虫这么简单的我笑出了声,剩下的工作也就是获取网址源码,然后正则解析出我想要的东西,循环以上步骤拿到图片地址下载即可,整体可分为三步。
爬取漫画首页,获取目录标题极其链接
通过目录拿到的链接去访问图片所在页面,然后解析出图片地址
下载图片,并存储在指定页面
有了如上结论的我开始奋笔疾书,然后当我进行到第二步获取图片地址的时候,发现爬下来的页面与网页实际的不一样。
我获取到网页内容是一个loading页面,而不是这个页面最终的状态。起初的想法是因为网页一开始打开时这样,加载完后就正常了,所以我隔几秒在读取网页内容,事实证明这样不行。
意外之喜
因为不知道啥原因导致的,所以也没找到解决办法,期间尝试了一下爬虫框架也依旧无果。但是也没有彻底放弃,没事就看看这页面的源码,希望能找出解决办法。
乍一看是某种解密的东西,尝试运行下发现可以得到这一页的图片地址,还剩下几个变量又都是干嘛的呢,好奇之下运行了一下。
如此一来,整个章节所有图片的地址也就都有了。问题也就是怎么在go里运行js,然后就找到了一个叫otto 的包,完美。
开始书写
因为在寻找解决办法的过程中接触了一些爬虫框架,整体感觉比源码撸方便一点,主要是不用自己写正则,可以像js操作DOM节点那样找到自己想要的内容,所以就换了框架colly 来完成整改爬虫。
定义一个全局变量,用于存放章节与之对应的链接以及该章节下的所有图片
1 2 3 4 5 6 7 type Catalog struct { Title string Url string ImgArr []string } var catalog []*Catalog
1. 漫画首页
第一步是爬取漫画的首页,通过获取目录内容去获取到章节的标题以及该章节的地址。比如一人之下 ,目录存放在 id="play_0"
下的无序列表中,获取对应 li
标签内 a
标签内容即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 c := colly.NewCollector() c.OnHTML("#play_0" , func (e *colly.HTMLElement) { e.ForEach("ul li a" , func (i int , element *colly.HTMLElement) { href := element.Attr("href" ) title := element.Text title = coverGBKToUTF8(title) catalog = append (catalog, &Catalog{Url: PF + href, Title: title}) }) }) c.OnRequest(func (r *colly.Request) { }) c.Visit(MANHUA + strconv.Itoa(Mid))
扑飞漫画整站的编码是 gb2312
,因为go只认 utf-8
,所以对于爬取的内容需要处理下。
1 2 3 4 5 import "github.com/axgle/mahonia" func coverGBKToUTF8 (src string ) string { return mahonia.NewDecoder("gbk" ).ConvertString(src) }
2. 获取图片地址
通过上面的代码,我们已经获取了所有目录对应地址,接下来需要做的只是去到对应页面,获取JS代码并执行,获得该章节下所有的图片地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func GetImgArr (catalog *Catalog) { c := colly.NewCollector() c.OnHTML("head script" , func (e *colly.HTMLElement) { if e.Text != "" { JavaScript := coverGBKToUTF8(e.Text) JavaScript += " function f() {return photosr;} f();" vm := otto.New() value, err := vm.Run(JavaScript) if err != nil { log.Println("解析图片地址失败:" , err) } imgStr, err := value.ToString() if err != nil { log.Println("图片地址解析出错:" , err) } imgArr := strings.Split(imgStr, "," ) catalog.ImgArr = imgArr[1 :] } }) c.OnRequest(func (r *colly.Request) { }) c.Visit(catalog.Url) }
3. 下载图片
这个网上一搜图片一大堆,所以直接copy一份就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func CreateFileGetImg (catalog *Catalog, index int ) { if catalog.Title == "通知" { return } dir := DIR + strconv.Itoa(index) + "--" + catalog.Title err := os.MkdirAll(dir, os.ModePerm) if err != nil { log.Println("创建文件夹失败" ) } for k, v := range catalog.ImgArr { resp, err := http.Get(ImgHeader + v) if err != nil { log.Println(err) } body, _ := ioutil.ReadAll(resp.Body) out, _ := os.Create(dir + "/" + strconv.Itoa(k+1 ) + ".jpg" ) io.Copy(out, bytes.NewReader(body)) resp.Body.Close() }
4. 细节处理
在获取图片地址并下载图片的时候,可能会出现类似 Get http://res.img.fffmanhua.com/2017/08/22/20/28acd5236d.jpg: EOF
的错误,我处理的方式是再获取一次,知道它不报错为止。
1 2 3 4 5 6 GetImage: resp, err := http.Get(ImgHeader + v) if err != nil { log.Println(err) goto GetImage }
5. 并发
上述代码已经可以完成我的程序需求了,但是面对漫画量很大的情况下,执行的时间还是很吃力的,所以需要加上并发。一开始的考虑是在获取章节的时候直接并发,顺道把图片也下载了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var wg sync.WaitGroup for k, v := range catalog { wg.Add(1 ) go func (v *Catalog, k int ) { GetImgArr(v) if runtime.NumGoroutine() > MaxNum { MaxNum = runtime.NumGoroutine() } CreateFileGetImg(v, len (catalog)-k) wg.Done() }(v, k) } wg.Wait()
我在编辑器Goland里面运行的时候是没有问题的,但是当我编译后拿去Windows和Mac里运行就会出一大堆的问题,最后只能限制一下并发量的问题,最终的解决办法是并发获取图片目录信息,然后做一个队列去下载图片,同时最多下载10张,这样一来,性能也能够吃得消,虽然慢一点,但是它稳。
源码
https://github.com/gooohlan/PF
后记
吐槽一下,苹果有时候真的很坑,我在通过队列下载的时候,出现了 catalog
数组中的 imgArr
长度为为空的情况,调试了很久都没用,扔给别人在Windows上就乱跑。
最后的最后附上一张成功的截图