—本博客為原創內容,轉載需註明本人— 前幾天有個師妹將要畢業,需要準備畢業論文,但是論文調研需要數據資料,上知網一查,十幾萬條數據!指導老師讓她手動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(" ","");
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