不会python?那就换一种姿势爬虫!Java爬虫技术总结

泪湿孤枕 提交于 2020-04-27 18:17:02

—本博客为原创内容,转载需注明本人—

前几天有个师妹将要毕业,需要准备毕业论文,但是论文调研需要数据资料,上知网一查,十几万条数据!指导老师让她手动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("&nbsp;",""); pc.setPatentNo(patentNo); } if (td.attr("width").equals("294") && td.attr("bgcolor").equals("#FFFFFF")){ String patentDate=td.text().replace("&nbsp;",""); pc.setPatentDate(patentDate); } if (td.attr("bgcolor").equals("#FFFFFF") && td.attr("class").equals("checkItem")){ String patentPerson=td.text().replace("&nbsp;",""); 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("&nbsp;",""); 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("&nbsp;",""); pc.setPatentNo(patentNo); } if (td.attr("width").equals("294") && td.attr("bgcolor").equals("#FFFFFF")){ String patentDate=td.text().replace("&nbsp;",""); pc.setPatentDate(patentDate); } if (td.attr("bgcolor").equals("#FFFFFF") && td.attr("class").equals("checkItem")){ String patentPerson=td.text().replace("&nbsp;",""); 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("&nbsp;",""); pc.setPatentAdress(patentAdress); break; } } pc.setPatentName(title.text()); return pc; } } 

现在再跑程序,速度快了一点,也能把数据爬下来了,项目源码可以在我的github下载:项目源码,感兴趣的同学可以下载来跑一下。有问题的可以在评论区交流,小弟我没什么经验,如果有什么问题还请指出,大家一起交流。

现在还有个难点没有解决就是知网的验证码验证,我这边想到的一个笨方法是缩小搜索范围,减少数据量从而减少点击下一页的次数来跳过验证码验证,不过这个需要手动改条件,重复跑很多次程序,如果有大佬有好的解决方案也可提出来。谢谢啦!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!