不會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("&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下載:項目源碼,感興趣的同學可以下載來跑一下。有問題的可以在評論區交流,小弟我沒什么經驗,如果有什么問題還請指出,大家一起交流。

現在還有個難點沒有解決就是知網的驗證碼驗證,我這邊想到的一個笨方法是縮小搜索范圍,減少數據量從而減少點擊下一頁的次數來跳過驗證碼驗證,不過這個需要手動改條件,重復跑很多次程序,如果有大佬有好的解決方案也可提出來。謝謝啦!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM