Java爬虫(二)
前言:在上一篇博客中我们采用了基于HttpURLConnection的方式进行数据爬取
Java爬虫(一),但是里面没有用任何框架,通过原生http进行爬取,那么问题来了,有没有什么轻便的框架供我们使用呢?
Jsoup 是一款 Java的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据,本篇博客将主要针对jsoup技术进行数据爬取,而且为了提高效率我们将采用多线程的方式进行数据的读取
爬取入口
我们就随便找个网站进行爬取吧,原理都一样,比如我们爬取一个国家统计局的数据,首先我们找到所爬取网站的页面如下:
比如我就爬天津市的数据,找到要爬取的网站页面入口为:
http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2018/12.html
爬取思路
其实对于爬虫的那些代码并不需要过于计较,先把思路来理清楚,先来一张图:
从上图可以看出我们的基本大概思路和流程,但是由于网站的多变性我们不能冒然爬取,造成数据冗余,此网页最后一层网页只有文本信息以外,其余的网页都是一条文本信息对应一个文本信息对应一个URL地址,并且出最后一层之外,每一层的数字和文本信息对应的URL是相同的,所以我们只爬取名称就行了,如下:
开始爬取
注意我们下面Document导包路径都为:
import org.jsoup.nodes.Document;
最后的项目目录结构如下:
1.简单的创建一个maven项目
2.在pom.xml文件中添加如下依赖
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>
3.创建主类
public class Main {
//待抓取的Url队列,全局共享
public static final LinkedBlockingQueue<String> UrlQueue = new LinkedBlockingQueue<>();
public static final WormCore wormCore = new WormCore();
public static void main(String[] args) {
//要抓取的根URL
String rootUrl = "http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2018/12/1201.html";
//先把根URL加入URL队列
UrlQueue.offer(rootUrl);
Runnable runnable = new MyRunnable();
//开启固定大小的线程池,线程数为10
ExecutorService Fixed = Executors.newFixedThreadPool(10);
//开始爬取
for (int i = 0;i < 10;i++){
Fixed.submit(runnable);
}
//关闭线程池
Fixed.shutdown();
}
}
主类中定义全局共享的URL队列,同时定义Runnable实现类所要使用的核心控制模块的对象,使用固定线程池进行线程调度管理,以及根URL的定义以及入队列
4.创建Runnable实现类
public class MyRunnable implements Runnable{
@Override
public void run() {
while (true) {
try {
Thread.sleep(200);
//把主方法中的URL队列传给核心控制类,开始该线程的爬取
Main.wormCore.Wormcore(Main.UrlQueue);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
通过此实现类不断调用核心控制类WormCore的方法,进行数据的爬取
5.核心控制类
public class WormCore {
//Document获取层对象
private volatile Catch Catch = new Catch();
//Document解析层对象
private volatile Analysis analysis = new Analysis();
//数据处理层对象
public volatile Access access = new Access();
public void Wormcore(LinkedBlockingQueue<String> UrlQueue) throws IOException, InterruptedException {
synchronized (this) {
if (!UrlQueue.isEmpty()) {
String Url = UrlQueue.take();
//通过Url队列中的Url抓取Document,进行Url和文本信息的抓取
Document document = Catch.CatchDocument(Url);
//数据解析模块返回的数据(含有文本信息以及URL)
HashMap<String, ArrayList<String>> DataMap = analysis.AnalysisDocument(document, Url);
//数据处理模块分离出的、只含有URL的集合
ArrayList<String> UrlList = access.DataAccess(DataMap);
//定义迭代器,把抓取到的Url添加到Url队列中
Iterator<String> iterator = UrlList.iterator();
while (iterator.hasNext()) {
UrlQueue.put(iterator.next());
}
//打印URL队列中的URL条数以及队列是否为空
System.out.println(UrlQueue.size());
System.out.println(UrlQueue.isEmpty());
//为空说明爬取完毕
if (UrlQueue.isEmpty()) {
System.out.println("抓取完毕!");
System.exit(1);
}
}
}
}
}
此类就是我们上面说的核心控制类,首先将URI队列队弹出发送给网页抓取模块从而获取Document,其次将获取的Document发送给网页解析模块提取URL和文本信息,最后将信息的HashMap交给数据处理模块获取网页中所有的URL并再加入URL队列
6.网页抓取模块
public class Catch {
//根据网页的Url获取网页Document
public Document CatchDocument(String Url) throws IOException {
try {
return Jsoup.connect(Url)
.userAgent("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)").timeout(5000).get();
//如出现超时问题就继续抓取
}catch (SocketTimeoutException s){
return Jsoup.connect(Url)
.userAgent("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)").timeout(5000).get();
}
}
}
根据核心控制类传来的URL获取Document并返回,自行设置超时时间以及超时之后的操作
7.网页解析模块
public class Analysis {
//根据Document解析网页
public HashMap<String, ArrayList<String>> AnalysisDocument(Document document, String Url){
//因为网页上的URL为相对地址,所以在这里进行URL的拼接,这是前半部分
String Before_Url = Url.substring(0, Url.lastIndexOf("/") + 1);
//储存文本信息的List
ArrayList<String> Text = new ArrayList<>();
//储存Url的List
ArrayList<String> Urls = new ArrayList<>();
HashMap<String,ArrayList<String>> Message = new HashMap<>();
//最后一个页面的前三个文本不是我们想要的
int Flag = 1;
Elements elements = document.select("tr[class]").select("a[href]");
//最后一个页面的处理
if(elements.isEmpty()){
elements = document.select("tr[class]").select("td");
for (Element element : elements) {
if (!Number.IsNumber(element.text()) && Flag > 3) {
System.out.println(element.text());
}
Flag++;
}
//普通页面的处理
}else {
for (Element element : elements) {
if (!Number.IsNumber(element.text())) {
Text.add(element.text());
System.out.println(element.text());
Urls.add(Before_Url + element.attr("href"));
}
}
}
//把文本集合和URL集合装到Map中返回
Message.put("text",Text);
Message.put("Url",Urls);
return Message;
}
}
由于最后的页面和前面的页面不同,所以需要因地制宜,我们抛出了之前说的统计用区划代码数字,只要名称,所以博主通过下面的Number类里面的方法进行了判断
8.判断是否为数字
public class Number {
public static boolean IsNumber(String str){
for(int i=0;i<str.length();i++){
char c = str.charAt(i);
if(c>=48 && c<=57){
}else {
return false;
}
}
return true;
}
}
9.数据处理模块
public class Access {
//数据处理,把信息中的Url返回给核心,文本信息储存
public ArrayList<String> DataAccess(HashMap<String, ArrayList<String>> Message){
return Message.get("Url");
}
}
将HashMap中保存URL的集合提取出来进行保存
10.最后运行结果如下:
结语:对于爬虫有多种方式,不同的方式存在着代码简明、爬取效率等各种差异,所以对于爬取数据应该要因地制宜