6

前言

本实现基于kotlin语言 ,但是并不算深入

技术栈

Kotlin ,Idea ,Gradle 用Java的同学都熟悉的三件套

如何运行该工程

https://segmentfault.com/a/1190000012848680

进入正题

首先上几张效果图:

抓取界面 -基于IDEA
抓取结果

开始写代码

1. 新建工程

利用Idea新建一个kotlin工程 ,安装jsoup 和requests依赖 , 这些百度到处都是教程的过程就不再赘述.

2. 添加依赖并更新Gradle配置

compile group: 'net.dongliu', name: 'requests', version: '4.14.1'
compile group: 'org.jsoup', name: 'jsoup', version: '1.11.2'

3. 代码解析

新建三连 :
  • 建立一个文件 ,暂且取名为main.kt
  • 新建一个入口方法 main()

    fun main(args: Array<String>) {
        MeiziTu()
    }
  • 新建一个普通类 Meizitu

    class MeiziTu {
        init{
        }
    }
  • 定义一类成员属 - 根地址 : var url

        // 根地址 ,index.html 是wordpress生成的首页文件 ,可以不加
        var url = "http://www.meizitu.com/index.html"
  • 根据功能定义6个方法 ,此时类的全貌

    class Meizitu{
        
        var url = "http://www.meizitu.com/index.html"
    
        /**
         * 入口 ,启动爬虫
         */
        private fun boot(url: String) {}
        
        /**
         * 得到所有的图片并保存
         */
        private fun getImgs(detail: MutableMap.MutableEntry<String, String>) {}
        
        /**
         * 得到所有的图片并保存
         */
        private fun getImgs(detail: MutableMap.MutableEntry<String, String>) {}
        
        /**
         * 得到一页的详情链接
         */
        private fun getDetailUrl(url: String): TreeMap<String, String> {}
        
        /**
         * 获得某分类的总页数并返回每一叶的链接
         */
        private fun getPagesUrls(url: String): Set<String> {}
        
        /**
         * 获得soup对象
         */
        private fun soup(url: String): Document {}
    }
    实现这些方法 :

    实现soup()

    我们是使用jsoup解析xml ,这里主要对一个固定的Url实现返回一个经过jsoup解析的文档对象 ,方便后期的选择器使用等 .

这个方法没有很多内容 ,直接返回即可

Document 是get()方法返回值类型 ,所以soup()的返回值类型也必须是Document ,也是可以指定返回值类型为Any ,但是IDEA将会难以进行代码提示

headers 各位同学大概看的明白 ,就是在模拟UA和HOST欺骗网站, 这里不再详述

另外get()并非是指发送get()请求 ,而仅代表得到Url内容

完整代码

    private fun soup(url: String): Document {

        /**
         * 利用指定的Header链接到URL ,并拉取资源
         */
        return Jsoup.connect(url)
                .headers(mapOf("User-Agent" to "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3298.3 Safari/537.36",
                        "Host" to "www.meizitu.com"
                )).get() 
    }
实现boot()方法 ,爬虫开始

首先解释init {} ,这个代码块类似java中的构造方法{}. 我直接调用了boot()方法 ,并传递了根url进去 ,实现了外部仅需实例化类即可开始爬取工作 ,无须手动调用方法 . 并且本类所有的类方法都是私有的 ,无法从外部调用 .

其次解释一下为什么有那么多tay{}catch(){} ,主要是为了实现在每一级循环遇到异常都可以继续执行 ,而不会影响整个爬虫运作 .不在最外层捕获异常是为了不会因为某一级的异常就跳过顶级的某次循环 ,尽可能缩小异常对整个爬虫的影响

  1. 使用刚刚实现的soup方法解析根Url ,并且用select选择到所有的分类 ,如下图.

    ![clipboard.png](/img/bV1ZaL)
    ![clipboard.png](/img/bV1Zbe)
    
  2. 第一层循环 ,select 得到的结果是一个列表(list) ,利用kotlin自身的for in遍历该列表 ,这里的tag是一个Element对象 .

    使用`this.getPagesUrls(tag.attr("href"))` <该方法解析在文章的后半部分> .    
    得到该分类下所有分页的每一页链接.`tag.attr("href")` 是从tag中拿到Href属性 ,也就是tag指向的url
    ![clipboard.png](/img/bV1Zdh)
    
  3. 进入第二层循环 ,遍历所有分页链接 .和得到分页的原理差不多 ,这里是利用每一页的链接得到该页面下的所有的卡片链接

    ![clipboard.png](/img/bV1Zdo)
    
  4. 进入第三层循环 ,原理和上两楼差不多 ,遍历所有的卡片链接 ,进入卡片详情使用this.getImgs(detail)<该方法解析在文章的后半部分> 命名和保存所有的图片 .

    ![clipboard.png](/img/bV1ZdJ)
    

完整代码

    init {
        this.boot(this.url)
    }
    
    private fun boot(url: String) {

        // 拿到首页的HTML
        val html = this.soup(url)

        // 得到所有的标签 ,也就是分类
        val tags = html.select(".tags a")

        /**
         * 忽略一切异常 ,遍历所有的分类并保存图片
         */
        for (tag in tags) {
            try {
                // 得到页数的链接
                var pagesLink = this.getPagesUrls(tag.attr("href"))
                // 遍历每一页
                for (pageLink in pagesLink) {
                    try {
                        // 得到所有的链接并遍历和保存
                        var detailUrls = this.getDetailUrl(pageLink)
                        for (detail in detailUrls) {
                            try {
                                // 保存图片
                                this.getImgs(detail)
                            } catch (e: Exception) {
                                continue
                            }
                        }
                    } catch (e: Exception) {
                        continue
                    }
                }
            } catch (e: Exception) {
                continue
            }
        }
    }
抓取链接方法之 getPagesUrls() getDetailUrl() 解析
  1. 首先是getPagesUrls(tag.attr("href")) ,利用每一个分类的链接得到该分类下每一页的链接

    > 经过观察我们可以发现 :每一页的链接都是由`http://www.meizitu.com/a/分类名_页码.html`构成 ,并且几乎所有的`分类名_页码.html`都可以从元素中取到 ,于是我们就可以开始伟大的字符串拼接之旅了.
    
    ![clipboard.png](/img/bV1ZeT) 
    
    ![clipboard.png](/img/bV1ZeW)
    
  2. 我们选中 #wp_page_numbers li a 这个选择器 ,遍历之 ,拼接上baseUrl丢到结果集中 , 只有2点需要特殊说明 :

    * 我们不需要下一页 ,首页 ... 这种 ,遍历时碰到他们就跳过不管.
    * 看看结果好像没有第一页的样子 ,观察之 ,发现第一页没有链接 ,但是经过楼主测试 ,使用`分类名_1.html` 即可 ,然后我们截掉链接的后6位拼上去1.html ,第一页的链接就出来了 ,有人会说楼主脑子一定是进水了 ,这样会插入很多次同样的链接 .不不不 ,最后返回时的toSet()已经帮忙去重了.
       ![clipboard.png](/img/bV1ZfF)
      
  3. ggetDetailUrl(pageLink) 除了结果集多了个标题 ,其他几乎一毛一样 ,不做赘述.

完整代码

private fun getPagesUrls(url: String): Set<String> {

        // 结果集
        var res: MutableList<String> = mutableListOf()

        // 根URL
        var baseUrl = "http://www.meizitu.com/a/"

        // 拿到所有的页数
        var pages = this.soup(url).select("#wp_page_numbers li a")

        for (page in pages) {

            // 过滤无用的链接
            if (page.attr("href") == "下一页" || page.attr("href") == "末页" ||                     page.attr("href") == "首页") {
                continue
            }

            // 插入第一页的链接
            res.add(baseUrl + page.attr("href").substring(0, page.attr("href").length - 6) + "1.html")

            // 添加URL到结果集
            res.add(baseUrl + page.attr("href"))
        }
        // URL去重
        return res.toSet()
    }
抓取和保存图片 getImgs() 解析

此时我们已经得到这一页的标题和链接 ,MutableMap.MutableEntry<String, String> 是红黑树的表结果数据中的一个 ,简单的地说就是字典或字面量对象中的一对key和value.

选中所有的图片 ,多次用到 ,不做赘述

楼主选择把土派年保存到/opt/imgs/$title ,$title 来自 getDetailUrl(pageLink)的返回值.
此时可以确定 ,目录一定不存在 ,创建之 File(filePath).mkdir()

老套路 ,对所有选中的图片对象遍历之 ,这里是使用requests进行文件存储 ,

完整代码

// 解析地址
Requests.get(img.attr("src"))
// 发送
.send()
// 返回类型是个文件 ,保存到指定的位置 : 为了避免文件名重复相互覆盖 ,加入自增变量flag作为文件名.
.toFileResponse(File(filePath + "/" + flag.toString() + ".jpg").toPath())

效果如下 :
clipboard.png

完整代码

private fun getImgs(detail: MutableMap.MutableEntry<String, String>) {

        // 选中所有图片
        var imgs = this.soup(detail.value).select("#picture img")

        // 以标题为目录名
        var filePath = "/opt/imgs/" + detail.key

        // 建立文件夹
        File(filePath).mkdir()

        // 声明一个FLAG ,用于图片名
        var flag = 0
        for (img in imgs) {

            // 利用Requests保存图片
            Requests.get(img.attr("src"))
                    .send()
                    .toFileResponse(File(filePath + "/" + flag.toString() + ".jpg").toPath())

            // 保存一张 ,给flag++
            flag++

            // 打印提示到控制台
            println("""${detail.key} 中的第 ${flag} 张妹子图 : ${img.attr("src")}""".trimIndent())
        }
    }

全剧终


Alex
112 声望11 粉丝