1.进入小说网站http://www.147xs.org/
可以看到有非常多的小说,我们随便点进去一个
点进来就是小说的目录部分了,但我们最终解析的并不是目录,而是有内容的网页,点进:第一章 此间少年
这才是我们真正要爬取的网页
但是我们进入这种网页似乎费了点功夫,这样的页面我们称作二级页面,爬取二级页面首先需要在一级页面(也就是章节目录页面)得到二级页面的链接,后面讲代码实现时会具体说到
由于我们要爬取所有小说,所以一级页面不止一个,这些链接就在网站首页里,每个小说都是一个一级链接,所以只需要将首页的所有小说的链接都抓取到即可,之后再进入每个小说的链接获取所有章节的链接,进入每个章节爬取内容
这么多一级页面(小说章节目录页面)和二级页面(每个章节内容页面)我们怎么区分呢,究竟什么时候存入二级页面?什么时候爬取二级页面内容?
我们总不能该存页面的时候去爬,该爬页面的时候去存
所以要对每级的页面的url进行比对分析
http://www.147xs.org/book/13794/
http://www.147xs.org/book/13794/206911.html
点进去发现第一个为一级页面,第二个为二级页面
两者的最大区别就是有没有.html后缀
那么思路很清晰了,如果该url有.html后缀则进去爬内容,如果没有则在一级页面将二级页面链接加入请求队列之中,即存入二级页面
接下来是对url分析的代码
public void process(Page page) {
//得到url,起始为http://www.147xs.org/,首页
String url=page.getUrl().get();
//通过分析知道带有html的url是之前获取到的每个章节的链接。
if(url.contains(".html")) {//如果是章节的链接则进入
this.getContent(page);//抓取内容函数
}else if(url.equals("http://www.147xs.org/")){//如果是网站首页则进入,获取所有小说的链接
List<String> links = page.getHtml().links()
.regex(".*book/\\d+/").all();//正则匹配url
page.addTargetRequests(links);//加入请求队列等待处理
page.setSkip(true);//跳过保存管道类
} else {
//获取该小说的所有章节的链接
List<String> links = page.getHtml().links()
.regex(".*\\d+\\.html").all();//正则匹配url
page.addTargetRequests(links);//加入请求队列等待处理
page.setSkip(true);//跳过保存管道类
}
}
分析如下
第一个if相信不用我说大家也能看懂,判断是否为二级页面的链接
如果是则进去爬内容,不是则再次判断
else if是为了判断是不是网站首页,如果是,则进入把一级页面的链接抓取下来加入到请求队列。这个else if在整个程序中只运行了一次,大家可以思考一下原因
else以上的都不是那么该页面所处的位置只能为一级页面了,所以将二级页面的链接抓取下来加入请求队列
请求队列这里我要交代一下
听他的名字就知道他是个队列,里面存放了你加入的url,只要队列中有url,那么刚才的process函数就会自动执行,并且是按照存入url的顺序执行的。具体为什么我们无需深究,框架自带的
比如我们初始进入的是网站首页,第一次执行了process函数后请求队列会加入很多的url
可能为(实际情况可能不会严格按照从小到大的顺序,随机抓取):
http://www.147xs.org/book/13794/
http://www.147xs.org/book/13795/
http://www.147xs.org/book/13796/
http://www.147xs.org/book/13797/
http://www.147xs.org/book/13798/
。。。。。。
执行完第一次后,请求队列中都是小说的链接,也就是一级页面的链接,
由于每次解析完一个页面,该页面的url就会出队列,所以网站首页地址没了
再次执行process,进入的页面为http://www.147xs.org/book/13794/
由于这个页面的url没有.html,则执行else,将每个章节的链接加入请求队列
执行完后请求队列变成
http://www.147xs.org/book/13795/
http://www.147xs.org/book/13796/
http://www.147xs.org/book/13797/
http://www.147xs.org/book/13798/
。。。。。。
http://www.147xs.org/book/66666/ 假设这个为最后一个小说的链接
http://www.147xs.org/book/13794/206911.html
http://www.147xs.org/book/13794/206912.html
http://www.147xs.org/book/13794/206913.html
http://www.147xs.org/book/13794/206914.html
。。。。。。。
可以看出需要等所有的一级页面全部解析完毕才能开始爬取二级页面
当然,如果你只想爬取一部小说,只需要将起始的url改为http://www.147xs.org/book/13794/(只是举例子)
爬取内容部分
首先要清楚自己需要爬什么(以《伏天氏》为例)
1.小说书名
2.章节标题
3.章节内容
首先是小说的书名
F12 开发者模式
小说书名的xpth://div[@class=‘con_top’]/a[3]/text()
得到伏天氏
同理
章节的xpth://div[@class=‘bookname’]/h1/text()
得到第一章 此间少年
内容
内容的xpth://div[@id=‘content’]//p/text() 注意这里的p有多个
最后的结果为所有内容
爬取内容代码如下
public void getContent(Page page){
//书名
String bookname=page.getHtml()
.xpath("//div[@class='con_top']/a[3]/text()").get();
//标题
String title=page.getHtml()
.xpath("//div[@class='bookname']/h1/text()").get();
//内容
List<String> all = page.getHtml()
.xpath("//div[@id='content']//p/text()").all();
//通过键值对的方式存入,用于后面写入文件时用到这些属性
page.putField("bookname",bookname);
page.putField("title",title.replaceAll("[/?\\\\]",""));
page.putField("content",all);
}
.get表示取出标签里的文本(只能取一个),返回值为字符串
.all是由于p标签有多个,所以需要取出每个p标签里面的内容,最后返回的也是一个集合
由于后面需要bookname属性和title属性的值来创建文件,所以要防止属性值出现"/","\\","?"干扰文件路径
putField方法类似于Map,通过键值对保存数据
最后爬取的内容都会经过一个管道,默认是输出到控制台
但我们希望通过文件保存下来
这时候就要自己实现一个管道
代码如下
class SavePipeline implements Pipeline{
@Override
public void process(ResultItems resultItems, Task task) {
//通过key获取值
String bookname=resultItems.get("bookname");
String title = resultItems.get("title");
List<String> contents=resultItems.get("content");
//创建小说目录
File file=new File("网络小说\\"+bookname);
if(!file.exists()){
file.mkdirs();
}
//写入文件
try {
FileOutputStream writer=new FileOutputStream(
file.getAbsolutePath()+"\\"+title+".txt");
//遍历集合,取出每段内容
for (String content : contents) {
String replace = content.replace("。", "\n");
writer.write(replace.getBytes("utf8");
}
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述的resultItems就是存入爬取的内容时所说的那个Map
通过get方法传入key,得到value
开始的process函数里有一个page.setSkip(true);//跳过保存管道类
可能大家有疑惑,这个函数是设置是否跳过管道,直接解析下一个url
由于我们的管道是保存爬取来的数据的,但是程序执行到page.setSkip(true)之前我们并没有爬取数据,如果不设置跳过管道,则会经过管道保存数据,此时出现空指针异常
后面的写入文件就不用多说了
接下来就是启动爬虫了
启动前需要配置一些参数
先配置Site
从第一行往下分别对应设置编码方式,设置超时时间,设置重试次数,设置循环次数
当然也不必配置这么多
个人认为在网速给力的时候只需要设置编码方式即可
接下来分析主函数里的配置
- new xioashuo()里面的xioashuo这个类就是实现了解析url的process函数的那个类,如果没看明白待会给出所有代码时你就懂了
- addurl为添加url,可以添加多次
- addPipeline(new SavePipeline()) 和*addPipeline(new ConsolePipeline())*添加管道,即处理数据的类,第一个是我们自己实现的
而第二个是框架自己带的,如果一个管道都不添加则默认为第二个,会将结果打印到控制台;我们让他即保存到文件,有能输出到控制台
4.thread开启多线程,本例中开启了50个线程同时爬取
5.run启动爬虫
private Site site = Site.me().setCharset("utf8").
setTimeOut(10000).
setRetryTimes(5).
setCycleRetryTimes(3);
@Override
public Site getSite() {
return site;
}
public static void main(String[] args) {
String startUrl="http://www.147xs.org";
Spider.create(new xioashuo()).addUrl(startUrl)
.addPipeline(new SavePipeline())
.addPipeline(new ConsolePipeline())
.thread(50)
.run();
}
全部代码如下:
首先创建一个Maven工程并导入依赖
<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>
import us.codecraft.webmagic.*;
import us.codecraft.webmagic.pipeline.ConsolePipeline;
import us.codecraft.webmagic.pipeline.Pipeline;
import us.codecraft.webmagic.processor.PageProcessor;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class xioashuo implements PageProcessor {
public void process(Page page) {
String url=page.getUrl().get();//得到url,起始为http://www.147xs.org/
//通过分析知道带有html的url是之前获取到的章节的链接。
if(url.contains(".html")) {//如果是章节的链接则进入
this.getContent(page);//抓取内容函数
}else if(url.equals("http://www.147xs.org/")){//如果是网站首页则进入,获取所有小说的链接
List<String> links = page.getHtml().links().regex(".*book/\\d+/").all();
page.addTargetRequests(links);
page.setSkip(true);
} else {
//获取该小说的所有章节的链接
List<String> links = page.getHtml().links().regex(".*\\d+\\.html").all();
page.addTargetRequests(links);
page.setSkip(true);
}
}
public void getContent(Page page){
//书名
String bookname=page.getHtml().xpath("//div[@class='con_top']/a[3]/text()").get();
//标题
String title=page.getHtml().xpath("//div[@class='bookname']/h1/text()").get();
//内容
List<String> all = page.getHtml().xpath("//div[@id='content']//p/text()").all();
//通过键值对的方式存入,用于后面写入文件时用到这些属性
page.putField("bookname",bookname);
page.putField("title",title.replaceAll("[/?\\\\]",""));
page.putField("content",all);
}
private Site site = Site.me().setCharset("utf8").
setTimeOut(10000).
setRetryTimes(5).
setCycleRetryTimes(3);
public Site getSite() {
return site;
}
public static void main(String[] args) {
String startUrl="http://www.147xs.org/";
Spider.create(new xioashuo()).addUrl(startUrl)
.addPipeline(new SavePipeline())
.addPipeline(new ConsolePipeline())
.thread(50)
.run();
}
}
/**
* 自定义管道类
* 用于保存爬取的小说到文件
*/
class SavePipeline implements Pipeline{
public void process(ResultItems resultItems, Task task) {
//通过key获取值
String bookname=resultItems.get("bookname");
String title = resultItems.get("title");
List<String> contents=resultItems.get("content");
//创建小说目录
File file=new File("网络小说\\"+bookname);
if(!file.exists()){
file.mkdirs();
}
//写入文件
try {
FileOutputStream writer=new FileOutputStream(
file.getAbsolutePath()+"\\"+title+".txt");
//遍历集合,取出每段内容
for (String content : contents) {
String replace = content.replace("。", "\n");
writer.write(replace.getBytes(StandardCharsets.UTF_8));
}
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行一下
开始时先存入每个小说链接
等到所有小说的章节链接爬取完后
开始爬取每部小说
剩下的就是耐心等待了
如果你只想爬取一部小说
只需要在主函数里将startUrl改成你想要爬取的小说的链接(但只能是
http://www.147xs.org/这个网站下的)
例如我只想爬取
《盗墓笔记》
这是他的url:http://www.147xs.org/book/3505/
大功告成!!!
如果有疑问可以评论区留言,感谢阅读