前言: 最近简单学习了一下一个比较知名的Java爬虫框架——webmagic。虽然还是不太理解它的整体结构,但是用来爬取数据,应该是没有问题了。而且,我发现它和我使用HttpClient和Jsoup这两个框架(类库)的时候,思考不太一样。使用上面两种工具,对网络数据进行爬取,还是比较原始的。因为爬虫其实涉及了到了很多知识,并不是简单的发起请求和接收响应,例如:URL调度、URL去重。这些东西以前都没有考虑到,对于URL调度来说,基本上没有调度,看到就爬了。这种对于小型网站或者单独的页面似乎是没有问题,但是对于大量数据的处理就显得力不从心了。不过,我现在也还是处理这些简单的页面(这样也导致了爬虫技术没有什么进步,基本上就是图片、视频、音频、文本爬一爬,应该去学习学习别人的经验了),但是使用webmagic框架之后,可以主要去思考爬取的逻辑,而不用去处理哪些细节性的问题了。
目的:我来抓取一些idol的图片,现在写爬虫的绅士太多了,都是千篇一律的抓美女图片,把那些网站搞得都弄起来反爬措施了。所以,为了表示我和他们不一样,我来换一个方向,来抓取idol的图片!
目标:TFBoys的若干张图片采集
起始地址:http://www.win4000/meinvtag22781_1.html
目标网站结构分析
由于这个网站没有反爬措施,我这里就不去分析数据包了。直接来分析一下网站的结构就行了 ,确定采集的逻辑。
首先是图片标题列表页:
它含有若干个分类标题,然后分为五页。但是这里只是一个学习而已,没必要采集那么多数据,所以我只采集一个页面的数据就行了。
然后是图片的标题页:
图片的标题页是一个标题下的若干张图片,但是每一个标题页下面的图片数量不等,**这里有一个小的分页,我们可以通过它来获取到下一页,但是比较坑的是它的下一页可以一直下去,导致我的爬虫停不下来了了。**因为它的最后一张图片的下一页是其它明星的图片了。不知道为什么这样设计,可能是增加浏览量吧。
简单的网站结构图示(简化)
这个processon的图片,怎么是透明背景的?算了,凑合着看吧。
爬取思路:
首先从起始页面开始,抓取起始页包含的所有的标题页链接,并将它们加入URL调度器中,然后对每一个标题页进行抓取,抓取一张图片后,把它的下一页链接添加到URL调度器中,知道没有下一页。重复此步骤,直到所有图片全部被抓取到,整个采集流程就结束了。
使用WebMagic进行图片采集
导入依赖
<dependencies>
<!-- webmagic框架 -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.3</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.3</version>
</dependency>
</dependencies>
在resource文件夹下添加日志文件的配置
log4j.rootLogger=debug, stdout, R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=firestorm.log
log4j.appender.R.MaxFileSize=100KB
log4j.appender.R.MaxBackupIndex=1
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n
log4j.logger.com.codefutures=DEBUG
注:这是网上找的代码,因为缺少日志配置文件,程序无法启动。但是我还没有真正使用过日志框架,所以对于它不是很了解。但是这里它是可以使用的,只是日志太多了。
主要代码逻辑
package com.spider;
import java.util.List;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
public class TFBoysSpider implements PageProcessor {
private static final int TIME_OUT = 10*1000;
private static final int SLEEP_TIME = 5*1000;
private static final String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) "
+ "AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31";
private Site site = Site.me()
.setTimeOut(TIME_OUT)
.setRetryTimes(3)
.setRetrySleepTime(SLEEP_TIME)
.setUserAgent(USER_AGENT);
@Override
public void process(Page page) {
// 我这里只是获取一页的数据,五页太多了。
if (page.getUrl().regex("http://www.win4000/meinvtag22781_").match()) {
List<String> urlList = page.getHtml().css("div.list_cont.Left_list_cont.Left_list_cont2 ul li a", "href").all();
// 将当前页的所有相关详情页添加到接下来的任务中
page.addTargetRequests(urlList);
page.setSkip(true); // 跳过该页
} else if (page.getUrl().regex("http://www.win4000/meinv+").match()) {
// 爬取图片页的图片
String name = page.getHtml().css("div.ptitle > h1", "text").toString();
String order = page.getHtml().css("div.ptitle > span", "text").toString();
// key 即作为文件名,然后 value 是链接地址
page.putField(name+order, page.getHtml().css("div.paper-down > a", "href").toString());
// 获取分页的数据详情
String nextUrl = page.getHtml().css("div.pic-next-img > a", "href").toString();
// String regex = "_\\d.html$" 以此结尾,就认为是分页链接,否则跳过。
String regex = "http://www.win4000/meinv\\d+_\\d\\.html";
if (nextUrl.matches(regex)) {
page.addTargetRequest(nextUrl);
}
}
}
@Override
public Site getSite() {
return site;
}
public static void main(String[] args) {
Spider.create(new TFBoysSpider())
.addUrl("http://www.win4000/meinvtag22781_1.html")
.addPipeline(new ImgPipeline())
.thread(3)
.run();
}
}
数据持久化是自定义了一个Pipeline
package com.spider;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
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 us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;
public class ImgPipeline implements Pipeline {
// 保存图片的文件夹
private static final String SAVE_DIR = "D:/DragonFile/img";
@Override
public void process(ResultItems resultItems, Task task) {
resultItems.getAll().forEach(this::download);
}
// 这里本来打算按照标题来分文件夹保存的,但是我初学此框架,
// 对此不是太熟悉,就一并保存了吧!
private void download(String imgName, Object imgUrl) {
// 这里使用连接池来管理,就不需要手动关闭连接了!不要再使用 try-with-resource 语句了!
CloseableHttpClient httpClient = HttpClientUtil.getHttpClient();
HttpGet get = new HttpGet(imgUrl.toString()); // 这里imgUrl是一个 Object 类型,但是它确实是一个String,这里是没问题的。
try (CloseableHttpResponse response = httpClient.execute(get)) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
entity.writeTo(new BufferedOutputStream(new FileOutputStream(new File(SAVE_DIR, imgName+".jpg"))));
EntityUtils.consume(entity);
} else {
System.out.println("下载失败:" + imgUrl);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
自定义了一个HttpClientUtil工具类,使用连接词来管理下载连接
package com.spider;
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();
}
}
运行结果
控制台的日志,这里人能阅读的信息很好,大概就最后总共145页内容被下载了。
这里是下载的图片,但是这里只有144张图片,为什么呢?答案很简单,因为第一页是非图片页,它是没有图片可下载的,所以跳过去了。
page.setSkip(true); // 它应该作用就是跳过持久化,但是我还是不太确定。
说明
这里的抓取应该是广度优先遍历的,因为它是通过一个队列来进行URL调度的。不过,我也是刚接触这个爬虫框架,还有很多不是很熟悉的地方。
使用HTTP Client 和 Jsoup采集
这里最好换一个工程,虽然webmagic里面已经添加了它们的依赖,但是我在控制台输出的日志无法看到了,都被日志框架掩盖了。
添加依赖
<!-- https://mvnrepository/artifact/org.apache.httpcomponents/httpclient -->
<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>
主要逻辑代码
package com.tf;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
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 com.tf.HttpClientUtil;
public class TFBSpider {
public static String savePath = "D:/DragonFile/img2";
private ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
// 爬虫初始爬取网页的链接地址
private String root;
public TFBSpider(String root) {
this.root = root;
}
public void spider() {
// 获取第一页的html,总共是分为5页,我只获取第一页的信息
String html = this.getHtml(root);
// 获取第一页所有的详情页,对每一个详情页的图片进行递归爬取
this.getList(html).forEach(url -> {
this.getImg(html, url);
});
// 关闭线程池,否则会导致程序无法正常停止
service.shutdown();
}
/**
* 通用方法,用于获取html
* */
public String getHtml(String url) {
CloseableHttpClient httpClient = HttpClientUtil.getHttpClient();
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/84.0.4147.105 Safari/537.36 Edg/84.0.522.52"); // 设置一个最基础的 User-Agent
try (CloseableHttpResponse response = httpClient.execute(get)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
if (entity != null) {
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
} else {
throw new IOException("抓取网页发生异常!状态码为:" + statusCode);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 获取详情页里所有的列表链接
public List<String> getList(String html) {
return Jsoup.parse(html)
.select("div.list_cont.Left_list_cont.Left_list_cont2 ul li a")
.eachAttr("href");
}
/**
* 递归爬取图片
* */
public void getImg(String savePath, String url) {
// 爬取图片页的图片
String html = this.getHtml(url);
Document doc = Jsoup.parse(html);
// 获取当前页图片的链接地址
String imgUrl = doc.select("div.paper-down > a").first().attr("href");
// name 为标题加序号
String name = doc.select("div.ptitle > h1").text() + doc.select("div.ptitle > span").text();
// 下一页的链接地址
String nextUrl = doc.select("div.pic-next-img > a").first().attr("href");
// 提交线程池,每 4 秒执行一次
service.schedule(new Downloader(name, imgUrl), 4, TimeUnit.SECONDS);
String regex = "http://www.win4000/meinv\\d+_\\d\\.html";
if (nextUrl.matches(regex)) {
getImg(savePath, nextUrl);
}
}
private class Downloader implements Runnable {
private String name;
private String url;
public Downloader(String name, String url) {
this.name = name;
this.url = url;
}
public void download() {
CloseableHttpClient httpClient = HttpClientUtil.getHttpClient();
HttpGet get = new HttpGet(url);
try (CloseableHttpResponse response = httpClient.execute(get)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
if (entity != null) {
entity.writeTo(new BufferedOutputStream(new FileOutputStream(new File(savePath, name+".jpg"))));
} else {
throw new IOException("HttpEntity 为 null!" + url);
}
} else {
throw new IOException("请求图片发生异常!状态码为:" + statusCode + " " + url);
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("下载完成了:" + name + " " + url);
}
@Override
public void run() {
this.download();
}
}
public static void main(String[] args) {
// 爬取第二页,和使用框架不一样的页
TFBSpider tfboysSpider = new TFBSpider("http://www.win4000/meinvtag22781_2.html");
tfboysSpider.spider();
}
}
注意:import com.tf.HttpClientUtil;
它就是上面的那个HttpClientUtil
。
运行结果:
注意:
我这里抓取的是第二页的图片,所以图片是165张。
说明:
从上面可以看出来,我这里抓取是按照标题来抓取得,就是一次性把当前标题下所有的图片全部抓取完,再去抓取其它标题下的。有点像深度遍历,但是好像也不完全是。
总结
使用爬虫的框架之后,对于数据采集来说确实是方便了很多。这样就不用取考虑很多细节方面的东西了,只需要考虑具体的逻辑是如何实现的即可。但是,同时也是失去了一定的自由度,你得按照爬虫框架的思路去处理,我以前写多了那种自由的代码(不正规),导致我接触这个爬虫框架有些不太适应,不知所措了。这是因为我以前的爬虫学习可能是走了点歪路了。不过,还是作者有先见之明!
PS:我使用HttpClient和Jsoup的爬虫,抓取图片的操作就是写成了递归的形式,不过我觉得我在五台机器上运行,分别抓取五页似乎也行。不过,这只是非常少量的数据,看不出框架的效果,还是得继续探索!
更多推荐
webmagic初次尝试——写给女生看的爬虫
发布评论