重慶分公司,新征程啟航
為企業提供網站建設、域名注冊、服務器等服務
為企業提供網站建設、域名注冊、服務器等服務
利用java怎么實現一個網頁爬蟲功能?針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
作為一家“創意+整合+營銷”的成都網站建設機構,我們在業內良好的客戶口碑。成都創新互聯公司提供從前期的網站品牌分析策劃、網站設計、成都網站設計、成都網站制作、創意表現、網頁制作、系統開發以及后續網站營銷運營等一系列服務,幫助企業打造創新的互聯網品牌經營模式與有效的網絡營銷方法,創造更大的價值。
爬蟲實現原理
網絡爬蟲基本技術處理
網絡爬蟲是數據采集的一種方法,實際項目開發中,通過爬蟲做數據采集一般只有以下幾種情況:
1) 搜索引擎
2) 競品調研
3) 輿情監控
4) 市場分析
網絡爬蟲的整體執行流程:
1) 確定一個(多個)種子網頁
2) 進行數據的內容提取
3) 將網頁中的關聯網頁連接提取出來
4) 將尚未爬取的關聯網頁內容放到一個隊列中
5) 從隊列中取出一個待爬取的頁面,判斷之前是否爬過。
6) 把沒有爬過的進行爬取,并進行之前的重復操作。
7) 直到隊列中沒有新的內容,爬蟲執行結束。
這樣完成爬蟲時,會有一些概念必須知道的:
1) 深度(depth):一般來說,表示從種子頁到當前頁的打開連接數,一般建議不要超過5層。
2) 廣度(寬度)優先和深度優先:表示爬取時的優先級。建議使用廣度優先,按深度的層級來順序爬取。
Ⅰ 在進行網頁爬蟲前,我們先針對一個飛機事故失事的文檔進行數據提取的練習,主要是溫習一下上一篇的java知識,也是為了下面爬蟲實現作一個熱身準備。
首先分析這個文檔,
,關于美國歷來每次飛機失事的數據,包含時間地點、駕駛員、死亡人數、總人數、事件描述,一共有12列,第一列是標題,下面一共有5268條數據。
現在我要對這個文件進行數據提取,并實現一下分析:
根據飛機事故的數據文檔來進行簡單數據統計。
1) 哪年出事故次數最多
2) 哪個時間段(上午8– 12,下午12– 18,晚上18– 24,凌晨0– 8)事故出現次數最多。
3) 哪年死亡人數最多
4)哪條數據的幸存率最高。
代碼實現:(一切知識從源碼獲取!)
package com.plane; import java.io.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; /** * 飛機事故統計 * @author k04 *sunwengang *2017-08-11 */ public class planeaccident { //數據獲取存取鏈表 private static Listalldata=new ArrayList<>(); public static void main(String args[]){ getData("飛行事故數據統計_Since_1908.csv"); alldata.remove(0); //System.out.println(alldata.size()); //死亡人數最多的年份 MaxDeadYear(); //事故發生次數最多的年份 MaxAccidentsYear(); //事故各個時間段發生的次數 FrequencyPeriod(); //幸村率最高的一條數據 MaximumSurvival(); } /** * 從源文件爬取數據 * getData(String filepath) * @param filepath */ public static void getData(String filepath){ File f=new File(filepath); //行讀取數據 try{ BufferedReader br=new BufferedReader(new FileReader(f)); String line=null; while((line=(br.readLine()))!=null){ alldata.add(line); } br.close(); }catch(Exception e){ e.printStackTrace(); } } /** * 記錄每年對應的死亡人數 * @throws * 并輸出死亡人數最多的年份,及該年死亡人數 */ public static void MaxDeadYear(){ //記錄年份對應死亡人數 Map map=new HashMap<>(); //時間用date顯示 SimpleDateFormat sdf=new SimpleDateFormat("MM/dd/YYYY"); //循環所有數據 for(String data:alldata){ //用逗號將數據分離,第一個是年份,第11個是死亡人數 String[] strs=data.split(","); if(strs[0]!=null){ //獲取年份 try { Date date=sdf.parse(strs[0]); int year=date.getYear(); //判斷map中是否記錄過這個數據 if(map.containsKey(year)){ //已存在,則記錄數+該年死亡人數 map.put(year, map.get(year)+Integer.parseInt(strs[10])); }else{ map.put(year, Integer.parseInt(strs[10])); } } catch (Exception e) { // TODO Auto-generated catch block } } } //System.out.println(map); //記錄死亡人數最多的年份 int max_year=-1; //記錄死亡人數 int dead_count=0; //用set無序獲取map中的key值,即年份 Set keyset=map.keySet(); // for(int year:keyset){ //當前年事故死亡最多的年份,記錄年和次數 if(map.get(year)>dead_count&&map.get(year)<10000){ max_year=year; dead_count=map.get(year); } } System.out.println("死亡人數最多的年份:"+(max_year+1901)+" 死亡人數:"+dead_count); } /** * 記錄事故次數最多的年份 * 輸出該年及事故次數 */ public static void MaxAccidentsYear(){ //存放年份,該年的事故次數 Map map=new HashMap<>(); SimpleDateFormat sdf =new SimpleDateFormat("MM/dd/YYYY"); //循環所有數據 for(String data:alldata){ String[] strs=data.split(","); if(strs[0]!=null){ try { Date date=sdf.parse(strs[0]); //獲取年份 int year=date.getYear(); //判斷是否存在記錄 if(map.containsKey(year)){ //已存在記錄,+1 map.put(year, map.get(year)+1); }else{ map.put(year, 1); } } catch (Exception e) { // TODO Auto-generated catch block } } } //記錄事故次數最多的年份 int max_year=0; //該年事故發生次數 int acc_count=0; //循環所有數據,獲取事故次數最多的年份 Set keyset=map.keySet(); for(int year:keyset){ if(map.get(year)>acc_count){ max_year=year; acc_count=map.get(year); } } //輸出結果 System.out.println("事故次數最多的年份"+(max_year+1901)+" 該年事故發生次數:"+acc_count); } /** * FrequencyPeriod() * 各個時間段發生事故的次數 */ public static void FrequencyPeriod(){ //key為時間段,value為發生事故次數 Map map=new HashMap<>(); //String數組存放時間段 String[] strsTime={"上午(6:00~12:00)","下午(12:00~18:00)","晚上(18:00~24:00)","凌晨(0:00~6:00)"}; //小時:分鐘 SimpleDateFormat sdf=new SimpleDateFormat("HH:mm"); for(String data:alldata){ String[] strs=data.split(","); //判斷時間是否記錄,未記錄則忽略 if(strs[1]!=null){ try { Date date=sdf.parse(strs[1]); //取得小時數 int hour=date.getHours(); //判斷小時數在哪個范圍中 int index=0; if(hour>=12&&hour<18){ index=1; }else if(hour>=18){ index=2; }else if(hour<6){ index=3; } //記錄到map中 if(map.containsKey(strsTime[index])){ map.put(strsTime[index], map.get(strsTime[index])+1); }else{ map.put(strsTime[index], 1); } } catch (ParseException e) { } } } /* System.out.println("各時間段發生事故次數:"); for(int i=0;i keySet = map.keySet(); for (String timeScope : keySet) { if (map.get(timeScope) > maxCount) { // 當前年就是出事故最多的年份,記錄下年和次數 maxTime = timeScope; maxCount = map.get(timeScope); } } System.out.println("發生事故次數最多的時間段:"); System.out.println(maxTime+" : "+maxCount); } /** * 獲取幸村率最高的一條數據的內容 * 返回該內容及幸存率 */ public static void MaximumSurvival(){ //存放事故信息以及該事故的幸村率 Map map=new HashMap<>(); //SimpleDateFormat sdf =new SimpleDateFormat("MM/dd/YYYY"); //事故幸存率=1-死亡率,第十一個是死亡人數,第十個是總人數 float survial=0; //循環所有數據 for(String data:alldata){ try{ String[] strs=data.split(","); //計算幸存率 float m=Float.parseFloat(strs[10]); float n=Float.parseFloat(strs[9]); survial=1-m/n; map.put(data, survial); }catch(Exception e){ } } //記錄事故次數最多的年份 float max_survial=0; //幸存率最高的數據信息 String this_data="null"; //循環所有數據,獲取事故次數最多的年份 Set keyset=map.keySet(); for(String data:keyset){ if(map.get(data)>max_survial){ this_data=data; max_survial=map.get(data); } } System.out.println("幸存率最高的事故是:"+this_data); System.out.println("幸存率為:"+survial); } }
Ⅱ 接下來我們就可以在網頁的數據上下手了。
下面先實現一個單網頁數據提取的功能。
使用的技術可以有以下幾類:
1) 原生代碼實現:
a) URL類
2) 使用第三方的URL庫
a) HttpClient庫
3) 開源爬蟲框架
a) Heritrix
b) Nutch
【一】
先使用URL類,來將當當網下搜索機械表的內容提取出來。
package com.exe1; /** * 讀取當當網下機械表的數據,并進行分析 * sunwengang 2017-08-13 20:00 */ import java.io.*; import java.net.*; public class URLDemo { public static void main(String args[]){ //確定爬取的網頁地址,此處為當當網搜機械表顯示的網頁 //網址為 http://search.dangdang.com/?key=%BB%FA%D0%B5%B1%ED&act=input String strurl="http://search.dangdang.com/?key=%BB%FA%D0%B5%B1%ED&act=input"; //建立url爬取核心對象 try { URL url=new URL(strurl); //通過url建立與網頁的連接 URLConnection conn=url.openConnection(); //通過鏈接取得網頁返回的數據 InputStream is=conn.getInputStream(); System.out.println(conn.getContentEncoding()); //一般按行讀取網頁數據,并進行內容分析 //因此用BufferedReader和InputStreamReader把字節流轉化為字符流的緩沖流 //進行轉換時,需要處理編碼格式問題 BufferedReader br=new BufferedReader(new InputStreamReader(is,"UTF-8")); //按行讀取并打印 String line=null; while((line=br.readLine())!=null){ System.out.println(line); } br.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
結果顯示:
【二】
下面嘗試將這個網頁的源代碼保存成為本地的一個文本文件,以便后續做離線分析。
如果想根據條件提取網頁中的內容信息,那么就需要使用Java的正則表達式。
正則表達式
Java.util包下提供了Pattern和Matcher這兩個類,可以根據我們給定的條件來進行數據的匹配和提取。
通過Pattern類中提供的規則字符或字符串,我們需要自己拼湊出我們的匹配規則。
正則表達式最常用的地方是用來做表單提交的數據格式驗證的。
常用的正則表達式規則一般分為兩類:
1) 內容匹配
a) \d:是否是數字
b) \w:匹配 字母、數字或下劃線
c) .:任意字符
d) [a-z]:字符是否在給定范圍內。
2) 數量匹配
a) +:1個或以上
b) *:0個或以上
c) ?:0或1次
d) {n,m}:n-m次
匹配手機電話號碼:
規則:1\\d{10}
匹配郵件地址:
規則:\\w+@\\w+.\\w+(\\.\\w+)?
通過Pattern和Matcher的配合,我們可以把一段內容中匹配我們要求的文字提取出來,方便我們來處理。
例如:將一段內容中的電話號碼提取出來。
public class PatternDemo { public static void main(String[] args) { Pattern p = Pattern.compile("1\\d{10}"); String content = "[轉讓]17610866588由 張云龍 300元轉讓,聯系電話:17610866588[轉讓]17777351513由 胡俊宏 888元轉讓,QQ:762670775,聯系電話:17777351513,可以小砍價..[求購]15019890606由 張寶紅 600元求購,聯系電話:15026815169"; Matcher m = p.matcher(content); // System.out.println(p.matcher("sf@sina").matches()); Setset = new HashSet<>(); // 通過Matcher類的group方法和find方法來進行查找和匹配 while (m.find()) { String value = m.group(); set.add(value); } System.out.println(set); } } 通過正則表達式完成超連接的連接匹配和提取
對爬取的HTML頁面來說,如果想提取連接地址,就必須找到所有超連接的標簽和對應的屬性。
規則:
廣度優先遍歷
需要有一個隊列(這里直接使用ArrayList來作為隊列)保存所有等待爬取的連接。
還需要一個Set集合記錄下所有已經爬取過的連接。
還需要一個深度值,記錄當前爬取的網頁深度,判斷是否滿足要求
此時對當當網首頁分類里的圖書進行深度為2的網頁爬取,參照上述對機械表單網頁的爬取,利用遞歸的方式進行數據獲取存到E:/dangdang_book/目錄下:
package com.exe1; /** * 讀取當當網下首頁圖書的數據,并進行分析 * 爬取深度為2 * 爬去數據存儲到E:/dangdang_book/目錄下,需自行創建 * sunwengang 2017-08-13 20:00 */ import java.io.*; import java.net.*; import java.util.*; import java.util.regex.*; public class URLDemo { //提取的數據存放到該目錄下 private static String savepath="E:/dangdang_book/"; //等待爬取的url private static Listallwaiturl=new ArrayList<>(); //爬取過的url private static Set alloverurl=new HashSet<>(); //記錄所有url的深度進行爬取判斷 private static Map allurldepth=new HashMap<>(); //爬取得深度 private static int maxdepth=2; public static void main(String args[]){ //確定爬取的網頁地址,此處為當當網首頁上的圖書分類進去的網頁 //網址為 http://book.dangdang.com/ // String strurl="http://search.dangdang.com/?key=%BB%FA%D0%B5%B1%ED&act=input"; String strurl="http://book.dangdang.com/"; workurl(strurl,1); } public static void workurl(String strurl,int depth){ //判斷當前url是否爬取過 if(!(alloverurl.contains(strurl)||depth>maxdepth)){ //建立url爬取核心對象 try { URL url=new URL(strurl); //通過url建立與網頁的連接 URLConnection conn=url.openConnection(); //通過鏈接取得網頁返回的數據 InputStream is=conn.getInputStream(); System.out.println(conn.getContentEncoding()); //一般按行讀取網頁數據,并進行內容分析 //因此用BufferedReader和InputStreamReader把字節流轉化為字符流的緩沖流 //進行轉換時,需要處理編碼格式問題 BufferedReader br=new BufferedReader(new InputStreamReader(is,"GB2312")); //按行讀取并打印 String line=null; //正則表達式的匹配規則提取該網頁的鏈接 Pattern p=Pattern.compile(""); //建立一個輸出流,用于保存文件,文件名為執行時間,以防重復 PrintWriter pw=new PrintWriter(new File(savepath+System.currentTimeMillis()+".txt")); while((line=br.readLine())!=null){ //System.out.println(line); //編寫正則,匹配超鏈接地址 pw.println(line); Matcher m=p.matcher(line); while(m.find()){ String href=m.group(); //找到超鏈接地址并截取字符串 //有無引號 href=href.substring(href.indexOf("href=")); if(href.charAt(5)=='\"'){ href=href.substring(6); }else{ href=href.substring(5); } //截取到引號或者空格或者到">"結束 try{ href=href.substring(0,href.indexOf("\"")); }catch(Exception e){ try{ href=href.substring(0,href.indexOf(" ")); }catch(Exception e1){ href=href.substring(0,href.indexOf(">")); } } if(href.startsWith("http:")||href.startsWith("https:")){ //輸出該網頁存在的鏈接 //System.out.println(href); //將url地址放到隊列中 allwaiturl.add(href); allurldepth.put(href,depth+1); } } } pw.close(); br.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } //將當前url歸列到alloverurl中 alloverurl.add(strurl); System.out.println(strurl+"網頁爬取完成,已爬取數量:"+alloverurl.size()+",剩余爬取數量:"+allwaiturl.size()); } //用遞歸的方法繼續爬取其他鏈接 String nexturl=allwaiturl.get(0); allwaiturl.remove(0); workurl(nexturl,allurldepth.get(nexturl)); } } 控制臺顯示:
本地目錄顯示:
但是,僅是深度為2的也運行不短地時間,
如果想提高爬蟲性能,那么我們就需要使用多線程來處理,例如:準備好5個線程來同時進行爬蟲操作。
這些線程需要標注出當前狀態,是在等待,還是在爬取。
如果是等待狀態,那么就需要取得集合中的一個連接,來完成爬蟲操作。
如果是爬取狀態,則在爬完以后,需要變為等待狀態。
多線程中如果想設置等待狀態,有一個方法可以實現:wait(),如果想從等待狀態喚醒,則可以使用notify()。
因此在多個線程中間我們需要一個對象來幫助我們進行線程之間的通信,以便喚醒其它線程。
多線程同時處理時,容易出現線程不安全的問題,導致數據出現錯誤。
為了保證線程的安全,就需要使用同步關鍵字,來對取得連接和放入連接操作加鎖。
多線程爬蟲實現
需要先自定義一個線程的操作類,在這個操作類中判斷不同的狀態,并且根據狀態來決定是進行wait()等待,還是取得一個新的url進行處理。
package com.exe1; /** * 讀取當當網下首頁圖書的數據,并進行分析 * 爬取深度為2 * 爬去數據存儲到E:/dangdang_book/目錄下,需自行創建 * 孫文剛 2017-08-13 20:00 */ import java.io.*; import java.net.*; import java.util.*; import java.util.regex.*; public class URLDemo { //提取的數據存放到該目錄下 private static String savepath="E:/dangdang_book/"; //等待爬取的url private static Listallwaiturl=new ArrayList<>(); //爬取過的url private static Set alloverurl=new HashSet<>(); //記錄所有url的深度進行爬取判斷 private static Map allurldepth=new HashMap<>(); //爬取得深度 private static int maxdepth=2; //生命對象,幫助進行線程的等待操作 private static Object obj=new Object(); //記錄總線程數5條 private static int MAX_THREAD=5; //記錄空閑的線程數 private static int count=0; public static void main(String args[]){ //確定爬取的網頁地址,此處為當當網首頁上的圖書分類進去的網頁 //網址為 http://book.dangdang.com/ // String strurl="http://search.dangdang.com/?key=%BB%FA%D0%B5%B1%ED&act=input"; String strurl="http://book.dangdang.com/"; //workurl(strurl,1); addurl(strurl,0); for(int i=0;i maxdepth)){ //檢測線程是否執行 System.out.println("當前執行:"+Thread.currentThread().getName()+" 爬取線程處理爬取:"+strurl); //建立url爬取核心對象 try { URL url=new URL(strurl); //通過url建立與網頁的連接 URLConnection conn=url.openConnection(); //通過鏈接取得網頁返回的數據 InputStream is=conn.getInputStream(); //提取text類型的數據 if(conn.getContentType().startsWith("text")){ } System.out.println(conn.getContentEncoding()); //一般按行讀取網頁數據,并進行內容分析 //因此用BufferedReader和InputStreamReader把字節流轉化為字符流的緩沖流 //進行轉換時,需要處理編碼格式問題 BufferedReader br=new BufferedReader(new InputStreamReader(is,"GB2312")); //按行讀取并打印 String line=null; //正則表達式的匹配規則提取該網頁的鏈接 Pattern p=Pattern.compile(""); //建立一個輸出流,用于保存文件,文件名為執行時間,以防重復 PrintWriter pw=new PrintWriter(new File(savepath+System.currentTimeMillis()+".txt")); while((line=br.readLine())!=null){ //System.out.println(line); //編寫正則,匹配超鏈接地址 pw.println(line); Matcher m=p.matcher(line); while(m.find()){ String href=m.group(); //找到超鏈接地址并截取字符串 //有無引號 href=href.substring(href.indexOf("href=")); if(href.charAt(5)=='\"'){ href=href.substring(6); }else{ href=href.substring(5); } //截取到引號或者空格或者到">"結束 try{ href=href.substring(0,href.indexOf("\"")); }catch(Exception e){ try{ href=href.substring(0,href.indexOf(" ")); }catch(Exception e1){ href=href.substring(0,href.indexOf(">")); } } if(href.startsWith("http:")||href.startsWith("https:")){ /* //輸出該網頁存在的鏈接 //System.out.println(href); //將url地址放到隊列中 allwaiturl.add(href); allurldepth.put(href,depth+1); */ //調用addurl方法 addurl(href,depth); } } } pw.close(); br.close(); } catch (Exception e) { // TODO Auto-generated catch block //e.printStackTrace(); } //將當前url歸列到alloverurl中 alloverurl.add(strurl); System.out.println(strurl+"網頁爬取完成,已爬取數量:"+alloverurl.size()+",剩余爬取數量:"+allwaiturl.size()); } /* //用遞歸的方法繼續爬取其他鏈接 String nexturl=allwaiturl.get(0); allwaiturl.remove(0); workurl(nexturl,allurldepth.get(nexturl)); */ if(allwaiturl.size()>0){ synchronized(obj){ obj.notify(); } }else{ System.out.println("爬取結束......."); } } /** * 將獲取的url放入等待隊列中,同時判斷是否已經放過 * @param href * @param depth */ public static synchronized void addurl(String href,int depth){ //將url放到隊列中 allwaiturl.add(href); //判斷url是否放過 if(!allurldepth.containsKey(href)){ allurldepth.put(href, depth+1); } } /** * 移除爬取完成的url,獲取下一個未爬取得url * @return */ public static synchronized String geturl(){ String nexturl=allwaiturl.get(0); allwaiturl.remove(0); return nexturl; } /** * 線程分配任務 */ public class MyThread extends Thread{ @Override public void run(){ //設定一個死循環,讓線程一直存在 while(true){ //判斷是否新鏈接,有則獲取 if(allwaiturl.size()>0){ //獲取url進行處理 String url=geturl(); //調用workurl方法爬取 workurl(url,allurldepth.get(url)); }else{ System.out.println("當前線程準備就緒,等待連接爬取:"+this.getName()); count++; //建立一個對象,讓線程進入等待狀態,即wait() synchronized(obj){ try{ obj.wait(); }catch(Exception e){ } } count--; } } } } } 控制臺顯示:
本地目錄顯示:
總結:
對于網頁數據爬取,用到了線程,類集處理,繼承,正則表達式等各方面的知識,從一個網頁以深度為主,廣度為基本進行爬取,獲取每一個網頁的源代碼,并寫入到一個本地的目錄下。
1、給出一個網頁鏈接,創建一個本地目錄;
2、用URL類本地連接,用字符流進行讀取,并寫入到本地;
3、利用正則表達式在按行讀取時獲取該網頁所存在的所有鏈接,以便進行深度+1的數據收集;
4、利用遞歸的方法,借助容器list,Set,Map來對鏈接進行爬取和未爬取得劃分;
5、每次爬取一個網頁時,所獲得的所有鏈接在當前基礎上深度+1,并且從未爬取隊列中移除,加入到已爬取隊列中;
6、為提升性能,在進行遞歸的時候,可以利用線程,復寫Thread的run()方法,用多線程進行網頁數據爬取;
7、直到爬取得網頁深度達到你期望的深度時,爬取結束,此時可以查看本地目錄生成的文件;
8、后續對本地生成的文件進行數據分析,即可獲取你想要的信息。
借此,我們就可以對這些數據進行歸約,分析,處理,來獲取我們想要的信息。
關于利用java怎么實現一個網頁爬蟲功能問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注創新互聯行業資訊頻道了解更多相關知識。
當前標題:利用java怎么實現一個網頁爬蟲功能
瀏覽路徑:http://www.xueling.net.cn/article/gcihdd.html