java实现网页爬虫的示例讲解
这一篇目的就是在于网页爬虫的实现,对数据的获取,以便分析。
目录:
1、爬虫原理
2、本地文件数据提取及分析
3、单网页数据的读取
4、运用正则表达式完成超连接的连接匹配和提取
5、广度优先遍历,多网页的数据爬取
6、多线程的网页爬取
7、总结
爬虫实现原理
网络爬虫基本技术处理
网络爬虫是数据采集的一种方法,实际项目开发中,通过爬虫做数据采集一般只有以下几种情况:
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 List<String> alldata=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<Integer,Integer> 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<Integer> 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<Integer,Integer> 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<Integer> 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<String,Integer> 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<strsTime.length;i++){ System.out.println(strsTime[i]+" : "+map.get(strsTime[i])); } */ // 记录出事故最多的时间范围 String maxTime = null; // 记录出事故最多的次数 int maxCount = 0; Set<String> 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<String,Float> 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<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); } }
Ⅱ 接下来我们就可以在网页的数据上下手了。
下面先实现一个单网页数据提取的功能。
使用的技术可以有以下几类:
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 = "<div><div class='jg666'>[转让]<a href='/17610866588' title='手机号码17610866588估价评估_值多少钱_归属地查询_测吉凶_数字含义_求购转让信息' class='lj44'>17610866588</a>由 张云龙 300元转让,联系电话:17610866588</div><div class='jg666'>[转让]<a href='/17777351513' title='手机号码17777351513估价评估_值多少钱_归属地查询_测吉凶_数字含义_求购转让信息' class='lj44'>17777351513</a>由 胡俊宏 888元转让,QQ:762670775,联系电话:17777351513,可以小砍价..</div><div class='jg666'>[求购]<a href='/15019890606' title='手机号码15019890606估价评估_值多少钱_归属地查询_测吉凶_数字含义_求购转让信息' class='lj44'>15019890606</a>由 张宝红 600元求购,联系电话:15026815169</div><div class='jg666'>"; Matcher m = p.matcher(content); // System.out.println(p.matcher("sf@sina").matches()); Set<String> set = new HashSet<>(); // 通过Matcher类的group方法和find方法来进行查找和匹配 while (m.find()) { String value = m.group(); set.add(value); } System.out.println(set); } }
通过正则表达式完成超连接的连接匹配和提取
对爬取的HTML页面来说,如果想提取连接地址,就必须找到所有超连接的标签和对应的属性。
超连接标签是<a></a>,保存连接的属性是:href。
<a href=”…”>…</a>
规则:
<a .*href=.+</a>
广度优先遍历
需要有一个队列(这里直接使用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 List<String> allwaiturl=new ArrayList<>(); //爬取过的url private static Set<String> alloverurl=new HashSet<>(); //记录所有url的深度进行爬取判断 private static Map<String,Integer> 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("<a .*href=.+</a>"); //建立一个输出流,用于保存文件,文件名为执行时间,以防重复 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 List<String> allwaiturl=new ArrayList<>(); //爬取过的url private static Set<String> alloverurl=new HashSet<>(); //记录所有url的深度进行爬取判断 private static Map<String,Integer> 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<MAX_THREAD;i++){ new URLDemo().new MyThread().start(); } } /** * 网页数据爬取 * @param strurl * @param depth */ public static void workurl(String strurl,int depth){ //判断当前url是否爬取过 if(!(alloverurl.contains(strurl)||depth>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("<a .*href=.+</a>"); //建立一个输出流,用于保存文件,文件名为执行时间,以防重复 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实现网页爬虫的示例讲解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。