概念原则
网络爬虫:是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。
Robots协议:也称为爬虫协议、爬虫规则、机器人协议等,是一种网站国际互联网界通行的道德规范,其目的是保护网站数据和敏感信息、确保用户个人信息和隐私不被侵犯。
Robots协议实现一般是一个robots.txt文件,通常放在网站的根目录下,如掘金的robots协议:
User-agent:*表示针对所有爬虫。
Disallow 表示这些路径下的所有资源是禁止爬取的。
Sitemap 表示站点地图,允许网络爬虫访问,提高SEO。
爬虫原则:
- 遵守法律法规,不要侵犯他人的商业利益,dddd。
- 遵守
robots.txt中的规则内容,不随意爬取禁止目录内容。 - 避免攻击网站,如实现脚本的请求速度要在合理的间隔等。
Wallhaven壁纸网站分析
Awesome Wallpapers - wallhaven.cc 是一个免费高清的壁纸网站。
首先访问该网站的robots.txt
User-agent: *
Disallow:
查阅资料得:在robots.txt文件中,单独的"Disallow:"本身没有具体的含义。它需要跟具体的路径来指定哪些页面不能被抓取。 所以该网站没有不可爬取的网页。
其次,本文以wallhaven.cc动漫女孩网页该网页地址作为首页进行分析,如下图:
可以得知,这里是一个图片列表,网址中的page属性作为分页,每页有大概24张图片,每张图链接都藏在<a class="preview" href="https://wallhaven.cc/w/281d5y" target="_blank"></a>这样的标签的格式中。
接着,点击图片进入图片的信息页中并分析该页面,如下图:
从图中可知图片链接藏于<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>标签中。
最后得出代码实现步骤:
- 访问有列表图片的网页的地址。
- 解析地址文本内容(程序html页面返回的是一个字符串),获取当前page的24张壁纸信息链接。
- 循环访问每张壁纸的信息链接html。
- 解析信息链接html,获取壁纸的下载地址和壁纸的颜色属性。
- 通过下载地址(这是一个二进制流)访问壁纸并保存在本地。
整个流程只有GET请求,没有其他复杂的操作,如代理、表单操作等,实现比较简单。
实现脚本
项目结构
本文的项目结构为双层,没什么特别含义,单层的项目,源码部分内容需要调整一下。
正则知识点
源码中的正则表达式非常简单
[\\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;
} }
}