不會python?那就換一種姿勢爬蟲!Java爬蟲技術總結

来源:https://www.cnblogs.com/carloschan/archive/2019/03/21/10572788.html
-Advertisement-
Play Games

—本博客為原創內容,轉載需註明本人— 前幾天有個師妹將要畢業,需要準備畢業論文,但是論文調研需要數據資料,上知網一查,十幾萬條數據!指導老師讓她手動copy收集,十幾萬的數據手動copy要浪費多少時間啊,然後她就找我幫忙。我想了一下,寫個爬蟲程式去爬下來或許是個不錯的解決方案呢!之前一直聽其他人說爬 ...


—本博客為原創內容,轉載需註明本人—

前幾天有個師妹將要畢業,需要準備畢業論文,但是論文調研需要數據資料,上知網一查,十幾萬條數據!指導老師讓她手動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 
              
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • https://github.com/cynthiawupore/wq-cli ...
  • 2019-03-21 16:05:45 周期:5天 學習資料:http://how2j.cn/k/css2/css2-tutorial/238.html?p=67889 源代碼地址 GitHub:https://github.com/BenCoper/CSS Readme:簡要概括知識點詳細代碼前往 ...
  • 學習原型鏈前需要瞭解 使用構造函數模式創建的對象實例,都有一個constructor(構造函數) 屬性,該屬性指向構造函數。 function Person (name, age, job) { this.name = name; this.age = age; this.job = job; th ...
  • hmtl代碼: <input type="button" value="按鈕" id="btn"/> <input type="button" value="幹掉第一個按鈕的事件" id="btn2"/> 解綁事件有三種: 註意:用什麼方式綁定事件,就應該用對應的方式解綁事件 1.解綁事件(IE 谷 ...
  • redux 記錄一下 redux 的一些用法,如果想學習 redux,建議看 "官方文檔" ,另外推薦一本 "huzidaha" 寫的 "react小書" ,裡面講解了一些 react 和 redux 的原理。 start 運行如下命令,不瞭解 npx 的,可以看一下 "阮一峰的文章" 。 然後安裝 ...
  • 錯誤原因: 當給子組件設置 props 屬性時,如果參數類型是 Array 或 Object ,它的預設值必須是由工場函數返回,不能直接賦值 錯誤代碼: 正確代碼: 註意事項:當是 Object 類型時,而且又使用 箭頭函數時,如果設置預設值為空對象,必須加上括弧 錯誤代碼: 正確代碼: ...
  • 之前寫過一個解密json格式加密的,我以為xml的和json的差不多,是上上個星期五吧,我的同事也是在做微信公眾號裡面的消息推送解密,發現好像只能使用xml加密格式的發送到伺服器,我們去年也做過企業微信的那個消息推送的解密,真的是,感覺雖然都差不多,但是三者如果使用同樣的代碼的話完全不能復用,只是你 ...
  • 深拷貝: 之前在開發中我遇到一個很大的bug,經過我多次調試之後我發現原本應該有保存數據的地方數據全部被清空,仔細一看發現原來是被人為刪除,明明操作的是一個副本,為什麼原本也會跟著一起被刪除呢?經過瞭解我發現深拷貝這個說法。深拷貝和淺拷貝在js以及python中都是存在的,指定一個數組然後 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...