Java 网络爬虫下载壁纸

202 阅读7分钟

概念原则

网络爬虫:是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。

Robots协议:也称为爬虫协议、爬虫规则、机器人协议等,是一种网站国际互联网界通行的道德规范,其目的是保护网站数据和敏感信息、确保用户个人信息和隐私不被侵犯。

Robots协议实现一般是一个robots.txt文件,通常放在网站的根目录下,如掘金的robots协议:

Pasted image 20230907212909.png

User-agent:*表示针对所有爬虫。

Disallow 表示这些路径下的所有资源是禁止爬取的。

Sitemap 表示站点地图,允许网络爬虫访问,提高SEO。

爬虫原则

  • 遵守法律法规,不要侵犯他人的商业利益,dddd。
  • 遵守robots.txt中的规则内容,不随意爬取禁止目录内容。
  • 避免攻击网站,如实现脚本的请求速度要在合理的间隔等。

Wallhaven壁纸网站分析

Awesome Wallpapers - wallhaven.cc 是一个免费高清的壁纸网站。

首先访问该网站的robots.txt

Pasted image 20230907225222.png

User-agent: * 
Disallow:

查阅资料得:在robots.txt文件中,单独的"Disallow:"本身没有具体的含义。它需要跟具体的路径来指定哪些页面不能被抓取。 所以该网站没有不可爬取的网页。

其次,本文以wallhaven.cc动漫女孩网页该网页地址作为首页进行分析,如下图:

Pasted image 20230905213149.png

可以得知,这里是一个图片列表,网址中的page属性作为分页,每页有大概24张图片,每张图链接都藏在<a class="preview" href="https://wallhaven.cc/w/281d5y" target="_blank"></a>这样的标签的格式中。

接着,点击图片进入图片的信息页中并分析该页面,如下图:

Pasted image 20230905213759.png Pasted image 20230905222108.png

从图中可知图片链接藏于<img id="wallpaper" src="https://w.wallhaven.cc/full/28/wallhaven-标签的src属性中,颜色藏于<li class="color" style="background-color:#cc3333"><a href="https://wallhaven.cc/search?colors=cc3333"></a></li>标签中。

最后得出代码实现步骤:

  1. 访问有列表图片的网页的地址。
  2. 解析地址文本内容(程序html页面返回的是一个字符串),获取当前page的24张壁纸信息链接。
  3. 循环访问每张壁纸的信息链接html。
  4. 解析信息链接html,获取壁纸的下载地址和壁纸的颜色属性。
  5. 通过下载地址(这是一个二进制流)访问壁纸并保存在本地。

整个流程只有GET请求,没有其他复杂的操作,如代理、表单操作等,实现比较简单。

实现脚本

项目结构

本文的项目结构为双层,没什么特别含义,单层的项目,源码部分内容需要调整一下。

Pasted image 20230907222139.png

正则知识点

源码中的正则表达式非常简单

[\\w/]+ // 匹配A-Z,a-z,_,/,0-9其中任意一个字符,数量1个到多个。
\s+ // 匹配一个或多个空白符
\s* // 匹配零个或多个空白符
(https://w.wallhaven.cc/|https://wallhaven.cc/) // 匹配https://w.wallhaven.cc/或https://wallhaven.cc/其中一个
\\. // 匹配`.`符号,不做正则规则
 // 其余类似...

Java中的正则相关类中方法

Pattern: 是Java 的 java.util.regex 包中的一个核心类,它用于编译正则表达式.

  • compile(String regex): 将给定的正则表达式编译为模式。
  • matcher(CharSequence input)

Matcher:  Java 的 java.util.regex 包中的另一个核心类,它用于对给定的字符串进行正则表达式的匹配操作

  • find(): 尝试查找与模式匹配的输入序列的下一个子序列。该方法从该匹配器区域的开头开始,或者,如果该方法的上一次调用成功,并且匹配器此后没有重置,则从上一次匹配未匹配的第一个字符开始。
  • group(): 返回与最近一次匹配的子串.

源码

注意点:

  • JDK版本17
  • 文件的存放路径为:F:\\Picture\\Camera Roll
  • 需要新建一个空白的 /webcrawler/crawler.properties
  • resourcePath路径需要自行调整
package com.silvergravel.webcrawler;  
  
import java.io.*;  
import java.net.URL;  
import java.net.URLConnection;  
import java.util.ArrayList;  
import java.util.List;  
import java.util.Map;  
import java.util.Properties;  
import java.util.concurrent.TimeUnit;  
import java.util.logging.Level;  
import java.util.logging.Logger;  
import java.util.regex.Matcher;  
import java.util.regex.Pattern;  
import java.util.stream.Collectors;  
  
/**  
 * @author DawnStar  
 * Date: 2023/9/5 */public class WebCrawlerService {  
  
    private final Logger logger = Logger.getLogger("webcrawler");  
  
    private final static String CRAWLER_PROPERTIES_PATH = File.separator  
            + "webcrawler" + File.separator + "crawler.properties";  
  
    /**  
     * 警告:对于本项目所在位置为: java-skill-learn/java-foundation  
     * 所以需要在 user.dir之后加上 /java-foundation  
     */    private final String resourcePath = System.getProperty("user.dir")  
            + File.separator + "java-foundation"  
            + File.separator + "src"  
            + File.separator + "main"  
            + File.separator + "resources";  
  
    private final String urlPrefix = "(https://w.wallhaven.cc/|https://wallhaven.cc/)";  
  
    /**  
     * <a class="preview" href="https://wallhaven.cc/w/281d5y" target="_blank"></a>     * href中的w/281d5y使用正则 [\\w/]+  
     * 由于每个链接的空格符的数量不太一致,所以使用 \\s+匹配空格符  
     */  
    private final String pictureInfoUrlRegex = "<a\s+class=\"preview\"\s+href=\"" +  
            urlPrefix + "[\\w/]+\"\s+target=\"_blank\"\s*></a>";  
    private final Pattern picturePattern = Pattern.compile(pictureInfoUrlRegex);  
  
    /**  
     * <a class="preview" href="https://wallhaven.cc/w/281d5y" target="_blank"></a>     * 只要图片链接路径 即:https://wallhaven.cc/w/281d5y  
     */    private final String imgInfoLinkRegex = urlPrefix + "[\\w/-]+";  
    private final Pattern imgInfoLinkPattern = Pattern.compile(imgInfoLinkRegex);  
  
  
    /**  
     * 16进制 颜色匹配正则  
     */  
    private final String colorRegex = "#[A-z0-9]+";  
    private final Pattern colorListPattern = Pattern.compile(colorRegex);  
  
  
    /**  
     * <li class="color" style="background-color:#999999"><a href="https://wallhaven.cc/search?colors=999999"></a></li>     * 只需要匹配 <li class="color" style="background-color:#999999"> 取出颜色16进制值  
     */  
    private final String colorInfoRegex = "<li\s+class=\"color\"\s+style=\"background-color:" + colorRegex + "\"\s*>";  
    private final Pattern colorPattern = Pattern.compile(colorInfoRegex);  
  
  
    private final String downloadLinkRegex = urlPrefix + "[\\w/-]+\\.(png|jpg|jpeg|gif)";  
    private final Pattern downloadPattern = Pattern.compile(downloadLinkRegex);  
  
    /**  
     * <img id="wallpaper" src="https://w.wallhaven.cc/full/28/wallhaven-281d5y.png">     * 匹配上述路径  
     */  
    private final String imgRegex = "<img\s+id=\"wallpaper\"\s+src=\"" + downloadLinkRegex;  
    private final Pattern imgPattern = Pattern.compile(imgRegex);  
  
  
    public static void main(String[] args) throws Exception {  
        System.out.println(System.getProperty("user.dir"));  
        String targetWebsiteUrl = "https://wallhaven.cc/search?q=anime%20girl&categories=111" +  
                "&purity=100" +  
                "&resolutions=1920x1080%2C2560x1440" +  
                "&ratios=16x9" +  
                "&sorting=favorites" +  
                "&order=desc" +  
                "&ai_art_filter=1" +  
                "&page=";  
        WebCrawlerService webCrawlerService = new WebCrawlerService();  
        webCrawlerService.setup(targetWebsiteUrl, 1);  
    }  
    public void setup(String websiteUrl, int page) throws Exception {  
        if (page <= 0) {  
            page = 1;  
        }        logger.log(Level.INFO, "分析网页:" + websiteUrl);  
        // 如果不需要写入文件注释掉相关 Properties即可  
        InputStream resourceAsStream = WebCrawlerService.class.getClassLoader()  
                .getResourceAsStream(CRAWLER_PROPERTIES_PATH);  
        Properties properties = new Properties();  
        String lastSectionUrl = "lastSectionUrl";  
        String lastDownLoadPage = "lastDownLoadPage";  
        properties.load(resourceAsStream);  
        Object o = properties.get(lastSectionUrl);  
        if (o != null) {  
            // 如果相同则检查页数是否比上次页数大1页  
            if (String.valueOf(o).equals(websiteUrl)) {  
                int lastPage = Integer.parseInt(String.valueOf(properties.get(lastDownLoadPage)));  
                if (page != lastPage + 1) {  
                    page = lastPage + 1;  
                }            }        }        // 解析列表主页,获取每个图片的信息路径  
        List<String> imgLinks = parseListPageHtml(websiteUrl + page);  
        // 访问图片信息页,并生成指定对象列表  
        List<Picture> pictures = accessLinksAndCreatePictures(imgLinks);  
        // 综合分析 color的值  
        analysisColors(pictures);  
        // 下载图片  
        List<String> downloadLinks = pictures.stream().map(Picture::getDownloadUrl).toList();  
        String path = "F:\\Picture\\Camera Roll";  
        boolean finish = downloadImgBatch(downloadLinks, path);  
        if (finish) {  
            properties.put(lastSectionUrl, websiteUrl);  
            properties.put(lastDownLoadPage, String.valueOf(page));  
            storeProperties(properties);  
        }    }  
    private void storeProperties(Properties properties) throws Exception {  
        // 下载完成之后存入properties  
        File file = new File(resourcePath + CRAWLER_PROPERTIES_PATH);  
        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {  
            properties.store(fileOutputStream, "修改原始resources的属性文件");  
            logger.log(Level.INFO, "原始文件存储路径:" + file.getAbsolutePath());  
  
        }        URL resource = WebCrawlerService.class.getClassLoader().getResource(CRAWLER_PROPERTIES_PATH);  
        if (resource == null) {  
            logger.log(Level.WARNING, "crawler.properties 文件不存在!");  
        } else {  
            logger.log(Level.INFO, "文件存储路径:" + resource.getFile());  
            try (FileOutputStream fileOutputStream = new FileOutputStream(resource.getFile())) {  
                properties.store(fileOutputStream, "修改target文件夹中的resources的属性文件");  
            }        }        logger.log(Level.INFO, "下载完成!");  
    }  
    private void analysisColors(List<Picture> pictures) {  
        // 分析颜色的占比  
        Map<String, Integer> colorNameCountMap = pictures.stream()  
                .flatMap(picture -> picture.colors.stream())  
                .collect(Collectors.groupingBy(String::toString, Collectors.summingInt(value -> 1)));  
        long total = pictures.stream()  
                .mapToLong(picture -> picture.colors.size()).sum();  
        colorNameCountMap.forEach(  
                (key, value) -> {  
                    System.out.println(key + "数量占比:" + value + "/" + total);  
                }        );    }  
    private BufferedReader openReader(String sourceUrl) {  
        try {  
            URL url = new URL(sourceUrl);  
            URLConnection urlConnection = url.openConnection();  
            return new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }    }  
    private List<String> parseListPageHtml(String websiteUrl) {  
        try (BufferedReader reader = openReader(websiteUrl)) {  
            List<String> htmlContents = reader.lines().toList();  
            List<String> imgLinks = new ArrayList<>();  
            for (String htmlContent : htmlContents) {  
                // 检索含有图片链接的标签  
                Matcher matcher = picturePattern.matcher(htmlContent);  
                while (matcher.find()) {  
                    String group = matcher.group();  
                    Matcher imgLinkMatcher = imgInfoLinkPattern.matcher(group);  
                    // 检索出符合条件的 图片信息链接  
                    while (imgLinkMatcher.find()) {  
                        imgLinks.add(imgLinkMatcher.group());  
                    }                }            }            return imgLinks;  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }    }  
    private List<Picture> accessLinksAndCreatePictures(List<String> links) {  
        List<Picture> pictures = new ArrayList<>(links.size());  
        try {  
            for (String link : links) {  
                // 暂停 500 毫秒,减少服务器的压力,避免被禁止访问站点  
                TimeUnit.MILLISECONDS.sleep(500);  
                Picture picture = new Picture();  
                picture.setInfoUrl(link);  
                BufferedReader reader = openReader(link);  
                List<String> contents = reader.lines().toList();  
                for (String content : contents) {  
                    // 访问图片信息,并解析获取其下载链接以及颜色属性  
                    Matcher matcher = imgPattern.matcher(content);  
                    if (matcher.find()) {  
                        Matcher downloadMatch = downloadPattern.matcher(matcher.group());  
                        if (downloadMatch.find()) {  
                            String downloadUrl = downloadMatch.group();  
                            picture.setDownloadUrl(downloadUrl);  
                        }                    }                    // 解析colors  
                    List<String> colors = parseColor(content);  
                    picture.setColors(colors);  
                }                pictures.add(picture);  
                reader.close();  
  
            }            return pictures;  
        } catch (Exception e) {  
            e.printStackTrace();  
            return pictures;  
        }    }  
    private List<String> parseColor(String content) {  
        Matcher matcher = colorPattern.matcher(content);  
        List<String> colors = new ArrayList<>();  
        while (matcher.find()) {  
            Matcher colorMatch = colorListPattern.matcher(matcher.group());  
            while (colorMatch.find()) {  
                colors.add(colorMatch.group());  
            }        }        return colors;  
    }  
    private boolean downloadImgBatch(List<String> downloadUrls, String path) {  
        try {  
            for (int i = 0; i < downloadUrls.size(); i++) {  
                String downloadUrl = downloadUrls.get(i);  
                if (downloadUrl == null) {  
                    continue;  
                }                String imgName = downloadUrl.substring(downloadUrl.lastIndexOf("/") + 1);  
                logger.log(Level.INFO, "本次批量下载第 " + (i + 1) + " 张图片链接: " + downloadUrl + " 执行下载!");  
                URL url = new URL(downloadUrl);  
                FileOutputStream fileOutputStream = new FileOutputStream(path + "/" + imgName);  
                InputStream inputStream = url.openConnection().getInputStream();  
                fileOutputStream.write(inputStream.readAllBytes());  
                inputStream.close();  
                fileOutputStream.close();  
                logger.log(Level.INFO, imgName + " 下载成功!");  
                TimeUnit.MILLISECONDS.sleep(200);  
            }            return true;  
        } catch (Exception e) {  
            e.printStackTrace();  
            return false;  
        }    }  
  
    private static class Picture {  
        /**  
         * 图片主要颜色列表  
         */  
        private List<String> colors;  
        /**  
         * 图片下载路径  
         */  
        private String downloadUrl;  
        /**  
         * 图片的主页路径  
         */  
        private String infoUrl;  
  
        public Picture() {  
        }  
        public Picture(List<String> colors, String downloadUrl, String infoUrl) {  
            this.colors = colors;  
            this.downloadUrl = downloadUrl;  
            this.infoUrl = infoUrl;  
        }  
        public List<String> getColors() {  
            return colors;  
        }  
        public void setColors(List<String> colors) {  
            this.colors = colors;  
        }  
        public String getDownloadUrl() {  
            return downloadUrl;  
        }  
        public void setDownloadUrl(String downloadUrl) {  
            this.downloadUrl = downloadUrl;  
        }  
        public String getInfoUrl() {  
            return infoUrl;  
        }  
        public void setInfoUrl(String infoUrl) {  
            this.infoUrl = infoUrl;  
        }    }  
}

效果图

Pasted image 20230908005137.png

参考文档

正则表达式 – 教程 | 菜鸟教程 (runoob.com)