使用java實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲(chóng)
接著上面一篇對(duì)爬蟲(chóng)需要的java知識(shí),這一篇目的就是在于網(wǎng)絡(luò)爬蟲(chóng)的實(shí)現(xiàn),對(duì)數(shù)據(jù)的獲取,以便分析。----->
爬蟲(chóng)實(shí)現(xiàn)原理
網(wǎng)絡(luò)爬蟲(chóng)基本技術(shù)處理
網(wǎng)絡(luò)爬蟲(chóng)是數(shù)據(jù)采集的一種方法,實(shí)際項(xiàng)目開(kāi)發(fā)中,通過(guò)爬蟲(chóng)做數(shù)據(jù)采集一般只有以下幾種情況:
1)搜索引擎
2)競(jìng)品調(diào)研
3)輿情監(jiān)控
4)市場(chǎng)分析
網(wǎng)絡(luò)爬蟲(chóng)的整體執(zhí)行流程:
1)確定一個(gè)(多個(gè))種子網(wǎng)頁(yè)
2)進(jìn)行數(shù)據(jù)的內(nèi)容提取
3)將網(wǎng)頁(yè)中的關(guān)聯(lián)網(wǎng)頁(yè)連接提取出來(lái)
4)將尚未爬取的關(guān)聯(lián)網(wǎng)頁(yè)內(nèi)容放到一個(gè)隊(duì)列中
5)從隊(duì)列中取出一個(gè)待爬取的頁(yè)面,判斷之前是否爬過(guò)。
6)把沒(méi)有爬過(guò)的進(jìn)行爬取,并進(jìn)行之前的重復(fù)操作。
7)直到隊(duì)列中沒(méi)有新的內(nèi)容,爬蟲(chóng)執(zhí)行結(jié)束。
這樣完成爬蟲(chóng)時(shí),會(huì)有一些概念必須知道的:
1)深度(depth):一般來(lái)說(shuō),表示從種子頁(yè)到當(dāng)前頁(yè)的打開(kāi)連接數(shù),一般建議不要超過(guò)5層。
2)廣度(寬度)優(yōu)先和深度優(yōu)先:表示爬取時(shí)的優(yōu)先級(jí)。建議使用廣度優(yōu)先,按深度的層級(jí)來(lái)順序爬取。
Ⅰ 在進(jìn)行網(wǎng)頁(yè)爬蟲(chóng)前,我們先針對(duì)一個(gè)飛機(jī)事故失事的文檔進(jìn)行數(shù)據(jù)提取的練習(xí),主要是溫習(xí)一下上一篇的java知識(shí),也是為了下面爬蟲(chóng)實(shí)現(xiàn)作一個(gè)熱身準(zhǔn)備。
首先分析這個(gè)文檔,

,關(guān)于美國(guó)歷來(lái)每次飛機(jī)失事的數(shù)據(jù),包含時(shí)間地點(diǎn)、駕駛員、死亡人數(shù)、總?cè)藬?shù)、事件描述,一共有12列,第一列是標(biāo)題,下面一共有5268條數(shù)據(jù)。
現(xiàn)在我要對(duì)這個(gè)文件進(jìn)行數(shù)據(jù)提取,并實(shí)現(xiàn)一下分析:
根據(jù)飛機(jī)事故的數(shù)據(jù)文檔來(lái)進(jìn)行簡(jiǎn)單數(shù)據(jù)統(tǒng)計(jì)。
1)哪年出事故次數(shù)最多
2)哪個(gè)時(shí)間段(上午 8 –12,下午 12 –18,晚上 18 –24,凌晨 0 –8 )事故出現(xiàn)次數(shù)最多。
3)哪年死亡人數(shù)最多
4)哪條數(shù)據(jù)的幸存率最高。


代碼實(shí)現(xiàn):(一切知識(shí)從源碼獲?。。?/p>
package com.plane;
import java.io.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 飛機(jī)事故統(tǒng)計(jì)
* @author k04
*sunwengang
*2017-08-11
*/
public class planeaccident {
//數(shù)據(jù)獲取存取鏈表
private static List<String> alldata=new ArrayList<>();
public static void main(String args[]){
getData("飛行事故數(shù)據(jù)統(tǒng)計(jì)_Since_1908.csv");
alldata.remove(0);
//System.out.println(alldata.size());
//死亡人數(shù)最多的年份
MaxDeadYear();
//事故發(fā)生次數(shù)最多的年份
MaxAccidentsYear();
//事故各個(gè)時(shí)間段發(fā)生的次數(shù)
FrequencyPeriod();
//幸村率最高的一條數(shù)據(jù)
MaximumSurvival();
}
/**
* 從源文件爬取數(shù)據(jù)
* getData(String filepath)
* @param filepath
*/
public static void getData(String filepath){
File f=new File(filepath);
//行讀取數(shù)據(jù)
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();
}
}
/**
* 記錄每年對(duì)應(yīng)的死亡人數(shù)
* @throws
* 并輸出死亡人數(shù)最多的年份,及該年死亡人數(shù)
*/
public static void MaxDeadYear(){
//記錄年份對(duì)應(yīng)死亡人數(shù)
Map<Integer,Integer> map=new HashMap<>();
//時(shí)間用date顯示
SimpleDateFormat sdf=new SimpleDateFormat("MM/dd/YYYY");
//循環(huán)所有數(shù)據(jù)
for(String data:alldata){
//用逗號(hào)將數(shù)據(jù)分離,第一個(gè)是年份,第11個(gè)是死亡人數(shù)
String[] strs=data.split(",");
if(strs[0]!=null){
//獲取年份
try {
Date date=sdf.parse(strs[0]);
int year=date.getYear();
//判斷map中是否記錄過(guò)這個(gè)數(shù)據(jù)
if(map.containsKey(year)){
//已存在,則記錄數(shù)+該年死亡人數(shù)
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);
//記錄死亡人數(shù)最多的年份
int max_year=-1;
//記錄死亡人數(shù)
int dead_count=0;
//用set無(wú)序獲取map中的key值,即年份
Set<Integer> keyset=map.keySet();
//
for(int year:keyset){
//當(dāng)前年事故死亡最多的年份,記錄年和次數(shù)
if(map.get(year)>dead_count&&map.get(year)<10000){
max_year=year;
dead_count=map.get(year);
}
}
System.out.println("死亡人數(shù)最多的年份:"+(max_year+1901)+" 死亡人數(shù):"+dead_count);
}
/**
* 記錄事故次數(shù)最多的年份
* 輸出該年及事故次數(shù)
*/
public static void MaxAccidentsYear(){
//存放年份,該年的事故次數(shù)
Map<Integer,Integer> map=new HashMap<>();
SimpleDateFormat sdf =new SimpleDateFormat("MM/dd/YYYY");
//循環(huán)所有數(shù)據(jù)
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
}
}
}
//記錄事故次數(shù)最多的年份
int max_year=0;
//該年事故發(fā)生次數(shù)
int acc_count=0;
//循環(huán)所有數(shù)據(jù),獲取事故次數(shù)最多的年份
Set<Integer> keyset=map.keySet();
for(int year:keyset){
if(map.get(year)>acc_count){
max_year=year;
acc_count=map.get(year);
}
}
//輸出結(jié)果
System.out.println("事故次數(shù)最多的年份"+(max_year+1901)+" 該年事故發(fā)生次數(shù):"+acc_count);
}
/**
* FrequencyPeriod()
* 各個(gè)時(shí)間段發(fā)生事故的次數(shù)
*/
public static void FrequencyPeriod(){
//key為時(shí)間段,value為發(fā)生事故次數(shù)
Map<String,Integer> map=new HashMap<>();
//String數(shù)組存放時(shí)間段
String[] strsTime={"上午(6:00~12:00)","下午(12:00~18:00)","晚上(18:00~24:00)","凌晨(0:00~6:00)"};
//小時(shí):分鐘
SimpleDateFormat sdf=new SimpleDateFormat("HH:mm");
for(String data:alldata){
String[] strs=data.split(",");
//判斷時(shí)間是否記錄,未記錄則忽略
if(strs[1]!=null){
try {
Date date=sdf.parse(strs[1]);
//取得小時(shí)數(shù)
int hour=date.getHours();
//判斷小時(shí)數(shù)在哪個(gè)范圍中
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("各時(shí)間段發(fā)生事故次數(shù):");
for(int i=0;i<strsTime.length;i++){
System.out.println(strsTime[i]+" : "+map.get(strsTime[i]));
}
*/
// 記錄出事故最多的時(shí)間范圍
String maxTime = null;
// 記錄出事故最多的次數(shù)
int maxCount = 0;
Set<String> keySet = map.keySet();
for (String timeScope : keySet) {
if (map.get(timeScope) > maxCount) {
// 當(dāng)前年就是出事故最多的年份,記錄下年和次數(shù)
maxTime = timeScope;
maxCount = map.get(timeScope);
}
}
System.out.println("發(fā)生事故次數(shù)最多的時(shí)間段:");
System.out.println(maxTime+" : "+maxCount);
}
/**
* 獲取幸村率最高的一條數(shù)據(jù)的內(nèi)容
* 返回該內(nèi)容及幸存率
*/
public static void MaximumSurvival(){
//存放事故信息以及該事故的幸村率
Map<String,Float> map=new HashMap<>();
//SimpleDateFormat sdf =new SimpleDateFormat("MM/dd/YYYY");
//事故幸存率=1-死亡率,第十一個(gè)是死亡人數(shù),第十個(gè)是總?cè)藬?shù)
float survial=0;
//循環(huán)所有數(shù)據(jù)
for(String data:alldata){
try{
String[] strs=data.split(",");
//計(jì)算幸存率
float m=Float.parseFloat(strs[10]);
float n=Float.parseFloat(strs[9]);
survial=1-m/n;
map.put(data, survial);
}catch(Exception e){
}
}
//記錄事故次數(shù)最多的年份
float max_survial=0;
//幸存率最高的數(shù)據(jù)信息
String this_data="null";
//循環(huán)所有數(shù)據(jù),獲取事故次數(shù)最多的年份
Set<String> 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);
}
}
Ⅱ 接下來(lái)我們就可以在網(wǎng)頁(yè)的數(shù)據(jù)上下手了。
下面先實(shí)現(xiàn)一個(gè)單網(wǎng)頁(yè)數(shù)據(jù)提取的功能。
使用的技術(shù)可以有以下幾類(lèi):
1)原生代碼實(shí)現(xiàn):
a)URL類(lèi)
2)使用第三方的URL庫(kù)
a)HttpClient庫(kù)
3)開(kāi)源爬蟲(chóng)框架
a)Heritrix
b)Nutch
【一】
先使用URL類(lèi),來(lái)將當(dāng)當(dāng)網(wǎng)下搜索機(jī)械表的內(nèi)容提取出來(lái)。

package com.exe1;
/**
* 讀取當(dāng)當(dāng)網(wǎng)下機(jī)械表的數(shù)據(jù),并進(jìn)行分析
* sunwengang 2017-08-13 20:00
*/
import java.io.*;
import java.net.*;
public class URLDemo {
public static void main(String args[]){
//確定爬取的網(wǎng)頁(yè)地址,此處為當(dāng)當(dāng)網(wǎng)搜機(jī)械表顯示的網(wǎng)頁(yè)
//網(wǎng)址為 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爬取核心對(duì)象
try {
URL url=new URL(strurl);
//通過(guò)url建立與網(wǎng)頁(yè)的連接
URLConnection conn=url.openConnection();
//通過(guò)鏈接取得網(wǎng)頁(yè)返回的數(shù)據(jù)
InputStream is=conn.getInputStream();
System.out.println(conn.getContentEncoding());
//一般按行讀取網(wǎng)頁(yè)數(shù)據(jù),并進(jìn)行內(nèi)容分析
//因此用BufferedReader和InputStreamReader把字節(jié)流轉(zhuǎn)化為字符流的緩沖流
//進(jìn)行轉(zhuǎn)換時(shí),需要處理編碼格式問(wèn)題
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();
}
}
}
結(jié)果顯示:

【二】
下面嘗試將這個(gè)網(wǎng)頁(yè)的源代碼保存成為本地的一個(gè)文本文件,以便后續(xù)做離線(xiàn)分析。
如果想根據(jù)條件提取網(wǎng)頁(yè)中的內(nèi)容信息,那么就需要使用Java的正則表達(dá)式。
正則表達(dá)式
Java.util包下提供了Pattern和Matcher這兩個(gè)類(lèi),可以根據(jù)我們給定的條件來(lái)進(jìn)行數(shù)據(jù)的匹配和提取。
通過(guò)Pattern類(lèi)中提供的規(guī)則字符或字符串,我們需要自己拼湊出我們的匹配規(guī)則。
正則表達(dá)式最常用的地方是用來(lái)做表單提交的數(shù)據(jù)格式驗(yàn)證的。
常用的正則表達(dá)式規(guī)則一般分為兩類(lèi):
1)內(nèi)容匹配
a)\d:是否是數(shù)字
b)\w:匹配 字母、數(shù)字或下劃線(xiàn)
c).:任意字符
d)[a-z]:字符是否在給定范圍內(nèi)。
2)數(shù)量匹配
a)+:1個(gè)或以上
b)*:0個(gè)或以上
c)?:0或1次
d){n,m}:n-m次
匹配手機(jī)電話(huà)號(hào)碼:
規(guī)則:1\\d{10}
匹配郵件地址:
規(guī)則:\\w+@\\w+.\\w+(\\.\\w+)?
通過(guò)Pattern和Matcher的配合,我們可以把一段內(nèi)容中匹配我們要求的文字提取出來(lái),方便我們來(lái)處理。
例如:將一段內(nèi)容中的電話(huà)號(hào)碼提取出來(lái)。
public class PatternDemo {
public static void main(String[] args) {
Pattern p = Pattern.compile("1\\d{10}");
String content = "<div><div class='jg666'>[轉(zhuǎn)讓]<a href='/17610866588' title='手機(jī)號(hào)碼17610866588估價(jià)評(píng)估_值多少錢(qián)_歸屬地查詢(xún)_測(cè)吉兇_數(shù)字含義_求購(gòu)轉(zhuǎn)讓信息' class='lj44'>17610866588</a>由 張?jiān)讫?300元轉(zhuǎn)讓,聯(lián)系電話(huà):17610866588</div><div class='jg666'>[轉(zhuǎn)讓]<a href='/17777351513' title='手機(jī)號(hào)碼17777351513估價(jià)評(píng)估_值多少錢(qián)_歸屬地查詢(xún)_測(cè)吉兇_數(shù)字含義_求購(gòu)轉(zhuǎn)讓信息' class='lj44'>17777351513</a>由 胡俊宏 888元轉(zhuǎn)讓,QQ:762670775,聯(lián)系電話(huà):17777351513,可以小砍價(jià)..</div><div class='jg666'>[求購(gòu)]<a href='/15019890606' title='手機(jī)號(hào)碼15019890606估價(jià)評(píng)估_值多少錢(qián)_歸屬地查詢(xún)_測(cè)吉兇_數(shù)字含義_求購(gòu)轉(zhuǎn)讓信息' class='lj44'>15019890606</a>由 張寶紅 600元求購(gòu),聯(lián)系電話(huà):15026815169</div><div class='jg666'>";
Matcher m = p.matcher(content);
// System.out.println(p.matcher("sf@sina").matches());
Set<String> set = new HashSet<>();
// 通過(guò)Matcher類(lèi)的group方法和find方法來(lái)進(jìn)行查找和匹配
while (m.find()) {
String value = m.group();
set.add(value);
}
System.out.println(set);
}
}
通過(guò)正則表達(dá)式完成超連接的連接匹配和提取
對(duì)爬取的HTML頁(yè)面來(lái)說(shuō),如果想提取連接地址,就必須找到所有超連接的標(biāo)簽和對(duì)應(yīng)的屬性。
超連接標(biāo)簽是<a></a>,保存連接的屬性是:href。
<a href=”…”>…</a>
規(guī)則:
<a .*href=.+</a>
廣度優(yōu)先遍歷
需要有一個(gè)隊(duì)列(這里直接使用ArrayList來(lái)作為隊(duì)列)保存所有等待爬取的連接。
還需要一個(gè)Set集合記錄下所有已經(jīng)爬取過(guò)的連接。
還需要一個(gè)深度值,記錄當(dāng)前爬取的網(wǎng)頁(yè)深度,判斷是否滿(mǎn)足要求
此時(shí)對(duì)當(dāng)當(dāng)網(wǎng)首頁(yè)分類(lèi)里的圖書(shū)進(jìn)行深度為2的網(wǎng)頁(yè)爬取,參照上述對(duì)機(jī)械表單網(wǎng)頁(yè)的爬取,利用遞歸的方式進(jìn)行數(shù)據(jù)獲取存到E:/dangdang_book/目錄下:
package com.exe1;
/**
* 讀取當(dāng)當(dāng)網(wǎng)下首頁(yè)圖書(shū)的數(shù)據(jù),并進(jìn)行分析
* 爬取深度為2
* 爬去數(shù)據(jù)存儲(chǔ)到E:/dangdang_book/目錄下,需自行創(chuàng)建
* sunwengang 2017-08-13 20:00
*/
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.regex.*;
public class URLDemo {
//提取的數(shù)據(jù)存放到該目錄下
private static String savepath="E:/dangdang_book/";
//等待爬取的url
private static List<String> allwaiturl=new ArrayList<>();
//爬取過(guò)的url
private static Set<String> alloverurl=new HashSet<>();
//記錄所有url的深度進(jìn)行爬取判斷
private static Map<String,Integer> allurldepth=new HashMap<>();
//爬取得深度
private static int maxdepth=2;
public static void main(String args[]){
//確定爬取的網(wǎng)頁(yè)地址,此處為當(dāng)當(dāng)網(wǎng)首頁(yè)上的圖書(shū)分類(lèi)進(jìn)去的網(wǎng)頁(yè)
//網(wǎng)址為 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){
//判斷當(dāng)前url是否爬取過(guò)
if(!(alloverurl.contains(strurl)||depth>maxdepth)){
//建立url爬取核心對(duì)象
try {
URL url=new URL(strurl);
//通過(guò)url建立與網(wǎng)頁(yè)的連接
URLConnection conn=url.openConnection();
//通過(guò)鏈接取得網(wǎng)頁(yè)返回的數(shù)據(jù)
InputStream is=conn.getInputStream();
System.out.println(conn.getContentEncoding());
//一般按行讀取網(wǎng)頁(yè)數(shù)據(jù),并進(jìn)行內(nèi)容分析
//因此用BufferedReader和InputStreamReader把字節(jié)流轉(zhuǎn)化為字符流的緩沖流
//進(jìn)行轉(zhuǎn)換時(shí),需要處理編碼格式問(wèn)題
BufferedReader br=new BufferedReader(new InputStreamReader(is,"GB2312"));
//按行讀取并打印
String line=null;
//正則表達(dá)式的匹配規(guī)則提取該網(wǎng)頁(yè)的鏈接
Pattern p=Pattern.compile("<a .*href=.+</a>");
//建立一個(gè)輸出流,用于保存文件,文件名為執(zhí)行時(shí)間,以防重復(fù)
PrintWriter pw=new PrintWriter(new File(savepath+System.currentTimeMillis()+".txt"));
while((line=br.readLine())!=null){
//System.out.println(line);
//編寫(xiě)正則,匹配超鏈接地址
pw.println(line);
Matcher m=p.matcher(line);
while(m.find()){
String href=m.group();
//找到超鏈接地址并截取字符串
//有無(wú)引號(hào)
href=href.substring(href.indexOf("href="));
if(href.charAt(5)=='\"'){
href=href.substring(6);
}else{
href=href.substring(5);
}
//截取到引號(hào)或者空格或者到">"結(jié)束
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:")){
//輸出該網(wǎng)頁(yè)存在的鏈接
//System.out.println(href);
//將url地址放到隊(duì)列中
allwaiturl.add(href);
allurldepth.put(href,depth+1);
}
}
}
pw.close();
br.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//將當(dāng)前url歸列到alloverurl中
alloverurl.add(strurl);
System.out.println(strurl+"網(wǎng)頁(yè)爬取完成,已爬取數(shù)量:"+alloverurl.size()+",剩余爬取數(shù)量:"+allwaiturl.size());
}
//用遞歸的方法繼續(xù)爬取其他鏈接
String nexturl=allwaiturl.get(0);
allwaiturl.remove(0);
workurl(nexturl,allurldepth.get(nexturl));
}
}
控制臺(tái)顯示:

本地目錄顯示:

但是,僅是深度為2的也運(yùn)行不短地時(shí)間,
如果想提高爬蟲(chóng)性能,那么我們就需要使用多線(xiàn)程來(lái)處理,例如:準(zhǔn)備好5個(gè)線(xiàn)程來(lái)同時(shí)進(jìn)行爬蟲(chóng)操作。
這些線(xiàn)程需要標(biāo)注出當(dāng)前狀態(tài),是在等待,還是在爬取。
如果是等待狀態(tài),那么就需要取得集合中的一個(gè)連接,來(lái)完成爬蟲(chóng)操作。
如果是爬取狀態(tài),則在爬完以后,需要變?yōu)榈却隣顟B(tài)。
多線(xiàn)程中如果想設(shè)置等待狀態(tài),有一個(gè)方法可以實(shí)現(xiàn):wait(),如果想從等待狀態(tài)喚醒,則可以使用notify()。
因此在多個(gè)線(xiàn)程中間我們需要一個(gè)對(duì)象來(lái)幫助我們進(jìn)行線(xiàn)程之間的通信,以便喚醒其它線(xiàn)程。
多線(xiàn)程同時(shí)處理時(shí),容易出現(xiàn)線(xiàn)程不安全的問(wèn)題,導(dǎo)致數(shù)據(jù)出現(xiàn)錯(cuò)誤。
為了保證線(xiàn)程的安全,就需要使用同步關(guān)鍵字,來(lái)對(duì)取得連接和放入連接操作加鎖。
多線(xiàn)程爬蟲(chóng)實(shí)現(xiàn)
需要先自定義一個(gè)線(xiàn)程的操作類(lèi),在這個(gè)操作類(lèi)中判斷不同的狀態(tài),并且根據(jù)狀態(tài)來(lái)決定是進(jìn)行wait()等待,還是取得一個(gè)新的url進(jìn)行處理。
package com.exe1;
/**
* 讀取當(dāng)當(dāng)網(wǎng)下首頁(yè)圖書(shū)的數(shù)據(jù),并進(jìn)行分析
* 爬取深度為2
* 爬去數(shù)據(jù)存儲(chǔ)到E:/dangdang_book/目錄下,需自行創(chuàng)建
* 孫文剛 2017-08-13 20:00
*/
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.regex.*;
public class URLDemo {
//提取的數(shù)據(jù)存放到該目錄下
private static String savepath="E:/dangdang_book/";
//等待爬取的url
private static List<String> allwaiturl=new ArrayList<>();
//爬取過(guò)的url
private static Set<String> alloverurl=new HashSet<>();
//記錄所有url的深度進(jìn)行爬取判斷
private static Map<String,Integer> allurldepth=new HashMap<>();
//爬取得深度
private static int maxdepth=2;
//生命對(duì)象,幫助進(jìn)行線(xiàn)程的等待操作
private static Object obj=new Object();
//記錄總線(xiàn)程數(shù)5條
private static int MAX_THREAD=5;
//記錄空閑的線(xiàn)程數(shù)
private static int count=0;
public static void main(String args[]){
//確定爬取的網(wǎng)頁(yè)地址,此處為當(dāng)當(dāng)網(wǎng)首頁(yè)上的圖書(shū)分類(lèi)進(jìn)去的網(wǎng)頁(yè)
//網(wǎng)址為 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<MAX_THREAD;i++){
new URLDemo().new MyThread().start();
}
}
/**
* 網(wǎng)頁(yè)數(shù)據(jù)爬取
* @param strurl
* @param depth
*/
public static void workurl(String strurl,int depth){
//判斷當(dāng)前url是否爬取過(guò)
if(!(alloverurl.contains(strurl)||depth>maxdepth)){
//檢測(cè)線(xiàn)程是否執(zhí)行
System.out.println("當(dāng)前執(zhí)行:"+Thread.currentThread().getName()+" 爬取線(xiàn)程處理爬?。?+strurl);
//建立url爬取核心對(duì)象
try {
URL url=new URL(strurl);
//通過(guò)url建立與網(wǎng)頁(yè)的連接
URLConnection conn=url.openConnection();
//通過(guò)鏈接取得網(wǎng)頁(yè)返回的數(shù)據(jù)
InputStream is=conn.getInputStream();
//提取text類(lèi)型的數(shù)據(jù)
if(conn.getContentType().startsWith("text")){
}
System.out.println(conn.getContentEncoding());
//一般按行讀取網(wǎng)頁(yè)數(shù)據(jù),并進(jìn)行內(nèi)容分析
//因此用BufferedReader和InputStreamReader把字節(jié)流轉(zhuǎn)化為字符流的緩沖流
//進(jìn)行轉(zhuǎn)換時(shí),需要處理編碼格式問(wèn)題
BufferedReader br=new BufferedReader(new InputStreamReader(is,"GB2312"));
//按行讀取并打印
String line=null;
//正則表達(dá)式的匹配規(guī)則提取該網(wǎng)頁(yè)的鏈接
Pattern p=Pattern.compile("<a .*href=.+</a>");
//建立一個(gè)輸出流,用于保存文件,文件名為執(zhí)行時(shí)間,以防重復(fù)
PrintWriter pw=new PrintWriter(new File(savepath+System.currentTimeMillis()+".txt"));
while((line=br.readLine())!=null){
//System.out.println(line);
//編寫(xiě)正則,匹配超鏈接地址
pw.println(line);
Matcher m=p.matcher(line);
while(m.find()){
String href=m.group();
//找到超鏈接地址并截取字符串
//有無(wú)引號(hào)
href=href.substring(href.indexOf("href="));
if(href.charAt(5)=='\"'){
href=href.substring(6);
}else{
href=href.substring(5);
}
//截取到引號(hào)或者空格或者到">"結(jié)束
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:")){
/*
//輸出該網(wǎng)頁(yè)存在的鏈接
//System.out.println(href);
//將url地址放到隊(duì)列中
allwaiturl.add(href);
allurldepth.put(href,depth+1);
*/
//調(diào)用addurl方法
addurl(href,depth);
}
}
}
pw.close();
br.close();
} catch (Exception e) {
// TODO Auto-generated catch block
//e.printStackTrace();
}
//將當(dāng)前url歸列到alloverurl中
alloverurl.add(strurl);
System.out.println(strurl+"網(wǎng)頁(yè)爬取完成,已爬取數(shù)量:"+alloverurl.size()+",剩余爬取數(shù)量:"+allwaiturl.size());
}
/*
//用遞歸的方法繼續(xù)爬取其他鏈接
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("爬取結(jié)束.......");
}
}
/**
* 將獲取的url放入等待隊(duì)列中,同時(shí)判斷是否已經(jīng)放過(guò)
* @param href
* @param depth
*/
public static synchronized void addurl(String href,int depth){
//將url放到隊(duì)列中
allwaiturl.add(href);
//判斷url是否放過(guò)
if(!allurldepth.containsKey(href)){
allurldepth.put(href, depth+1);
}
}
/**
* 移除爬取完成的url,獲取下一個(gè)未爬取得url
* @return
*/
public static synchronized String geturl(){
String nexturl=allwaiturl.get(0);
allwaiturl.remove(0);
return nexturl;
}
/**
* 線(xiàn)程分配任務(wù)
*/
public class MyThread extends Thread{
@Override
public void run(){
//設(shè)定一個(gè)死循環(huán),讓線(xiàn)程一直存在
while(true){
//判斷是否新鏈接,有則獲取
if(allwaiturl.size()>0){
//獲取url進(jìn)行處理
String url=geturl();
//調(diào)用workurl方法爬取
workurl(url,allurldepth.get(url));
}else{
System.out.println("當(dāng)前線(xiàn)程準(zhǔn)備就緒,等待連接爬?。?+this.getName());
count++;
//建立一個(gè)對(duì)象,讓線(xiàn)程進(jìn)入等待狀態(tài),即wait()
synchronized(obj){
try{
obj.wait();
}catch(Exception e){
}
}
count--;
}
}
}
}
}
控制臺(tái)顯示:

本地目錄顯示:

總結(jié):
對(duì)于網(wǎng)頁(yè)數(shù)據(jù)爬取,用到了線(xiàn)程,類(lèi)集處理,繼承,正則表達(dá)式等各方面的知識(shí),從一個(gè)網(wǎng)頁(yè)以深度為主,廣度為基本進(jìn)行爬取,獲取每一個(gè)網(wǎng)頁(yè)的源代碼,并寫(xiě)入到一個(gè)本地的目錄下。
1、給出一個(gè)網(wǎng)頁(yè)鏈接,創(chuàng)建一個(gè)本地目錄;
2、用URL類(lèi)本地連接,用字符流進(jìn)行讀取,并寫(xiě)入到本地;
3、利用正則表達(dá)式在按行讀取時(shí)獲取該網(wǎng)頁(yè)所存在的所有鏈接,以便進(jìn)行深度+1的數(shù)據(jù)收集;
4、利用遞歸的方法,借助容器list,Set,Map來(lái)對(duì)鏈接進(jìn)行爬取和未爬取得劃分;
5、每次爬取一個(gè)網(wǎng)頁(yè)時(shí),所獲得的所有鏈接在當(dāng)前基礎(chǔ)上深度+1,并且從未爬取隊(duì)列中移除,加入到已爬取隊(duì)列中;
6、為提升性能,在進(jìn)行遞歸的時(shí)候,可以利用線(xiàn)程,復(fù)寫(xiě)Thread的run()方法,用多線(xiàn)程進(jìn)行網(wǎng)頁(yè)數(shù)據(jù)爬取;
7、直到爬取得網(wǎng)頁(yè)深度達(dá)到你期望的深度時(shí),爬取結(jié)束,此時(shí)可以查看本地目錄生成的文件;
8、后續(xù)對(duì)本地生成的文件進(jìn)行數(shù)據(jù)分析,即可獲取你想要的信息。
借此,我們就可以對(duì)這些數(shù)據(jù)進(jìn)行歸約,分析,處理,來(lái)獲取我們想要的信息。
這也是大數(shù)據(jù)數(shù)據(jù)收集的一個(gè)基礎(chǔ)。
到此這篇關(guān)于使用java實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲(chóng)的文章就介紹到這了,更多相關(guān)java實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲(chóng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- java編程實(shí)現(xiàn)簡(jiǎn)單的網(wǎng)絡(luò)爬蟲(chóng)示例過(guò)程
- Java 使用maven實(shí)現(xiàn)Jsoup簡(jiǎn)單爬蟲(chóng)案例詳解
- Java 實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲(chóng)框架詳細(xì)代碼
- 半小時(shí)實(shí)現(xiàn)Java手?jǐn)]網(wǎng)絡(luò)爬蟲(chóng)框架(附完整源碼)
- Java實(shí)現(xiàn)的爬蟲(chóng)抓取圖片并保存操作示例
- java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)爬蟲(chóng)代碼示例
- java實(shí)現(xiàn)網(wǎng)頁(yè)爬蟲(chóng)的示例講解
- java實(shí)現(xiàn)簡(jiǎn)單的爬蟲(chóng)之今日頭條
- Java爬蟲(chóng) 信息抓取的實(shí)現(xiàn)
- Java實(shí)現(xiàn)爬蟲(chóng)
相關(guān)文章
詳解使用spring aop實(shí)現(xiàn)業(yè)務(wù)層mysql 讀寫(xiě)分離
本篇文章主要介紹了使用spring aop實(shí)現(xiàn)業(yè)務(wù)層mysql 讀寫(xiě)分離,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01
詳解spring cloud hystrix請(qǐng)求緩存(request cache)
這篇文章主要介紹了詳解spring cloud hystrix請(qǐng)求緩存(request cache),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
Java設(shè)計(jì)模式之觀(guān)察者模式(Observer模式)
這篇文章主要介紹了Java設(shè)計(jì)模式之觀(guān)察者模式(Observer模式),文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04
Java中新建一個(gè)文件、目錄及路徑操作實(shí)例
這篇文章主要給大家介紹了關(guān)于Java中新建一個(gè)文件、目錄及路徑操作的相關(guān)資料,新建文件、目錄及路徑是我們?nèi)粘i_(kāi)發(fā)中經(jīng)常會(huì)遇到的一個(gè)需求,本文通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12
Java經(jīng)驗(yàn)點(diǎn)滴:處理沒(méi)有被捕獲的異常
Java經(jīng)驗(yàn)點(diǎn)滴:處理沒(méi)有被捕獲的異常...2006-12-12
Javaweb實(shí)戰(zhàn)之實(shí)現(xiàn)蛋糕訂購(gòu)系統(tǒng)
隨著網(wǎng)絡(luò)的普及與發(fā)展,網(wǎng)上購(gòu)物逐漸成為一種主流消費(fèi)的方式。這篇文章主要介紹了通過(guò)JavaWeb制作一個(gè)線(xiàn)上蛋糕訂購(gòu)系統(tǒng),文中示例代碼講解詳細(xì),需要的朋友可以參考一下2021-12-12
Java中的Optional使用詳細(xì)說(shuō)明
這篇文章主要介紹了Java中的Optional使用詳細(xì)說(shuō)明,Optional就是相當(dāng)于把對(duì)象包了一層,將判斷空的部分代碼給單獨(dú)抽出來(lái)了,主要就是為了避免null引起的部分問(wèn)題,需要的朋友可以參考下2023-11-11

