이런 종류의 이미지 획득은 본질적으로 파일 다운로드(HttpClient)입니다. 하지만 단순히 사진을 얻는 것만이 아니기 때문에 페이지를 파싱하는 과정(Jsoup)도 있습니다.
Jsoup: HTML 페이지를 구문 분석하고 이미지에 대한 링크를 가져옵니다.
HttpClient: 이미지 링크를 요청하고 이미지를 로컬에 저장합니다.
먼저 분석을 위해 홈 페이지에 들어가면 주로 다음과 같은 카테고리가 있습니다(여기서 모든 카테고리는 아니지만 이것만으로 충분합니다. 이것은 단지 학습 기술일 뿐입니다.). 우리의 목표는 각 카테고리 아래의 정보를 얻는 것입니다. 카테고리 사진.
웹사이트의 구조를 간단하게 분석해 보겠습니다. 아래 그림은 일반적인 구조를 보여줍니다. 여기서는 설명을 위해 분류 레이블을 선택했습니다. 카테고리 탭 페이지에는 여러 제목 페이지가 포함되어 있으며 각 제목 페이지에는 여러 이미지 페이지가 포함되어 있습니다. (제목 페이지에 해당하는 수십 장의 사진)
프로젝트 종속 jar 패키지 좌표를 가져오거나 해당 jar 패키지를 직접 다운로드하거나 프로젝트를 가져올 수도 있습니다.
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.11.3</version> </dependency>
Entity 클래스: 속성을 객체로 캡슐화하여 호출을 더 쉽게 만듭니다.
package com.picture; public class Picture { private String title; private String url; public Picture(String title, String url) { this.title = title; this.url = url; } public String getTitle() { return this.title; } public String getUrl() { return this.url; } }
도구 카테고리 : 계속 바뀌는 UA (유용한지는 모르겠지만 자체 IP를 사용하다보니 거의 활용도가 없을 것 같네요)
package com.picture; public class HeaderUtil { public static String[] headers = { "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)", "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11", "Opera/9.25 (Windows NT 5.1; U; en)", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.5 (like Gecko) (Kubuntu)", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12", "Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.2.9", "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.7 (KHTML, like Gecko) Ubuntu/11.04 Chromium/16.0.912.77 Chrome/16.0.912.77 Safari/535.7", "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:10.0) Gecko/20100101 Firefox/10.0 " }; }
멀티스레딩이 정말 너무 빠르네요, 게다가 IP가 하나뿐이고 사용할 프록시 IP도 없습니다. (저는 그것에 대해 잘 모릅니다.) IP를 차단하기 위해 멀티스레딩을 사용하는 것이 매우 빠릅니다.
package com.picture; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Random; import org.apache.http.HttpEntity; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import com.m3u8.HttpClientUtil; public class SinglePictureDownloader { private String referer; private CloseableHttpClient httpClient; private Picture picture; private String filePath; public SinglePictureDownloader(Picture picture, String referer, String filePath) { this.httpClient = HttpClientUtil.getHttpClient(); this.picture = picture; this.referer = referer; this.filePath = filePath; } public void download() { HttpGet get = new HttpGet(picture.getUrl()); Random rand = new Random(); //设置请求头 get.setHeader("User-Agent", HeaderUtil.headers[rand.nextInt(HeaderUtil.headers.length)]); get.setHeader("referer", referer); System.out.println(referer); HttpEntity entity = null; try (CloseableHttpResponse response = httpClient.execute(get)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { entity = response.getEntity(); if (entity != null) { File picFile = new File(filePath, picture.getTitle()); try (OutputStream out = new BufferedOutputStream(new FileOutputStream(picFile))) { entity.writeTo(out); System.out.println("下载完毕:" + picFile.getAbsolutePath()); } } } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { //关闭实体,关于 httpClient 的关闭资源,有点不太了解。 EntityUtils.consume(entity); } catch (IOException e) { e.printStackTrace(); } } } }
자주 연결을 생성하여 성능 소모를 피하기 위해 HttpClient 연결을 얻기 위한 도구 클래스입니다. (그런데 여기서는 단일 스레드를 사용해서 크롤링을 하기 때문에 그다지 유용하지는 않습니다. HttpClient 연결 하나만 사용하면 크롤링이 가능합니다. 처음에는 여러 스레드를 사용하여 크롤링을 했기 때문이지만 기본적으로 몇 장의 사진을 얻는 것은 금지되었기 때문에 단일 스레드 크롤러로 변경되었으므로 연결 풀은 그대로 유지됩니다.)
package com.m3u8; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; public class HttpClientUtil { private static final int TIME_OUT = 10 * 1000; private static PoolingHttpClientConnectionManager pcm; //HttpClient 连接池管理类 private static RequestConfig requestConfig; static { requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(TIME_OUT) .setConnectTimeout(TIME_OUT) .setSocketTimeout(TIME_OUT).build(); pcm = new PoolingHttpClientConnectionManager(); pcm.setMaxTotal(50); pcm.setDefaultMaxPerRoute(10); //这里可能用不到这个东西。 } public static CloseableHttpClient getHttpClient() { return HttpClients.custom() .setConnectionManager(pcm) .setDefaultRequestConfig(requestConfig) .build(); } }
package com.picture; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.apache.http.HttpEntity; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; import com.m3u8.HttpClientUtil; /** * 首先从顶部分类标题开始,依次爬取每一个标题(小分页),每一个标题(大分页。) * */ public class PictureSpider { private CloseableHttpClient httpClient; private String referer; private String rootPath; private String filePath; public PictureSpider() { httpClient = HttpClientUtil.getHttpClient(); } /** * 开始爬虫爬取! * * 从爬虫队列的第一条开始,依次爬取每一条url。 * * 分页爬取:爬10页 * 每个url属于一个分类,每个分类一个文件夹 * */ public void start(List<String> urlList) { urlList.stream().forEach(url->{ this.referer = url; String dirName = url.substring(22, url.length()-1); //根据标题名字去创建目录 //创建分类目录 File path = new File("D:/DragonFile/DBC/mzt/", dirName); //硬编码路径,需要用户自己指定一个 if (!path.exists()) { path.mkdir(); rootPath = path.toString(); } for (int i = 1; i <= 10; i++) { //分页获取图片数据,简单获取几页就行了 this.page(url + "page/"+ 1); } }); } /** * 标题分页获取链接 * */ public void page(String url) { System.out.println("url:" + url); String html = this.getHtml(url); //获取页面数据 Map<String, String> picMap = this.extractTitleUrl(html); //抽取图片的url if (picMap == null) { return ; } //获取标题对应的图片页面数据 this.getPictureHtml(picMap); } private String getHtml(String url) { String html = null; HttpGet get = new HttpGet(url); get.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36"); get.setHeader("referer", url); try (CloseableHttpResponse response = httpClient.execute(get)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { HttpEntity entity = response.getEntity(); if (entity != null) { html = EntityUtils.toString(entity, "UTf-8"); //关闭实体? } } else { System.out.println(statusCode); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return html; } private Map<String, String> extractTitleUrl(String html) { if (html == null) { return null; } Document doc = Jsoup.parse(html, "UTF-8"); Elements pictures = doc.select("ul#pins > li"); //不知为何,无法直接获取 a[0],我不太懂这方面的知识。 //那我就多处理一步,这里先放下。 Elements pictureA = pictures.stream() .map(pic->pic.getElementsByTag("a").first()) .collect(Collectors.toCollection(Elements::new)); return pictureA.stream().collect(Collectors.toMap( pic->pic.getElementsByTag("img").first().attr("alt"), pic->pic.attr("href"))); } /** * 进入每一个标题的链接,再次分页获取图片的链接 * */ private void getPictureHtml(Map<String, String> picMap) { //进入标题页,在标题页中再次分页下载。 picMap.forEach((title, url)->{ //分页下载一个系列的图片,每个系列一个文件夹。 File dir = new File(rootPath, title.trim()); if (!dir.exists()) { dir.mkdir(); filePath = dir.toString(); //这个 filePath 是每一个系列图片的文件夹 } for (int i = 1; i <= 60; i++) { String html = this.getHtml(url + "/" + i); if (html == null) { //每个系列的图片一般没有那么多, //如果返回的页面数据为 null,那就退出这个系列的下载。 return ; } Picture picture = this.extractPictureUrl(html); System.out.println("开始下载"); //多线程实在是太快了(快并不是好事,我改成单线程爬取吧) SinglePictureDownloader downloader = new SinglePictureDownloader(picture, referer, filePath); downloader.download(); try { Thread.sleep(1500); //不要爬的太快了,这里只是学习爬虫的知识。不要扰乱别人的正常服务。 System.out.println("爬取完一张图片,休息1.5秒。"); } catch (InterruptedException e) { e.printStackTrace(); } } }); } /** * 获取每一页图片的标题和链接 * */ private Picture extractPictureUrl(String html) { Document doc = Jsoup.parse(html, "UTF-8"); //获取标题作为文件名 String title = doc.getElementsByTag("h3") .first() .text(); //获取图片的链接(img 标签的 src 属性) String url = doc.getElementsByAttributeValue("class", "main-image") .first() .getElementsByTag("img") .attr("src"); //获取图片的文件扩展名 title = title + url.substring(url.lastIndexOf(".")); return new Picture(title, url); } }
여기에 크롤러 대기열이 있습니다. 결국 첫 번째 크롤링도 하지 못했습니다. 계산을 잘못해서 두 자릿수만큼 계산을 놓쳤기 때문입니다. 그러나 프로그램의 기능은 정확합니다.
package com.picture; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 爬虫启动类 * */ public class BootStrap { public static void main(String[] args) { //反爬措施:UA、refer 简单绕过就行了。 //refer https://www.mzitu.com //使用数组做一个爬虫队列 String[] urls = new String[] { "https://www.mzitu.com/xinggan/", "https://www.mzitu.com/zipai/" }; // 添加初始队列,启动爬虫 List<String> urlList = new ArrayList<>(Arrays.asList(urls)); PictureSpider spider = new PictureSpider(); spider.start(urlList); } }
여기에 계산 오류가 있습니다. 코드는 다음과 같습니다.
for (int i = 1; i <= 10; i++) { //分页获取图片数据,简单获取几页就行了 this.page(url + "page/"+ 1); }
계산에 실수를 해서 i 값이 너무 큽니다. 이 상황에 따라 다운로드하면 총 4*10*(30-5)*60=64800장의 사진이 다운로드됩니다. (각 페이지에는 30개의 제목 페이지가 포함되어 있으며 약 5개는 광고입니다.) 처음에는 사진이 몇백 장밖에 없는 줄 알았습니다! 이는 추정치이지만 실제 다운로드량은 이와 크게 다르지 않을 것입니다. (차수차이는 없습니다) 그래서 한동안 다운로드를 해보니 첫 번째 대기열의 사진만 다운로드되는 것을 발견했습니다. 물론 크롤러 학습 프로그램으로서는 여전히 자격을 갖추고 있습니다.
이 프로그램은 학습용으로 각 그림의 다운로드 간격을 1.5초로 설정했고, 단일 스레드 프로그램이므로 속도가 매우 느립니다. 그러나 그것은 중요하지 않습니다. 프로그램이 올바르게 작동하는 한 누구도 이미지가 다운로드될 때까지 기다리지 않을 것입니다.
오랜 시간이 소요될 것으로 예상됩니다: 64800*1.5s = 97200s = 27h. 이는 대략적인 추정일 뿐이며 프로그램의 다른 실행 시간을 고려하지 않지만 다른 시간은 기본적으로 무시할 수 있습니다.
위 내용은 Java 크롤러를 사용하여 이미지를 일괄적으로 크롤링하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!