不会python?那就换一种姿势爬虫!Java爬虫技术总结
—本博客为原创内容,转载需注明本人—
前几天有个师妹将要毕业,需要准备毕业论文,但是论文调研需要数据资料,上知网一查,十几万条数据!指导老师让她手动copy收集,十几万的数据手动copy要浪费多少时间啊,然后她就找我帮忙。我想了一下,写个爬虫程序去爬下来或许是个不错的解决方案呢!之前一直听其他人说爬虫最好用python,但是我是一名Java工程师啊!鲁迅曾说过,学python救不了中国人,但是Java可以!
好啦,开个玩笑,主要是她急着要,我单独学一门语言去做爬虫,有点不现实,然后我就用了Java,去知乎看一下,发现原来Java也有很多开源的爬虫api嘛,然后就是开始干了,三天时间写好程序,可以爬数据下来,下面分享一下技术总结,感兴趣的朋友可以一起交流一下!
在分享技术之前,先简单说一下爬虫的原理吧。网络爬虫听起来很高大上,其实就是原理很简单,说的通俗一点就是,程序向指定连接发出请求,服务器返回完整的html回来,程序拿到这个html之后就进行解析,解析的原理就是定位html元素,然后将你想要的数据拿下来。
那再看一下Java开源的爬虫API,挺多的,具体可以点击链接看一下:推荐一些优秀的开源Java爬虫项目
因为我不是要在实际的项目中应用,所以我选择非常轻量级易上手的 crawler4j 。感兴趣的可以去github看看它的介绍,我这边简单介绍一下怎么应用。用起来非常简单,现在maven导入依赖。
<dependency>
<groupId>edu.uci.ics</groupId>
<artifactId>crawler4j</artifactId>
<version>4.2</version>
</dependency>
自定义爬虫类继承插件的WebCrawler类,然后重写里面shouldVisit和Visit方法。
package com.chf;
import edu.uci.ics.crawler4j.crawler.Page;
import edu.uci.ics.crawler4j.crawler.WebCrawler;
import edu.uci.ics.crawler4j.parser.HtmlParseData;
import edu.uci.ics.crawler4j.url.WebURL;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author:chf
* @description: 自定义爬虫类需要继承WebCrawler类,决定哪些url可以被爬以及处理爬取的页面信息
* @date:2019/3/8
**/
public class MyCraeler extends WebCrawler {
/**
* 正则匹配指定的后缀文件
*/
private final static Pattern FILTERS = Pattern.compile(".*(\\.(css|js|bmp|gif|jpe?g" + "|png|tiff?|mid|mp2|mp3|mp4"
+ "|wav|avi|mov|mpeg|ram|m4v|pdf" + "|rm|smil|wmv|swf|wma|zip|rar|gz))$");
/**
* 这个方法主要是决定哪些url我们需要抓取,返回true表示是我们需要的,返回false表示不是我们需要的Url
* 第一个参数referringPage封装了当前爬取的页面信息
* 第二个参数url封装了当前爬取的页面url信息
*/
@Override
public boolean shouldVisit(Page referringPage, WebURL url) {
String href = url.getURL().toLowerCase(); // 得到小写的url
return !FILTERS.matcher(href).matches() // 正则匹配,过滤掉我们不需要的后缀文件
&& href.startsWith("http://r.cnki.net/kns/brief/result.aspx"); // url必须是http://www.java1234.com/开头,规定站点
}
/**
* 当我们爬到我们需要的页面,这个方法会被调用,我们可以尽情的处理这个页面
* page参数封装了所有页面信息
*/
@Override
public void visit(Page page) {
String url = page.getWebURL().getURL(); // 获取url
System.out.println("URL: " + url);
if (page.getParseData() instanceof HtmlParseData) { // 判断是否是html数据
HtmlParseData htmlParseData = (HtmlParseData) page.getParseData(); // 强制类型转换,获取html数据对象
String text = htmlParseData.getText(); // 获取页面纯文本(无html标签)
String html = htmlParseData.getHtml(); // 获取页面Html
Set<WebURL> links = htmlParseData.getOutgoingUrls(); // 获取页面输出链接
System.out.println("纯文本长度: " + text.length());
System.out.println("html长度: " + html.length());
System.out.println("输出链接个数: " + links.size());
}
}
}
然后定义一个Controller来执行你的爬虫类
package com.chf;
import edu.uci.ics.crawler4j.crawler.CrawlConfig;
import edu.uci.ics.crawler4j.crawler.CrawlController;
import edu.uci.ics.crawler4j.fetcher.PageFetcher;
import edu.uci.ics.crawler4j.robotstxt.RobotstxtConfig;
import edu.uci.ics.crawler4j.robotstxt.RobotstxtServer;
/**
* @author:chf
* @description: 爬虫机器人控制器
* @date:2019/3/8
**/
public class Controller {
public static void main(String[] args) throws Exception {
String crawlStorageFolder = "C:/Users/94068/Desktop/logs/crawl"; // 定义爬虫数据存储位置
int numberOfCrawlers =2; // 定义7个爬虫,也就是7个线程
CrawlConfig config = new CrawlConfig(); // 定义爬虫配置
config.setCrawlStorageFolder(crawlStorageFolder); // 设置爬虫文件存储位置
/*
* 最多爬取多少个页面
*/
config.setMaxPagesToFetch(1000);
//爬取二进制文件
// config.setIncludeBinaryContentInCrawling(true);
//爬取深度
config.setMaxDepthOfCrawling(1);
/*
* 实例化爬虫控制器
*/
PageFetcher pageFetcher = new PageFetcher(config); // 实例化页面获取器
RobotstxtConfig robotstxtConfig = new RobotstxtConfig(); // 实例化爬虫机器人配置 比如可以设置 user-agent
// 实例化爬虫机器人对目标服务器的配置,每个网站都有一个robots.txt文件 规定了该网站哪些页面可以爬,哪些页面禁止爬,该类是对robots.txt规范的实现
RobotstxtServer robotstxtServer = new RobotstxtServer(robotstxtConfig, pageFetcher);
// 实例化爬虫控制器
CrawlController controller = new CrawlController(config, pageFetcher, robotstxtServer);
/**
* 配置爬虫种子页面,就是规定的从哪里开始爬,可以配置多个种子页面
*/
controller.addSeed("http://r.cnki.net/kns/brief/result.aspx?dbprefix=gwkt");
/**
* 启动爬虫,爬虫从此刻开始执行爬虫任务,根据以上配置
*/
controller.start(MyCraeler.class, numberOfCrawlers);
}
}
直接运行main方法,你的第一个爬虫程序就完成了,非常容易上手。
那接下来我们说一下程序的应用,我需要抓取中国知网上2016-2017两年的中国专利数据。
那么说一下这个应用的几个难点。
1.知网的接口使用asp.net做的,每次请求接口都要传当前的cookies,接口不直接返回数据,而是返回HTML界面
2.数据量过于庞大,而且需要爬取的是动态资源数据,需要输入条件检索之后,才能有数据
3.数据检索是内部用js进行跳转,直接访问链接没有数据出来
4.这个是最难的,知网做了反爬虫设置,当点击了15次下一页之后,网页提示输入验证码,才能继续下一页的操作
那接下来就根据以上的难点来一步一步的想解决方案吧。
首先就是数据检索是内部用js进行跳转,直接访问链接没有数据出来,这就表示上面的crawler4j没有用了,因为他是直接访问连接去拿html代码然后解析拿数据的。然后我再网上查了一下资料,发现Java有一个HtmlUtil。他相当于一个Java的浏览器,这简直是一个神器啊,访问到网页之后还能对返回来的网页进行操作,我用个工具类来创建它
<!-- 获取js动态生成之后的html -->
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.29</version>
</dependency>
package com.chf.Utils;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import java.io.IOException;
import java.net.MalformedURLException;
/**
* @author:chf
* @description:模拟浏览器执行各种操作
* @date:2019/3/20
**/
public class HtmlUtil {
/*
* 启动JS
*/
public static WebClient iniParam_Js() {
final WebClient webClient = new WebClient(BrowserVersion.CHROME);
// 启动JS
webClient.getOptions().setJavaScriptEnabled(true);
//将ajax解析设为可用
webClient.getOptions().setActiveXNative(true);
//设置Ajax的解析器
webClient.setAjaxController(new NicelyResynchronizingAjaxController());
// 禁止CSS
webClient.getOptions().setCssEnabled(false);
// 启动客户端重定向
webClient.getOptions().setRedirectEnabled(true);
// JS遇到问题时,不抛出异常
webClient.getOptions().setThrowExceptionOnScriptError(false);
// 设置超时
webClient.getOptions().setTimeout(10000);
//禁止下载照片
webClient.getOptions().setDownloadImages(false);
return webClient;
}
/*
* 禁止JS
*/
public static WebClient iniParam_NoJs() {
final WebClient webClient = new WebClient(BrowserVersion.CHROME);
// 禁止JS
webClient.getOptions().setJavaScriptEnabled(false);
// 禁止CSS
webClient.getOptions().setCssEnabled(false);
// 将返回错误状态码错误设置为false
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
// 启动客户端重定向
webClient.getOptions().setRedirectEnabled(true);
// 设置超时
webClient.getOptions().setTimeout(5000);
//禁止下载照片
webClient.getOptions().setDownloadImages(false);
return webClient;
}
/**
* 根据url获取页面,这里需要加载JS
* @param url
* @return 网页
* @throws FailingHttpStatusCodeException
* @throws MalformedURLException
* @throws IOException
*/
public static HtmlPage getPage_Js(String url) throws FailingHttpStatusCodeException, MalformedURLException, IOException{
final WebClient webClient = iniParam_Js();
HtmlPage page = webClient.getPage(url);
//webClient.waitForBackgroundJavaScriptStartingBefore(5000);
return page;
}
/**
* 根据url获取页面,这里不加载JS
* @param url
* @return 网页
* @throws FailingHttpStatusCodeException
* @throws MalformedURLException
* @throws IOException
*/
public static HtmlPage getPage_NoJs(String url) throws FailingHttpStatusCodeException, MalformedURLException, IOException {
final WebClient webClient = iniParam_NoJs();
HtmlPage page = webClient.getPage(url);
return page;
}
}
有了这个HtmlUtil,基本已经解决了大部分问题,我这里的操作逻辑是先用HtmlUtil访问知网,然后用定位器找到条件,输入搜索条件,然后点击检索按钮,用Java程序模拟人在浏览器的操作。
//获取客户端,禁止JS
WebClient webClient = HtmlUtil.iniParam_Js();
//获取搜索页面,搜索页面包含多个学者,机构通常是非完全匹配,姓名是完全匹配的,我们需要对所有的学者进行匹配操作
HtmlPage page = webClient.getPage(orgUrl);
// 根据名字得到一个表单,查看上面这个网页的源代码可以发现表单的名字叫“f”
final HtmlForm form = page.getFormByName("Form1");
// 同样道理,获取”检 索“这个按钮
final HtmlButtonInput button = form.getInputByValue("检 索");
// 得到搜索框
final HtmlTextInput from = form.getInputByName("publishdate_from");
final HtmlTextInput to = form.getInputByName("publishdate_to");
//设置搜索框的value
from.setValueAttribute("2016-01-01");
to.setValueAttribute("2016-12-31");
// 设置好之后,模拟点击按钮行为。
final HtmlPage nextPage = button.click();
HtmlAnchor date=nextPage.getAnchorByText("申请日");
final HtmlPage secondPage = date.click();
HtmlAnchor numNow=secondPage.getAnchorByText("50");
final HtmlPage thirdPage = numNow.click();
上述代码的thirdPage就是最终有数据的html页面。
那下面就是爬虫最关键的一个地方,解析爬下来的html代码,分析html代码的话,我就不在这里分析,html基础不好的朋友可以去w3cshool补一下,我这里直接说HtmlUtil定位html元素的的方法吧。上面的代码可以看到HtmlUtil可以通过value,text,id,name定位元素,如果上面这些都定位不了元素的话,那就使用Xpath来定位。
//解析知网原网页,获取列表的所有链接
List<HtmlAnchor> anchorList=thirdPage.getByXPath("//table[@class='GridTableContent']/tbody/tr/td/a[@class='fz14']");
那拿到列表数据之后呢,我就用HtmlUtil一个个点击进去,进去专利的详情页。
这里面的专利名,申请日期,申请人和地址就是我要爬的数据,因为详情页的html比较复杂,我使用了Java一个比较好用的html解析器jsoup
<!-- jsoup的支持 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.7.3</version>
</dependency>
private static PatentDoc analyzeDetailPage(String detailPage) {
PatentDoc pc=new PatentDoc();
Document doc = Jsoup.parse(detailPage);
Element title=doc.select("td[style=font-size:18px;font-weight:bold;text-align:center;]").first();
Elements table=doc.select("table[id=box]>tbody>tr>td");
for (Element td:table) {
if (td.attr("width").equals("471") && td.attr("bgcolor").equals("#FFFFFF") && td.attr("class").equals("checkItem")){
String patentNo=td.text().replace(" ","");
pc.setPatentNo(patentNo);
}
if (td.attr("width").equals("294") && td.attr("bgcolor").equals("#FFFFFF")){
String patentDate=td.text().replace(" ","");
pc.setPatentDate(patentDate);
}
if (td.attr("bgcolor").equals("#FFFFFF") && td.attr("class").equals("checkItem")){
String patentPerson=td.text().replace(" ","");
pc.setPatentPerson(patentPerson);
}
if (td.attr("bgcolor").equals("#f8f0d2") && td.text().equals(" 【地址】")){
int index=table.indexOf(td);
String patentAdress=table.get(index+1).text().replace(" ","");
pc.setPatentAdress(patentAdress);
break;
}
}
pc.setPatentName(title.text());
return pc;
}
解析完之后呢,将数据封装到对象里,然后将对象存在一个List里,全部数据解析完之后,就把数据导出的csv文件中。
String path = "C://exportParent";
String fileName = "导出专利";
String fileds[] = new String[] { "patentName", "patentPerson","patentDate", "patentNo","patentAdress"};// 设置列英文名(也就是实体类里面对应的列名)
CSVUtils.createCSVFile(resultList, fileds, map, path,fileName);
resultList.clear();
这样爬虫程序就基本写好了,运行一下发现效率太慢了,爬一页列表的数据加导出,花了1分多钟,然后我优化了一下程序,将解析和导出业务逻辑开一条线程来做,主线程负责操作HtmlUtil和返回Html。
//建立线程池管理线程
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
//利用线程池开启线程解析首页的数据
fixedThreadPool.execute(new AnalyzedTask(lastOnePage,18));
package com.chf.enilty;
import com.chf.Utils.CSVUtils;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
/**
* @author:chf
* @description: 解析详情并导出出的线程
* @date:2019/3/20
**/
public class AnalyzedTask implements Runnable{
//建立返回结果对象集
List<PatentDoc> resultList=new ArrayList<>();
private HtmlPage lastOnePage =null;
private int curPage=0;
public AnalyzedTask(HtmlPage lastOnePage,int curPage) {
this.lastOnePage = lastOnePage;
this.curPage=curPage;
}
@Override
public void run() {
/** 获取当前系统时间*/
long startTime = System.currentTimeMillis();
System.out.println("线程开始第"+curPage+"页的解析数据。");
//解析首页的数据
try {
startAnalyzed(lastOnePage);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("第"+curPage+"页数据解析完成。耗时:"+((System.currentTimeMillis()-startTime)/1000)+"s");
}
//开始解析列表数据
private void startAnalyzed(HtmlPage thirdPage) throws Exception {
//解析知网原网页,获取列表的所有链接
List<HtmlAnchor> anchorList=thirdPage.getByXPath("//table[@class='GridTableContent']/tbody/tr/td/a[@class='fz14']");
//遍历点击链接,抓取数据
for (HtmlAnchor anchor:anchorList) {
HtmlPage detailPage = anchor.click();
PatentDoc pc=analyzeDetailPage(detailPage.asXml());
resultList.add(pc);
}
LinkedHashMap map = new LinkedHashMap();
map.put("1", "专利名");
map.put("2", "申请人");
map.put("3", "申请日期");
map.put("4", "申请号");
map.put("5", "申请地址");
String path = "C://exportParent";
String fileName = "导出专利";
String fileds[] = new String[] { "patentName", "patentPerson","patentDate", "patentNo","patentAdress"};// 设置列英文名(也就是实体类里面对应的列名)
CSVUtils.createCSVFile(resultList, fileds, map, path,fileName);
resultList.clear();
}
private PatentDoc analyzeDetailPage(String detailPage) {
PatentDoc pc=new PatentDoc();
Document doc = Jsoup.parse(detailPage);
Element title=doc.select("td[style=font-size:18px;font-weight:bold;text-align:center;]").first();
Elements table=doc.select("table[id=box]>tbody>tr>td");
for (Element td:table) {
if (td.attr("width").equals("471") && td.attr("bgcolor").equals("#FFFFFF") && td.attr("class").equals("checkItem")){
String patentNo=td.text().replace(" ","");
pc.setPatentNo(patentNo);
}
if (td.attr("width").equals("294") && td.attr("bgcolor").equals("#FFFFFF")){
String patentDate=td.text().replace(" ","");
pc.setPatentDate(patentDate);
}
if (td.attr("bgcolor").equals("#FFFFFF") && td.attr("class").equals("checkItem")){
String patentPerson=td.text().replace(" ","");
pc.setPatentPerson(patentPerson);
}
if (td.attr("bgcolor").equals("#f8f0d2") && td.text().equals(" 【地址】")){
int index=table.indexOf(td);
String patentAdress=table.get(index+1).text().replace(" ","");
pc.setPatentAdress(patentAdress);
break;
}
}
pc.setPatentName(title.text());
return pc;
}
}
现在再跑程序,速度快了一点,也能把数据爬下来了,项目源码可以在我的github下载:项目源码,感兴趣的同学可以下载来跑一下。有问题的可以在评论区交流,小弟我没什么经验,如果有什么问题还请指出,大家一起交流。
现在还有个难点没有解决就是知网的验证码验证,我这边想到的一个笨方法是缩小搜索范围,减少数据量从而减少点击下一页的次数来跳过验证码验证,不过这个需要手动改条件,重复跑很多次程序,如果有大佬有好的解决方案也可提出来。谢谢啦!
不会python?那就换一种姿势爬虫!Java爬虫技术总结的更多相关文章
- python打开文件的N种姿势
# python打开文件的N种姿势 print('[1]使用open()函数+简单for循环') f1 = open('python.txt') for line in f1: print(line. ...
- 转载 Python导入模块的几种姿势
作为一名新手Python程序员,你首先需要学习的内容之一就是如何导入模块或包.但是我注意到,那些许多年来不时使用Python的人并不是都知道Python的导入机制其实非常灵活.在本文中,我们将探讨以下 ...
- Python导入模块的几种姿势
中文翻译:http://codingpy.com/article/python-import-101/ 英文原文:http://www.blog.pythonlibrary.org/2016/03/0 ...
- 换一种思维看待PHP VS Node.js
php和javascript都是非常流行的编程语言,刚刚开始一个服务于服务端,一个服务于前端,长久以来,它们都能够和睦相处,直到有一天,一个叫做node.js的JavaScript运行环境诞生后,再加 ...
- Python下载网页的几种方法
get和post方式总结 get方式:以URL字串本身传递数据参数,在服务器端可以从'QUERY_STRING'这个变量中直接读取,效率较高,但缺乏安全性,也无法来处理复杂的数据(只能是字符串,比如在 ...
- Python字符串连接的5种方法
总结了一下Python字符串连接的5种方法: 加号 第一种,有编程经验的人,估计都知道很多语言里面是用加号连接两个字符串,Python里面也是如此直接用 "+" 来连接两个字符串: ...
- Android 反射-换一种方式编程
Android 反射-换一种方式编程 转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/59109933 本文出自[赵彦军的博客] 上一 ...
- Android图表库MPAndroidChart(六)——换一种思考方式,水平条形图的实现过程
Android图表库MPAndroidChart(六)--换一种思考方式,水平条形图的实现过程 一.基本实现 我们之前实现了条形图,现在来看下水平条形图是怎么实现的,说白了就是横起来,看下效果: 说起 ...
- python实现单例模式的三种方式及相关知识解释
python实现单例模式的三种方式及相关知识解释 模块模式 装饰器模式 父类重写new继承 单例模式作为最常用的设计模式,在面试中很可能遇到要求手写.从最近的学习python的经验而言,singlet ...
随机推荐
- A - The Water Bowls POJ - 3185 (bfs||高斯消元)
题目链接:https://vjudge.net/contest/276374#problem/A 题目大意:给你20个杯子,每一次操作,假设当前是对第i个位置进行操作,那么第i个位置,第i+1个位置, ...
- 2018-2019-2 网络对抗技术 20165320 Exp1 PC平台逆向破解
学到的新知识总结 管道:符号为| 前一个进程的输出直接作为后一个进程的输入 输出重定向:符号为> 将内容定向输入到文件中 perl:一门解释性语言,不需要预编译,直接在命令行中使用.常与输出重定 ...
- Ubuntu 下更简单的防火墙 Uncomplicated Firewall
一看名字就十分的明确“不复杂防火墙”没错,它就是 ufw,在 Ubuntu 操作系统当中已经内置,使用它可以简单快速的操作防火墙的功能,比如开关端口,访问 IP,限制连接等等等等.它与一系列 Linu ...
- centos下httpd-2.4的编译安装
httpd-2.4编译安装 依赖于更高版本的apr和apr-util apr 全称 apache portable runtime 首先停用低版本的httpd服务 service ...
- linux块设备驱动
块设备驱动程序<1>.块设备和字符设备的区别1.读取数据的单元不同,块设备读写数据的基本单元是块,字符设备的基本单元是字节.2.块设备可以随机访问,字符设备只能顺序访问. 块设备的访问:当 ...
- jquery load加载页面内ajax返回的div不能响应页面js的问题的解决方案
1. 前言 由于项目需要,需要load一个页面并保持ajax返回的div能响应其页面内的JS的click事件.这个不是 解决用jquery load加载页面到div时,不执行页面js的问题 这类问题, ...
- 转:前端页面a标签嵌套a标签效果的两种解决方案
这是由工作中的一个小改动需求得到的这个解决方案的:那个需求是这样的,如图: 需求原来是球队名字没有点击功能的,而蓝色方框两队之间的比赛点击的时候会跳转到比赛文字直播页面.现在需要要求点击球队名字要 ...
- hdu 1542 线段树+扫描线 学习
学习扫描线ing... 玄学的东西... 扫描线其实就是用一条假想的线去扫描一堆矩形,借以求出他们的面积或周长(这一篇是面积,下一篇是周长) 扫描线求面积的主要思想就是对一个二维的矩形的某一维上建立一 ...
- 性能测试二十六:环境部署之Mysql+Redis+Tomcat环境整合
系统中使用了缓存+数据库,通用读取数据规则1.先从缓存读数据,如果有,直接返回数据:2.如果没有,去数据库中读,然后再插入到缓存中,再返回数据 Mysql+Redis+Tomcat环境整合 1.修改P ...
- 读取web.config和app.config配置文件
app.config: <add key="Password" value="123456"/> C#: string TQpwd ...