java学习快到碗里来Java 核心技术Java 进阶

WebMagic爬取新年图片

2022-01-05  本文已影响0人  我犟不过你

虎年就快到了,最近有没有想要做ppt,写总结缺少素材的小伙伴?苦于没有背景素材啊,我来教你一招,爬取大量春节图片,助你在工作生活中春节气氛满满,虎虎生风啊~~

首先说明啊,我爬取的都是免费网站,并且绝不会用作商用的啊,同学们一定要有版权意识啊。

推荐个免费的网站:

下面开始步入正文。

一、认识webmagic

本文使用java开发,webmagic作为爬虫工具,简单快捷,下面简单认识下什么是webmagic:

官方文档:http://webmagic.io/

1.1 简介

WebMagic的设计参考了业界最优秀的爬虫Scrapy,而实现则应用了HttpClient、Jsoup等Java世界最成熟的工具。

此项目分为两个模块:

所以我们在使用时,要使用全部功能,需要引用如下两个组件:

        <!--WebMagic-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.5</version>
<!--            <exclusions>-->
<!--                <exclusion>-->
<!--                    <groupId>org.slf4j</groupId>-->
<!--                    <artifactId>slf4j-log4j12</artifactId>-->
<!--                </exclusion>-->
<!--            </exclusions>-->
        </dependency>
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.5</version>
        </dependency>

在上面的pom中,注释部分去除了slf4j-log4j12的依赖,因为这里的依赖与我的springboot项目冲突了。如果发现有相关的报错,可以像我一样处理。

1.2 架构

WebMagic的结构分为四大组件:

还有一个引擎:

源自官方的架构图如下:

webmagic.png

1.3 数据流转对象

Request是对URL地址的一层封装,一个Request对应一个URL地址。

它是PageProcessor与Downloader交互的载体,也是PageProcessor控制Downloader唯一方式。

除了URL本身外,它还包含一个Key-Value结构的字段extras。你可以在extra中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。

Page代表了从Downloader下载到的一个页面(可能是HTML,也可能是JSON或者其他文本格式的内容)。

Page是WebMagic抽取过程的核心对象。后面会在示例中使用常用的方法。

ResultItems相当于一个Map,它保存PageProcessor处理的结果,供Pipeline使用。

它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则不应被Pipeline处理。

1.4 爬虫引擎 - Spider

Spider是webmagic的核心组件,其包含了上述介绍的四大组件,同时提供手动设置属性的方法。

除此之外,Spider也是整个爬虫的创建、启动、终止的控制器。

官方实例如下,含义都在注释当中:

    public static void main(String[] args) {
        Spider.create(new VulnPageProcessor())
                //从https://github.com/code4craft开始抓    
                .addUrl("https://github.com/code4craft")
                //设置Scheduler,使用Redis来管理URL队列
                .setScheduler(new RedisScheduler("localhost"))
                //设置Pipeline,将结果以json方式保存到文件
                .addPipeline(new JsonFilePipeline("D:\\data\\webmagic"))
                //开启5个线程同时执行,此处不能小于2
                .thread(5)
                //启动爬虫
                .run();
    }

二、动手爬取新年图片

2.1 添加maven依赖

上面已经介绍过了,直接添加maven依赖

        <!--WebMagic-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.5</version>
<!--            <exclusions>-->
<!--                <exclusion>-->
<!--                    <groupId>org.slf4j</groupId>-->
<!--                    <artifactId>slf4j-log4j12</artifactId>-->
<!--                </exclusion>-->
<!--            </exclusions>-->
        </dependency>
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.5</version>
        </dependency>

2.2 定制PageProcessor

2.2.1 获取图片地址

我们想要处理爬取到的网站内容,就需要实现自己的PageProcessor,如下所示:

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;

/**
 * @description: 新年图片处理器
 * @author:weirx
 * @date:2022/1/5 14:45
 * @version:3.0
 */
public class ChineseNewYearImgPageProcessor implements PageProcessor {
    @Override
    public void process(Page page) {
        
    }

    @Override
    public Site getSite() {
        return null;
    }
}

如上所示,有两个方法需要实现:

    private Site site = Site.me()
            // 重试次数
            .setRetryTimes(3)
            //编码
            .setCharset(StandardCharsets.UTF_8.name())
            // 超时时间
            .setTimeOut(1000)
            // 休眠时间
            .setSleepTime(1000); 
  
    @Override
    public Site getSite() {
        return site;
    }

我们看下页面源码的样式组成,我们根据图上画圈的标签样式表获取元素,img内的就是我们需要的图片:

image.png

具体实现,详细解释看注释:

    @Override
    public void process(Page page) {
        //获取当前页面的所有满足条件的img
        List<String> srcset = page.getHtml()
                //在元素div class = kG7WW下的内容
                .css("div.kG7WW")
                //在上一层基础上,查找div.VQW0y下的img标签,获取img标签的srcset属性
                .css("div.VQW0y > img", "srcset")
                // 获取符合条件的所有元素,返回List<String>
                //注意:此处是所有的img,并非第一个,取第一个使用get()
                .all();

        // 根据当前网页的内容,需要去重图片路径,此处直接使用list
        Set distinct =new HashSet<>();
        // java8 流式 并发遍历
        srcset.parallelStream().forEach(s -> {
            // 数据处理,截取我们需要的数据
            distinct.add(s.substring(0, StringUtils.indexOf(s,"?")));
        });
        //打印结果
        distinct.forEach(System.out::println);
    }

好了,这样我们就完成一个PageProcessor的编写,最后我们写一个main方法,使用Spider启动这个爬虫:

    public static void main(String[] args) {
        Spider.create(new ChineseNewYearImgPageProcessor())
                //此处从收集中国年(chinese new year)的页面作为首页
                .addUrl("https://unsplash.com/s/photos/chinese-new-year")
                //开启5个线程同时执行,此处不能小于2
                .thread(5)
                //启动爬虫
                .run();
    }

启动看结果如下,将图片的地址都打印出来了:

2022-01-05 15:41:30.681 [springAppName_IS_UNDEFINED: N/A] [pool-1-thread-1] INFO u.c.webmagic.downloader.HttpClientDownloader - downloading page success https://unsplash.com/s/photos/chinese-new-year
https://images.unsplash.com/photo-1581792408272-a9227ce6b363
https://images.unsplash.com/photo-1601402420504-a3f1b910679e
https://images.unsplash.com/photo-1578073273382-f847b29d2192
https://images.unsplash.com/photo-1517315029683-c6497375b3da
https://images.unsplash.com/photo-1517009832553-cfcd067adf81
https://images.unsplash.com/photo-1612201598945-f66a763965bd
https://images.unsplash.com/photo-1597533456003-27f3ce4d0d62
https://images.unsplash.com/photo-1579626362137-b6d68a1ebba6
https://images.unsplash.com/photo-1549767742-ccfdeb07b71d
https://images.unsplash.com/photo-1541379889336-70f26e4c4617
https://images.unsplash.com/photo-1587133966114-7f69b8803e1b
https://images.unsplash.com/photo-1580524765545-f159d96bb9a9
https://images.unsplash.com/photo-1589803196808-b395a6f32a9a
https://images.unsplash.com/photo-1600582201908-183d607504c3
https://images.unsplash.com/photo-1565457210787-a4e17b40f04e
https://images.unsplash.com/photo-1612201578303-25f1712d20cc
https://images.unsplash.com/photo-1544032745-e96acb1caa9c
https://images.unsplash.com/photo-1518894347072-3bfedb006095
https://images.unsplash.com/photo-1571306130639-22a185b29822
https://images.unsplash.com/photo-1587314857323-5e93cc3d718e
get page: https://unsplash.com/s/photos/chinese-new-year
2022-01-05 15:41:31.901 [springAppName_IS_UNDEFINED: N/A] [main] INFO us.codecraft.webmagic.Spider - Spider unsplash.com closed! 1 pages downloaded.

支持我们已经拿到需要的图片地址了。迈入了成功的第一步。

2.2.2 获取下一页

在获取了首页的内容后,我们尝试获取下一页更多的图片。

这个网站的分页按钮叫做【Load more photos】,所以我们需要在网页的内容当中,获取到这个按钮所在的标签,并获取到其跳转的地址:

       <div class="gDCZZ">
        <button type="button" class="CwMIr DQBsa p1cWU jpBZ0 AYOsT Olora I0aPD dEcXu">Load more photos</button>
       </div>

发现这个标签并没有地址,我前端有很水,所以直接在浏览器控制台找到如下分页地址:

https://unsplash.com/napi/search/photos?query=chinese%20new%20year&per_page=20&page=2&xp=

我们每查一次就手动将页数加1好了。

说句题外话,我们都拿到了api接口,其实也就不需要爬虫了对吧,但是爬虫才是我们本篇文章学习重点,所以继续搞。

webmagic也支持json的解析,而鉴于上面的原因我们就需要使用json的方式了,因为此网页点击下一页按钮后,并不是整体页面的跳转,是通过返回数据驱动视图变化,所以我们需要解析接口返回的api数据了。

接下来我们对下一页的代码进行完善,如下所示:

    /**
     * 定义一个全局页数,方便下次获取下一页
     *
     * 此处使用AtomicInteger是为了防止多线程造成数据重复,webmagic在获取页时是多线程的
     */
    private AtomicInteger pageNum = new AtomicInteger(2);
        // 我们看是否有下一页的内容,有的话就将下一页也添加到url管理容器,
        //https://unsplash.com/napi/search/photos?query=chinese%20new%20year&per_page=20&page=2&xp="
        String url = "https://unsplash.com/napi/search/photos?query=chinese%20new%20year&per_page=20" + "page=" + pageNum;
        List<String> nextPage = new ArrayList<>();
        nextPage.add(url);
        page.addTargetRequests(nextPage);
        // 页数+1
        pageNum.incrementAndGet();

启动,第二次download时发现page的html是空的,但是rawText正是我们需要的数据:

json

下面我们就要解析这个json数据了,需要判断是json还是html,完整的process方法如下:

    @Override
    public void process(Page page) {
        //获取当前页面的所有满足条件的img
        List<String> srcset = page.getHtml()
                //在元素div class = kG7WW下的内容
                .css("div.kG7WW")
                //在上一层基础上,查找div.VQW0y下的img标签,获取img标签的srcset属性
                .css("div.VQW0y > img", "srcset")
                // 获取符合条件的所有元素,返回List<String>
                //注意:此处是所有的img,并非第一个,取第一个使用get()
                .all();
        if (CollectionUtil.isEmpty(srcset)){
            JSONObject jsonObject = JSONObject.parseObject(page.getRawText());
            JSONArray results = jsonObject.getJSONArray("results");
            List<String> list = new ArrayList<>();
            results.forEach(o->{
                String url = JSONObject.parseObject(o.toString()).getJSONObject("urls").getString("raw");
                String substring = url.substring(0, StringUtils.indexOf(url, "?"));
                list.add(substring);
            });

            list.forEach(System.out::println);
        }else {
            // 根据当前网页的内容,需要去重图片路径,此处直接使用list
            Set distinct =new HashSet<>();
            // java8 流式 并发遍历
            srcset.parallelStream().forEach(s -> {
                // 数据处理,截取我们需要的数据
                distinct.add(s.substring(0, StringUtils.indexOf(s,"?")));
            });
            //打印结果
            distinct.forEach(System.out::println);
        }

        // 我们看是否有下一页的内容,有的话就将下一页也添加到url管理容器,
        //https://unsplash.com/napi/search/photos?query=chinese%20new%20year&per_page=20&page=2&xp="
        String url = "https://unsplash.com/napi/search/photos?query=chinese%20new%20year&per_page=20" + "page=" + pageNum;
        List<String> nextPage = new ArrayList<>();
        nextPage.add(url);
        page.addTargetRequests(nextPage);
        // 页数+1
        pageNum.incrementAndGet();
    }

至此,我们成功拿到所有的图片地址了,运行后将会不停的爬取。

2.3 pipeline保存

WebMagic用于保存结果的组件叫做Pipeline。其实在PageProcessor当中就可以完成数据持久化,增加Pipeline的根本原因是为了职责划分。

我们需要实现Pipeline接口,然后实现其process方法,通过ResultItems获取我们的结果,而ResultItems当中的结果是我们在PageProcessor当中处理完后使用page.putField手动添加的。

添加结果代码,因为我们有两种方式,json和html:

page.putField("jsonImg", list);
----
page.putField("htmlImg", list);

保存图片到本地,pipeline:

import cn.hutool.core.collection.CollectionUtil;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

/**
 * @description: 保存图片到文件
 * @author:weirx
 * @date:2022/1/5 17:14
 * @version:3.0
 */
public class SaveImgPipeline implements Pipeline {

    @Override
    public void process(ResultItems resultItems, Task task) {
        List<String> jsonImg = (List<String>) resultItems.getAll().get("jsonImg");
        List<String> htmlImg = (List<String>) resultItems.getAll().get("htmlImg");

        if (CollectionUtil.isNotEmpty(jsonImg)) {
            jsonImg.forEach(this::saveImg);
        }

        if (CollectionUtil.isNotEmpty(htmlImg)) {
            htmlImg.forEach(this::saveImg);
        }
    }

    /**
     * description: 保存图片到本地
     *
     * @param imgUrl
     * @return: void
     * @author: weirx
     * @time: 2022/1/5 17:46
     */
    private void saveImg(String imgUrl) {
        try {
            URL url = new URL(imgUrl);
            // 打开连接
            URLConnection con = url.openConnection();
            // 输入流
            InputStream is = con.getInputStream();
            Files.copy(is, Paths.get("C:\\Users\\P50\\Desktop\\pic\\" + System.currentTimeMillis() + ".png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

至此,我们一个简单的爬取新年图片的爬虫就完成了,看看我的成果吧:

成果

三、完整代码

代码又开始就要有结束,所以我们前面缺少了一个结束,比如我只想获取10页的图片,那么我可以增加页数的判断,当到达第十页了,就调用stop方法,关于这部分不细说了,直接附上全部的代码,总共两个bean:

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 新年图片处理器
 * @author:weirx
 * @date:2022/1/5 14:45
 * @version:3.0
 */
public class ChineseNewYearImgPageProcessor implements PageProcessor {

    /**
     * 全局spider,方便使用stop方法
     */
    private static Spider spider = Spider.create(new ChineseNewYearImgPageProcessor())
            //此处从收集中国年(chinese new year)的页面作为首页
            .addUrl("https://unsplash.com/s/photos/chinese-new-year")
            .addPipeline(new SaveImgPipeline())
            //开启5个线程同时执行,此处不能小于2
            .thread(11);

    /**
     * 定义一个全局页数,方便下次获取下一页
     * <p>
     * 此处使用AtomicInteger是为了防止多线程造成数据重复,webmagic在获取页时是多线程的
     */
    private AtomicInteger pageNum = new AtomicInteger(2);

    /**
     * 初始化Site配置
     */
    private Site site = Site.me()
            // 重试次数
            .setRetryTimes(3)
            //编码
            .setCharset(StandardCharsets.UTF_8.name())
            // 超时时间
            .setTimeOut(1000)
            // 休眠时间
            .setSleepTime(1000);

    @Override
    public void process(Page page) {
        //获取当前页面的所有满足条件的img
        List<String> srcset = page.getHtml()
                //在元素div class = kG7WW下的内容
                .css("div.kG7WW")
                //在上一层基础上,查找div.VQW0y下的img标签,获取img标签的srcset属性
                .css("div.VQW0y > img", "srcset")
                // 获取符合条件的所有元素,返回List<String>
                //注意:此处是所有的img,并非第一个,取第一个使用get()
                .all();
        if (CollectionUtil.isEmpty(srcset)) {
            JSONObject jsonObject = JSONObject.parseObject(page.getRawText());
            JSONArray results = jsonObject.getJSONArray("results");
            List<String> list = new ArrayList<>();
            results.forEach(o -> {
                String url = JSONObject.parseObject(o.toString()).getJSONObject("urls").getString("raw");
                String substring = url.substring(0, StringUtils.indexOf(url, "?"));
                list.add(substring);
            });

            page.putField("jsonImg", list);
        } else {
            // 根据当前网页的内容,需要去重图片路径,此处直接使用list
            Set distinct = new HashSet<>();
            // java8 流式 并发遍历
            srcset.parallelStream().forEach(s -> {
                // 数据处理,截取我们需要的数据
                distinct.add(s.substring(0, StringUtils.indexOf(s, "?")));
            });
            page.putField("htmlImg", new ArrayList<>(distinct));
        }

        // 我们看是否有下一页的内容,有的话就将下一页也添加到url管理容器,
        //https://unsplash.com/napi/search/photos?query=chinese%20new%20year&per_page=20&page=2&xp="
        String url = "https://unsplash.com/napi/search/photos?query=chinese%20new%20year&per_page=20" + "page=" + pageNum;
        List<String> nextPage = new ArrayList<>();
        nextPage.add(url);
        page.addTargetRequests(nextPage);
        // 页数+1
        pageNum.incrementAndGet();

        //如果已经爬取10页,则停止爬取
        if (pageNum.get() > 10) {
            spider.stop();
        }
    }

    @Override
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {
        spider.run();
    }
}
import cn.hutool.core.collection.CollectionUtil;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

/**
 * @description: 保存图片到文件
 * @author:weirx
 * @date:2022/1/5 17:14
 * @version:3.0
 */
public class SaveImgPipeline implements Pipeline {

    @Override
    public void process(ResultItems resultItems, Task task) {
        List<String> jsonImg = (List<String>) resultItems.getAll().get("jsonImg");
        List<String> htmlImg = (List<String>) resultItems.getAll().get("htmlImg");

        if (CollectionUtil.isNotEmpty(jsonImg)) {
            jsonImg.forEach(this::saveImg);
        }

        if (CollectionUtil.isNotEmpty(htmlImg)) {
            htmlImg.forEach(this::saveImg);
        }
    }

    /**
     * description: 保存图片到本地
     *
     * @param imgUrl
     * @return: void
     * @author: weirx
     * @time: 2022/1/5 17:46
     */
    private void saveImg(String imgUrl) {
        try {
            URL url = new URL(imgUrl);
            // 打开连接
            URLConnection con = url.openConnection();
            // 输入流
            InputStream is = con.getInputStream();
            Files.copy(is, Paths.get("C:\\Users\\P50\\Desktop\\pic\\" + System.currentTimeMillis() + ".png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四、扩展

4.1 集成springboot

    @Autowired
    private ChineseNewYearImgPageProcessor ChineseNewYearImgPageProcessor;
XxxxxxxxxxxImpl xxxxxxxxxxxImpl = ApplicationContextProvider.getBean(XxxxxxxxxxxImpl.class)
2022-01-06 17:03:06.355 [vuln-prioritization-tech: N/A] [pool-599-thread-11] WARN u.c.webmagic.downloader.HttpClientDownloader - download page http://www.cnnvd.org.cn/web/xxk/bdxqById.tag?id=6575 error
java.net.SocketException: 打开的文件过多
    at java.net.Socket.createImpl(Socket.java:460)
    at java.net.Socket.getImpl(Socket.java:520)
    at java.net.Socket.setSoTimeout(Socket.java:1141)
    at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:120)
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376)
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
    at us.codecraft.webmagic.downloader.HttpClientDownloader.download(HttpClientDownloader.java:83)
    at us.codecraft.webmagic.Spider.processRequest(Spider.java:419)
    at us.codecraft.webmagic.Spider.access$000(Spider.java:61)
    at us.codecraft.webmagic.Spider$1.run(Spider.java:322)
    at us.codecraft.webmagic.thread.CountableThreadPool$1.run(CountableThreadPool.java:74)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

4.2 minio保存图片

绝大多数情况下,我们的图片不会保存到本地的,要有其对应的文件服务器,我这里给大伙提供这个minio的教程,也是我前面写的文章,希望大家用得到。

关注我的minio文集,有环境搭建和springboot集成:https://www.jianshu.com/nb/51056774

五、随便说说

相信学完本篇文章,大家一定还是有点收获的吧。点赞关注啊。

关于本篇文章的所有内容,有错误或不理解,欢迎留言一起讨论。

现在2022年了,希望大家牛气满满,钱包也满满、好好学习、天天向上!!!

上一篇下一篇

猜你喜欢

热点阅读