Lucene搜索引擎+HDFS+MR完成垂直搜索
来源:互联网 发布:CMS淀粉 编辑:程序博客网 时间:2024/06/05 00:15
介于上一篇的java实现网络爬虫基础之上,这一篇的思想是将网络收集的数据保存到HDFS和数据库(Mysql)中;然后用MR对HDFS的数据进行索引处理,处理成倒排索引;搜索时先用HDFS建立好的索引来搜索对应的数据ID,根据ID从数据库中提取数据,呈现到网页上。
这是一个完整的集合网络爬虫、数据库、HDFS、MapReduce、DAO设计模式、JSP/Servlet的项目,完成了数据收集、数据分析、数据索引并分页呈现。
完整的代码呈现,希望认真仔细阅读。
------>
目录:
1、搜索引擎阐述
2、数据库表建立
3、使用DAO设计模式进行数据库操作
【Ⅰ】数据库连接类DataBaseConnection
【Ⅱ】表单元素的封装类News
【Ⅲ】编写DAO接口INewsDAO
【Ⅳ】DAO接口的实现类NewsDAOImp类
【Ⅴ】工厂类DAOFactory类
4、网络爬虫实现★★ 【参考博客《java实现网络爬虫》和《Heritrix实现网络爬虫》】
5、MR(MapReduce)对HDFS数据进行索引处理★★
6、实现搜索引擎
【Ⅰ】创建web项目,编写测试用例,测试是否可以读取HDFS的数据内容
【Ⅱ】 编写index首页
【Ⅲ】处理HDFS查询的操作
【Ⅳ】servlet类搜索结果向页面传递
【Ⅴ】结果呈现,实现分页
7、总结
------>
1、搜索引擎阐述
搜索引擎的执行流程:
1) 通过爬虫来将数据下载到本地
2) 将数据提取出来保存到HDFS和数据库中(MySQL)
3) 通过MR来对HDFS的数据进行索引处理,处理成为倒排索引
4) 搜索时先使用HDFS建立好的索引来搜索对应的数据ID,再根据ID来从MySQL数据库中提取数据的具体信息。
5) 可以完成分页等操作。
倒排索引对应的就是正排索引,正排索引指的就是MySQL数据库中id的索引。
而倒排索引的目的是可以根据关键字查询出该关键字对应的数据id。
这里就需要用到MySQL数据库,以及通过Java EE版的Eclipse来完成网站的开发。
为了开发起来更方便,我们这里使用MyEclipse来完成。
2、数据库表建立
先安装好MySQL数据库。
安装时,注意编码选择gbk。
通过控制台的mysql -u用户名–p密码 即可登录mysql数据库。
之后使用show databases可以看到所有的数据库。
使用create database 可以建立一个新的库。
使用 use 库名 ,可以切换到另一个库。
使用show tables可以看到一个库下的所有表。
之后就可以通过普通的sql语句来建立表和进行数据的操作了。
在进行数据库操作时,企业开发中必定要使用DAO(Data Access Object)设计模式
组成如下:
1) DataBaseConnection:建立数据库连接
2) VO:与表对应的数据对象
3) DAO接口:规范操作方法
4) DAOImpl:针对DAO接口进行方法实现。
5) Factory:用来建立DAO接口对象。
首先根据需求,将数据库表建立出来,这里只需建立一个简单的news新闻表,用于存储网络上爬取得数据。
1 CREATE TABLE news (2 id int primary key ,3 title varchar(200) not null,4 description text ,5 url varchar(200)6 );
3、使用DAO设计模式进行数据库操作
根据上述的DAO设计模式,我们需要编写相关操作类,来完成数据库的操作。
【Ⅰ】 数据库连接类DataBaseConnection,需要导入jar包:
1 package org.liky.sina.dbc; 2 3 import java.sql.Connection; 4 import java.sql.DriverManager; 5 import java.sql.SQLException; 6 7 /** 8 * 连接数据库mysql的sina_news 9 * @author k0410 *11 */12 public class DataBaseConnection {
//此处可以试着两种表达加载类的方法13 // private static final String DBORIVER="org.git.mm.mysql.Driver";14 private static final String DBORIVER="com.mysql.jdbc.Driver";15 private static final String DBURL="jdbc:mysql://localhost:3306/sina_news";16 private static final String DBUSER="root";17 private static final String DBPASSWORD="admin";18 19 private Connection conn;20 /**21 * 创建数据库连接22 * @return23 */24 public Connection getConnection(){25 try {26 if(conn==null||conn.isClosed()){27 //建立一个新的连接28 Class.forName(DBORIVER);29 conn=DriverManager.getConnection(DBURL, DBUSER, DBPASSWORD);30 //System.out.println("success to connect!");31 }32 }catch (ClassNotFoundException e) {33 // TODO Auto-generated catch block34 e.printStackTrace(); 35 } catch (SQLException e) {36 // TODO Auto-generated catch block37 e.printStackTrace();38 }39 return conn;40 }41 /*42 * 关闭连接43 */44 public void close(){45 if(conn!=null){46 try{47 conn.close();48 }catch(SQLException e){49 e.printStackTrace();50 }51 }52 }53 54 }
【Ⅱ】表单元素的封装类News,根据建表时设定的四种元素id,title,description,url,将网络爬取得内容完整的导入数据库中,此处可以用shift+Alt+S在Eclipse快捷创建封装类:
1 package org.liky.sina.vo; 2 /** 3 * news封装类 4 * @author k04 5 * 6 */ 7 public class News { 8 private Integer id; 9 private String title;10 private String description;11 private String url;12 13 14 public News() {15 }16 public News(Integer id,String title,String description,String url) {17 this.id=id;18 this.title=title;19 this.description=description;20 this.url=url;21 }22 23 24 public Integer getId() {25 return id;26 }27 public void setId(Integer id) {28 this.id = id;29 }30 public String getTitle() {31 return title;32 }33 public void setTitle(String title) {34 this.title = title;35 }36 public String getDescription() {37 return description;38 }39 public void setDescription(String description) {40 this.description = description;41 }42 public String getUrl() {43 return url;44 }45 public void setUrl(String url) {46 this.url = url;47 }48 }
【Ⅲ】编写DAO接口INewsDAO,存放数据库操作类的方法名:
1 package org.liky.sina.dao; 2 /** 3 * 接口,呈现三个方法 4 */ 5 import java.util.List; 6 import org.liky.sina.vo.News; 7 8 public interface INewsDAO { 9 /**10 * 添加数据11 * @param news 要添加的对象12 * @throws Exception13 */14 public void doCreate(News news)throws Exception;15 /**16 * 根据主键id查询数据17 * 18 */19 public News findById(int id)throws Exception;20 /**21 * 根据一组id查询所有结果22 * @param ids 所有要查询的id23 * @return 查询到的数据24 * 因为索引是根据热词查到一堆的id25 */26 public List<News> findByIds(int[] ids)throws Exception;27 28 }
【Ⅳ】DAO接口的实现类NewsDAOImp类:
1 package org.liky.sina.dao.impl; 2 /** 3 * 继承INewsDAO接口 4 * 实现三个方法,插入数据,查找指定id数据,查找一组id数据 5 */ 6 import java.sql.PreparedStatement; 7 import java.sql.ResultSet; 8 import java.util.ArrayList; 9 import java.util.List;10 11 import org.liky.sina.dao.INewsDAO;12 import org.liky.sina.dbc.DataBaseConnection;13 import org.liky.sina.vo.News;14 15 public class NewsDAOImpl implements INewsDAO {16 //声明一个数据库连接类对象17 private DataBaseConnection dbc;18 19 20 //构造器,参数为数据库连接类对象21 public NewsDAOImpl(DataBaseConnection dbc) {22 this.dbc=dbc;23 24 }25 26 @Override27 public void doCreate(News news) throws Exception {28 // TODO Auto-generated method stub29 String sql="INSERT INTO news (id,title,description,url) VALUES (?,?,?,?)";30 PreparedStatement pst=dbc.getConnection().prepareStatement(sql);31 //设置参数32 pst.setInt(1, news.getId());33 pst.setString(2, news.getTitle());34 pst.setString(3, news.getDescription());35 pst.setString(4, news.getUrl());36 37 pst.executeUpdate();38 System.out.println("create success.");39 }40 41 @Override42 public News findById(int id) throws Exception {43 // TODO Auto-generated method stub44 String sql="SELECT id,title,description,url FROM news WHERE id = ?";45 PreparedStatement pst=dbc.getConnection().prepareStatement(sql);46 pst.setInt(1, id);47 ResultSet rs=pst.executeQuery();48 News news=null;49 //将符合id的数据遍历写入news并返回50 if(rs.next()){51 news=new News();52 news.setId(rs.getInt(1));53 news.setTitle(rs.getString(2));54 news.setDescription(rs.getString(3));55 news.setUrl(rs.getString(4));56 }57 //System.out.println("find success.");58 return news;59 }60 61 @Override62 public List<News> findByIds(int[] ids) throws Exception {63 // TODO Auto-generated method stub64 StringBuilder sql=new StringBuilder("SELECT id,title,description,url FROM news WHERE id IN (");65 //将id写入ids,并用逗号隔开66 if(ids!=null&&ids.length>0){67 for(int id:ids){68 sql.append(id);69 sql.append(",");70 }71 //截取最后一个逗号,并补上括号72 String resultSQL=sql.substring(0, sql.length()-1)+")";73 74 PreparedStatement pst=dbc.getConnection().prepareStatement(resultSQL);75 ResultSet rs=pst.executeQuery();76 //存取一组id到链表中77 List<News> list=new ArrayList<>();78 while(rs.next()){79 News news=new News();80 news.setId(rs.getInt(1));81 news.setTitle(rs.getString(2));82 news.setDescription(rs.getString(3));83 news.setUrl(rs.getString(4));84 list.add(news);85 }86 }87 //System.out.println("find success.");88 return null;89 }90 91 }
【Ⅴ】工厂类DAOFactory类,此类写入了数据库连接类参数,返回DAO实现类对象:
java中,我们通常有以下几种创建对象的方式:
(1) 使用new关键字直接创建对象;
(2) 通过反射机制创建对象;
(3) 通过clone()方法创建对象;
(4) 通过工厂类创建对象。
1 package org.liky.sina.factory; 2 /** 3 * 工厂类 4 * 输入一个连接数据库对象的参数,返回数据库表操作的类 5 */ 6 import org.liky.sina.dao.INewsDAO; 7 import org.liky.sina.dao.impl.NewsDAOImpl; 8 import org.liky.sina.dbc.DataBaseConnection; 9 10 public class DAOFactory {11 public static INewsDAO getINewsDAOInstance(DataBaseConnection dbc){12 return new NewsDAOImpl(dbc);13 }14 }
4、网络爬虫实现
现在编写整个项目的重点,编写URLDemo类,在爬虫中进行数据库的操作以及HDFS的写入:
a' 关于此类,在网页解析时用了简单的Jsoup,并没有如《java网络爬虫》用正则表达式,所以需要导入jsoup的jar包 ;
b' 关于HDFS在eclipse的配置以及本机的连接,我后续博客会阐述,也可以网络查询方法;
c' 这个类也是执行类,我收集的是新浪新闻网的数据,爬取深度为5,设置线程数5,并且筛选了只有链接含有“sian.news.com.cn”的。
d' 网络爬虫我讲了两种方法:(1)java代码实现网络爬虫
(2)Heritrix工具实现网络爬虫
此处我还是选择了直接写代码实现,自由度高也方便读写存取。
1 package org.liky.sina.craw; 2 3 import java.util.ArrayList; 4 import java.util.HashMap; 5 import java.util.HashSet; 6 import java.util.List; 7 import java.util.Map; 8 import java.util.Set; 9 10 import org.apache.hadoop.conf.Configuration; 11 import org.apache.hadoop.fs.FSDataOutputStream; 12 import org.apache.hadoop.fs.FileSystem; 13 import org.apache.hadoop.fs.Path; 14 import org.jsoup.Jsoup; 15 import org.jsoup.nodes.Document; 16 import org.jsoup.nodes.Element; 17 import org.jsoup.select.Elements; 18 import org.liky.sina.dao.INewsDAO; 19 import org.liky.sina.dbc.DataBaseConnection; 20 import org.liky.sina.factory.DAOFactory; 21 import org.liky.sina.vo.News; 22 23 /** 24 * 爬虫开始进行数据库操作以及HDFS写入 25 * 26 * @author k04 27 * 28 */ 29 public class URLDemo { 30 // 该对象的构造方法会默认加载hadoop中的两个配置文件,hdfs-site.xml和core-site.xml 31 // 这两个文件包含访问hdfs所需的参数值 32 private static Configuration conf = new Configuration(); 33 34 private static int id = 1; 35 36 private static FileSystem fs; 37 38 private static Path path; 39 40 // 等待爬取的url 41 private static List<String> allWaitUrl = new ArrayList<>(); 42 // 已经爬取的url 43 private static Set<String> allOverUrl = new HashSet<>(); 44 // 记录所有url的深度,以便在addUrl方法内判断 45 private static Map<String, Integer> allUrlDepth = new HashMap<>(); 46 // 爬取网页的深度 47 private static int maxDepth = 5; 48 // 声明object独享帮助进行线程的等待操作 49 private static Object obj = new Object(); 50 // 设置总线程数 51 private static final int MAX_THREAD = 20; 52 // 记录空闲的线程数 53 private static int count = 0; 54 55 // 声明INewsDAO对象, 56 private static INewsDAO dao; 57 58 static { 59 dao = DAOFactory.getINewsDAOInstance(new DataBaseConnection()); 60 } 61 62 public static void main(String args[]) { 63 // 爬取的目标网址 64 String strUrl = "http://news.sina.com.cn/"; 65 66 // 爬取第一个输入的url 67 addUrl(strUrl, 0); 68 // 建立多个线程 69 for (int i = 0; i < MAX_THREAD; i++) { 70 new URLDemo().new MyThread().start(); 71 } 72 73 // DataBaseConnection dc=new DataBaseConnection(); 74 // dc.getConnection(); 75 76 } 77 78 public static void parseUrl(String strUrl, int depth) { 79 // 先判断当前url是否爬取过 80 // 判断深度是否符合要求 81 if (!(allOverUrl.contains(strUrl) || depth > maxDepth)) { 82 System.out.println("当前执行的 " + Thread.currentThread().getName() 83 + " 爬虫线程处理爬取: " + strUrl); 84 85 try { 86 // 用jsoup进行数据爬取 87 Document doc = Jsoup.connect(strUrl).get(); 88 // 通过doc接受返回的结果 89 // 提取有效的title和description 90 String title = doc.title(); 91 Element descE = doc.getElementsByAttributeValue("name", 92 "description").first(); 93 String desc = descE.attr("content"); 94 95 // System.out.println(title + " --> " + desc); 96 97 // 如果有效,则惊醒保存 98 if (title != null && desc != null && !title.trim().equals("") 99 && !desc.trim().equals("")) {100 // 需要生成一个id,以便放入数据库中,因此id也要加入到HDFS中,便于后续索引101 News news = new News();102 news.setId(id++);103 news.setTitle(title);104 news.setDescription(desc);105 news.setUrl(strUrl);106 // 添加到数据库语句107 dao.doCreate(news);108 // 向HDFS保存数据109 path = new Path("hdfs://localhost:9000/sina_news_input/"110 + System.currentTimeMillis() + ".txt");111 fs = path.getFileSystem(conf);112 FSDataOutputStream os = fs.create(path);113 // 进行内容输出,此处需要用news.getId(),不然数据库和HDFS的id会不相同,因为多线程的运行114 os.writeUTF(news.getId() + "\r\n" + title + "\r\n" + desc);115 os.close();116 117 // 解析所有超链接118 Elements aEs = doc.getElementsByTag("a");119 // System.out.println(aEs);120 if (aEs != null && aEs.size() > 0) {121 for (Element aE : aEs) {122 String href = aE.attr("href");123 System.out.println(href);124 // 截取网址,并给出筛选条件!!!125 if ((href.startsWith("http:") || href126 .startsWith("https:"))127 && href.contains("news.sina.com.cn")) {128 // 调用addUrl()方法129 addUrl(href, depth + 1);130 }131 }132 }133 134 }135 136 } catch (Exception e) {137 138 }139 // 吧当前爬完的url放入到偶尔中140 allOverUrl.add(strUrl);141 System.out.println(strUrl + "爬去完成,已经爬取的内容量为:" + allOverUrl.size()142 + "剩余爬取量为:" + allWaitUrl.size());143 144 // 判断是否集合中海油其他的内容需要进行爬取,如果有,则进行线程的唤醒145 if (allWaitUrl.size() > 0) {146 synchronized (obj) {147 obj.notify();148 }149 } else {150 System.out.println("爬取结束...");151 System.exit(0);152 }153 154 }155 }156 157 /**158 * url加入到等待队列中 并判断是否已经放过,若没有就放入allUrlDepth中159 * 160 * @param href161 * @param depth162 */163 public static synchronized void addUrl(String href, int depth) {164 // 将url放入队列中165 allWaitUrl.add(href);166 // 判断url是否已经存在167 if (!allUrlDepth.containsKey(href)) {168 allUrlDepth.put(href, depth + 1);169 }170 }171 172 /**173 * 获取等待队列下一个url,并从等待队列中移除174 * 175 * @return176 */177 public static synchronized String getUrl() {178 if (allWaitUrl.size() > 0) {179 String nextUrl = allWaitUrl.get(0);180 allWaitUrl.remove(0);181 return nextUrl;182 }183 return null;184 }185 186 /**187 * 用多线程进行url爬取188 * 189 * @author k04190 * 191 */192 public class MyThread extends Thread {193 194 @Override195 public void run() {196 // 编写一个死循环,以便线程可以一直存在197 while (true) {198 //199 200 String url = getUrl();201 if (url != null) {202 // 调用该方法爬取url的数据203 parseUrl(url, allUrlDepth.get(url));204 } else {205 System.out.println("当前线程准备就绪,等待连接爬取:" + this.getName());206 // 线程+1207 count++;208 // 建立一个对象,帮助线程进入等待状态wait()209 synchronized (obj) {210 try {211 obj.wait();212 } catch (Exception e) {213 e.printStackTrace();214 }215 // 线程-1216 count--;217 }218 }219 }220 }221 222 }223 224 }
现在执行上述类URLDemo,该类执行流程:
1‘ 读取源链接,分配线程任务;
2’ 数据根据DAO设计模式写入到数据库中;
3‘ 数据上传到HDFS的input文件夹中;
执行完可以查看HDFS和数据库的结果:
关于此处,还是有些许瑕疵,双方的数据量并不相等,主要是多线程爬取的时候,id的传输并不相同!
将重复的数据进行清洗,
当数据爬取完成后,我们会发现有很多重复的数据,这可以通过SQL语句来进行清洗的操作。
SQL中可以使用group by关键字来完成分组函数的处理。
我们可以先通过该函数来测试一下。
测试后会发现真正有效数据量应该是4634条。
这就需要我们通过建立一张新表,把所有不重复的数据加入到新表中。
mysql支持create table时通过select语句来查询出一些结果作为新表的结构和数据。
CREATE TABLE new_news SELECT id,title,description,url FROM news WHERE id IN (SELECT min(id) FROM news GROUP BY title)
5、MR(MapReduce)对HDFS数据进行索引处理
下面就可以开始编写MR程序。
Map格式,要求key为关键字,value为id。
由于一个关键字可能会对应多个id,所以id之间我们想使用,来分隔。所以key和value的类型都应该是String(Text)
1 package org.liky.sina.index; 2 /** 3 * 编写MR程序。 4 Map格式,要求key为关键字,value为id。 5 由于一个关键字可能会对应多个id,所以id之间我们想使用,来分隔。所以key和value的类型都应该是String(Text) 6 */ 7 import java.io.IOException; 8 import java.util.*; 9 import java.util.regex.*; 10 import jeasy.analysis.MMAnalyzer; 11 import org.apache.hadoop.conf.*; 12 import org.apache.hadoop.fs.Path; 13 import org.apache.hadoop.io.*; 14 import org.apache.hadoop.mapred.*; 15 import org.apache.hadoop.util.*; 16 17 18 public class IndexCreater extends Configured implements Tool { 19 20 public static void main(String[] args) throws Exception { 21 int res = ToolRunner.run(new Configuration(), new IndexCreater(), 22 new String[] { "hdfs://localhost:9000/sina_news_input/", 23 "hdfs://localhost:9000/output_news_map/" }); 24 System.exit(res); 25 } 26 27 28 29 //Mapper接口中的后两个泛型参数,第一个表示返回后Map的key的类型,第二个表示返回后的value类型 30 public static class MapClass extends MapReduceBase implements Mapper<LongWritable,Text,Text,Text>{ 31 32 private static Pattern p; 33 34 static{ 35 System.out.println("开始Map操作....."); 36 p=Pattern.compile("\\d+"); 37 } 38 39 private int id; 40 private int line=1; 41 42 private static MMAnalyzer mm=new MMAnalyzer(); 43 44 //输出的词 45 private Text word=new Text(); 46 47 //map过程的核心方法 48 @Override 49 public void map(LongWritable key, Text value, 50 OutputCollector<Text, Text> output, Reporter reporter) 51 throws IOException { 52 if (line == 1) { 53 // 读取的是第一行,我们就需要将第一行的id保留下来 54 line++; 55 Matcher m = p.matcher(value.toString()); 56 if (m.find()) { 57 id = Integer.parseInt(m.group()); 58 } 59 } else { 60 String tempStr = value.toString(); 61 // 按空格将单词拆分出来 62 // StringTokenizer itr = new StringTokenizer(line); 63 // 使用分词器来进行词组的拆分 64 String[] results = mm.segment(tempStr, "|").split("\\|"); 65 // 每个单词记录出现了1次 66 for (String temp : results) { 67 word.set(temp.toLowerCase()); 68 output.collect(word, new Text(id + "")); 69 } 70 } 71 72 } 73 74 75 76 77 } 78 79 80 //对所有的结果进行规约,合并 81 //Reducer中也有泛型,前两个表示Map过程输出的结果类型,后两个表示Reduce处理后输出的类型 82 public static class Reduce extends MapReduceBase implements Reducer<Text,Text,Text,Text>{ 83 84 static{ 85 System.out.println("开始reduce操作....."); 86 } 87 @Override 88 public void reduce(Text key, Iterator<Text> values, 89 OutputCollector<Text, Text> output, Reporter repoter) 90 throws IOException { 91 //将所有key值相同的结果,求和 92 StringBuilder result=new StringBuilder(); 93 while(values.hasNext()){ 94 //存在一个key相同的,加入result 95 String temp=values.next().toString(); 96 if(!result.toString().contains(temp+",")){ 97 result.append(temp+","); 98 } 99 100 }101 //将其规约102 output.collect(key, new Text(result.substring(0, result.length()-1)));103 //输出key相同的id值104 System.out.println(key+"---->"+result);105 }106 107 108 109 }110 111 static int printUsage() {112 System.out113 .println("wordcount [-m <maps>] [-r <reduces>] <input> <output>");114 ToolRunner.printGenericCommandUsage(System.out);115 return -1;116 }117 118 119 @Override120 public int run(String[] args) throws Exception {121 // TODO Auto-generated method stub122 JobConf conf = new JobConf(getConf(), IndexCreater.class);123 conf.setJobName("wordcount");124 125 // 输出结果的Map的key值类型126 conf.setOutputKeyClass(Text.class);127 // 输出结果的Map的value值类型128 conf.setOutputValueClass(Text.class);129 130 conf.setMapperClass(MapClass.class);131 conf.setCombinerClass(Reduce.class);132 conf.setReducerClass(Reduce.class);133 134 List<String> other_args = new ArrayList<String>();135 for (int i = 0; i < args.length; ++i) {136 try {137 if ("-m".equals(args[i])) {138 conf.setNumMapTasks(Integer.parseInt(args[++i]));139 } else if ("-r".equals(args[i])) {140 conf.setNumReduceTasks(Integer.parseInt(args[++i]));141 } else {142 other_args.add(args[i]);143 }144 } catch (NumberFormatException except) {145 System.out.println("ERROR: Integer expected instead of "146 + args[i]);147 return printUsage();148 } catch (ArrayIndexOutOfBoundsException except) {149 System.out.println("ERROR: Required parameter missing from "150 + args[i - 1]);151 return printUsage();152 }153 }154 // Make sure there are exactly 2 parameters left.155 if (other_args.size() != 2) {156 System.out.println("ERROR: Wrong number of parameters: "157 + other_args.size() + " instead of 2.");158 return printUsage();159 }160 // 设置输出结果按照什么格式保存,以便后续使用。161 conf.setOutputFormat(MapFileOutputFormat.class);162 // 输入文件的HDFS路径163 FileInputFormat.setInputPaths(conf, other_args.get(0));164 // 输出结果的HDFS路径165 FileOutputFormat.setOutputPath(conf, new Path(other_args.get(1)));166 167 JobClient.runJob(conf);168 169 return 0;170 }171 172 }
6、实现搜索引擎
【Ⅰ】创建web项目,编写测试用例,测试是否可以读取HDFS的数据内容
在安装好的MyEclipse开发工具中,开始编写搜索引擎展示部分的内容。
这里先使用普通的JSP + Servlet的模式来完成程序的编写。
首先建立一个普通的Web项目。
之后,在里面编写一个测试用例,测试是否可以读取HDFS中的数据内容,注意需要先将hadoop/lib目录下的所有jar包,以及hadoop根目录下的支持jar包拷贝到项目WEB-INF目录下的lib目录中。
注意!!!完成上面数据的收集和分析之后,现在读取的内容需要从经过MR处理之后存储在HDFS的sina_new_ouputwenjianjia内读取。
1 package org.liky.sina.test; 2 3 import org.apache.hadoop.conf.Configuration; 4 import org.apache.hadoop.fs.FileSystem; 5 import org.apache.hadoop.fs.Path; 6 import org.apache.hadoop.io.MapFile.Reader; 7 import org.apache.hadoop.io.Text; 8 import org.apache.hadoop.mapred.MapFileOutputFormat; 9 import org.junit.Test;10 11 public class TestCaseSina {12 13 @Test14 public void test() throws Exception {15 Configuration conf = new Configuration();16 Path path = new Path("hdfs://localhost:9000/output_news_map/");17 FileSystem fs = path.getFileSystem(conf);18 Reader reader = MapFileOutputFormat.getReaders(fs, path, conf)[0];19 Text value = (Text) reader.get(new Text("印度"), new Text());20 21 System.out.println(value);22 23 }24 }
【Ⅱ】 编写index首页
将之前写好的DAO代码也拷贝到项目中,以便以后查询数据库使用。
之后编写一个jsp页面,用来接收用户输入的查询关键字。
此处我就从简了 O(∩_∩)O
1 <body>2 <center>3 <form action="SearchServlet" method="post">4 请输入查询关键字:5 <input type="text" name="keyword"> 6 <input type="submit" value="查询">7 </form> 8 </center>9 </body>
【Ⅲ】处理HDFS查询的操作
根据设置好的路径,建立一个SearchServlet,并完成doGet和doPost方法。
在这个Servlet中会用到处理HDFS查询的操作方法,因此我们需要单独声明一个HDFSUtils工具类,来帮助我们实现查询的功能。
1 package org.liky.sina.utils; 2 3 import java.io.IOException; 4 import java.util.Set; 5 import java.util.TreeSet; 6 7 import org.apache.hadoop.conf.Configuration; 8 import org.apache.hadoop.fs.FileSystem; 9 import org.apache.hadoop.fs.Path;10 import org.apache.hadoop.io.MapFile.Reader;11 import org.apache.hadoop.io.Text;12 import org.apache.hadoop.mapred.MapFileOutputFormat;13 14 public class HDFSUtils {15 //连接hadoop的配置16 private static Configuration conf = new Configuration();17 //创建需要读取数据的hdfs路径18 private static Path path = new Path(19 "hdfs://localhost:9000/output_news_map/");20 21 private static FileSystem fs = null;22 23 static {24 try {25 fs = path.getFileSystem(conf);26 } catch (IOException e) {27 e.printStackTrace();28 }29 }30 31 public static Integer[] getIdsByKeyword(String keyword) throws Exception {32 33 Reader reader = MapFileOutputFormat.getReaders(fs, path, conf)[0];34 Text value = (Text) reader.get(new Text(keyword), new Text());35 //set存放关键词搜索的一组id36 Set<Integer> set = new TreeSet<Integer>();37 String[] strs = value.toString().split(",");38 39 for (String str : strs) {40 set.add(Integer.parseInt(str));41 }42 43 return set.toArray(new Integer[0]);44 }45 46 }
【Ⅳ】servlet类搜索结果向页面传递
在Servlet中通过调用HDFSUtils和之前写过的DAO方法,即可查询到结果并设置向页面传递。
此处将关键词搜索的结果呈现到result.jsp界面,所以下面就是编写该界面呈现最终结果。
1 package org.liky.sina.servlet; 2 3 import java.io.IOException; 4 import java.util.List; 5 6 import javax.servlet.ServletException; 7 import javax.servlet.http.HttpServlet; 8 import javax.servlet.http.HttpServletRequest; 9 import javax.servlet.http.HttpServletResponse;10 11 import org.liky.sina.dbc.DataBaseConnection;12 import org.liky.sina.factory.DAOFactory;13 import org.liky.sina.utils.HDFSUtils;14 import org.liky.sina.vo.News;15 16 public class SearchServlet extends HttpServlet {17 18 public void doGet(HttpServletRequest request, HttpServletResponse response)19 throws ServletException, IOException {20 this.doPost(request, response);21 }22 23 public void doPost(HttpServletRequest request, HttpServletResponse response)24 throws ServletException, IOException {25 // 接收提交的查询关键字参数26 // 先处理乱码27 request.setCharacterEncoding("UTF-8");28 // 接收参数29 String keyword = request.getParameter("keyword");30 // 根据关键字进行查询。31 try {32 Integer[] ids = HDFSUtils.getIdsByKeyword(keyword);33 // 根据这些id来查询出相应的结果34 List<News> allNews = DAOFactory.getINewsDAOInstance(35 new DataBaseConnection()).findByIds(ids);36 37 // 将结果传递回页面显示38 request.setAttribute("allNews", allNews);39 40 // 切换到页面上41 request.getRequestDispatcher("/result.jsp").forward(request,42 response);43 } catch (Exception e) {44 e.printStackTrace();45 }46 47 }48 }
【Ⅴ】结果呈现,实现分页
最后,我们需要在页面上将结果呈现出来,在web根目录下编写result.jsp文件。
可以通过JSTL + EL来完成内容的输出。
1 <%@page import="org.liky.sina.vo.News"%> 2 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> 3 <% 4 String path = request.getContextPath(); 5 String basePath = request.getScheme() + "://" 6 + request.getServerName() + ":" + request.getServerPort() 7 + path + "/"; 8 %> 9 10 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">11 <html>12 <head>13 <base href="<%=basePath%>">14 15 <title>新浪新闻搜索</title>16 </head>17 18 <body>19 <center>20 <%21 List<News> allNews = (List<News>)request.getAttribute("allNews");22 %>23 <table width="80%">24 <%25 for (News n : allNews) {26 27 %>28 <tr>29 <td>30 <a href="<%=n.getUrl() %>" target="_blank"><%=n.getTitle() %></a> <br>31 <%=n.getDescription() %>32 <hr/>33 </td>34 </tr>35 <%36 }37 38 %>39 </table>40 41 <%42 int cp = (Integer)request.getAttribute("currentPage");43 int allPages = (Integer)request.getAttribute("allPages");44 %>45 <form action="SearchServlet" method="post">46 <input type="hidden" name="currentPage" value="<%=cp %>" />47 <input type="button" <%=cp == 1?"disabled":"" %> value="首页" onclick="changeCp(1);">48 <input type="button" <%=cp == 1?"disabled":"" %> value="上一页" onclick="changeCp(<%=cp - 1 %>);">49 <input type="button" <%=cp == allPages?"disabled":"" %> value="下一页" onclick="changeCp(<%=cp + 1 %>);">50 <input type="button" <%=cp == allPages?"disabled":"" %> value="尾页" onclick="changeCp(<%=allPages %>);">51 第 <%=cp %> 页 / 共 <%=allPages %> 页52 <br>53 请输入查询关键字:<input type="text" name="keyword" value="<%=request.getParameter("keyword")%>">54 <input type="submit" value="查询">55 </form>56 <script type="text/javascript">57 function changeCp(newcp) {58 // 改变当前页数59 document.getElementById("cp").value = newcp;60 // 提交表单61 document.getElementById("split_page_form").submit();62 }63 </script>64 65 </center>66 </body>67 </html>
总结:
这个项目看起来很简单,但是囊括了很多知识,包括
1’ java的多线程处理,接口及方法实现;
2‘ 数据库的基础操作及代码连接与表操作;
3’ 网络爬虫进行数据的收集,包含两种方法(仅我会的,这个是难点):
(1)java代码实现(正则表达式或者Jsoup)
(2)Heritrix工具实现(抑或其他工具)
4‘ Hadoop的基本配置和HDFS于eclipse的配置(后续阐述);
5’ HDFS的文件存取、MapReduce方法的编写(这个是难点);
6‘ DAO设计模式的代码实现;
7’ Jsp/Servlet的基础知识,以及JSTL和EL的了解
该项目实现的功能包含:
1‘ 数据收集(网络爬虫);
2’ 数据保存(DAO,数据库及HDFS);
3‘ 数据分析/规约(MapReduce);
4’ 搜索引擎(jsp/servlet,jstl/el);
5‘ web呈现(web项目)
这是一个简单的大数据搜索引擎的实现,综合性较强,需要多多阅读学习。
此外,还有一些缺陷未能实现,会有些麻烦,一个在编码格式上,一个在搜索的关键字交叉搜索上。
- Lucene搜索引擎+HDFS+MR完成垂直搜索
- 搜索引擎开发,垂直搜索开发探讨:蜘蛛,并行,搜索,垂直搜索,搜索开发,lucene,java,分布[原创]
- Lucene+nutch构建垂直搜索引擎
- 最全面的垂直搜索引擎统计-行业搜索--垂直搜索
- 垂直搜索开发:垂直搜索引擎开发全过程[原创]
- 垂直化搜索引擎——Lucene、Solr简介
- 垂直搜索引擎
- hdfs HA + MR HA
- lucene搜索引擎
- 搜索引擎lucene
- Lucene搜索引擎
- lucene搜索引擎
- 搜索引擎lucene
- Lucene搜索引擎
- 利用Lucene.net搜索引擎进行多条件搜索的做法
- 利用Lucene.net搜索引擎进行多条件搜索的做法
- 利用Lucene.net搜索引擎进行多条件搜索的做法
- 利用Lucene.net搜索引擎进行多条件搜索的做法
- 常用命令总结
- 数据挖掘中的关联规则
- Android中常见的内存泄漏汇总
- sas统计分析学习笔记(五)
- MAC上读出usb
- Lucene搜索引擎+HDFS+MR完成垂直搜索
- 匈牙利算法
- Flask-hello程序
- Super Jumping! Jumping! Jumping!
- 今天写了一条把我两个月职业生涯学到的SQL知识全用上了的语句
- 关闭服务器windows server的IE浏览器的增强安全配置
- 文本相关性排序
- 用javadoc命令生成api帮助文档
- MySQL的InnoDB索引原理详解