spark实战之:分析维基百科网站统计数据(java版)

本次实战开发的spark应用的功能,是对网站统计数据进行排名,找出访问量最高的前100地址,在控制台打印出来并保存到hdsf;

源码下载

接下来详细讲述应用的编码过程,如果您不想自己写代码,也可以在GitHub下载完整的应用源码,地址和链接信息如下表所示:

| 名称 | 链接 | 备注 |

| :-- | :-- | :-- |

| 项目主页 | https://github.com/zq2599/blog_demos | 该项目在GitHub上的主页 |

| git仓库地址(https) | https://github.com/zq2599/blog_demos.git | 该项目源码的仓库地址,https协议 |

| git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh协议 |

这个git项目中有多个文件夹,本章源码在sparkdemo这个文件夹下,如下图红框所示:

在这里插入图片描述

详细开发

  1. 基于maven创建工程,pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns=“http://maven.apache.org/POM/4.0.0”

xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”

xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”>

4.0.0

com.bolingcavalry

sparkdemo

1.0-SNAPSHOT

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

org.apache.spark

spark-core_2.11

2.3.2

src/main/java

src/test/java

maven-assembly-plugin

jar-with-dependencies

make-assembly

package

single

org.codehaus.mojo

exec-maven-plugin

1.2.1

exec

java

false

false

compile

com.bolingcavalry.sparkdemo.app.WikiRank

org.apache.maven.plugins

maven-compiler-plugin

1.8

1.8

  1. 创建一个数据结构类PageInfo,在运行过程中会用到,里面记录了业务所需的字段:

package com.bolingcavalry.sparkdemo.bean;

import java.io.Serializable;

import java.util.LinkedList;

import java.util.List;

/**

  • @Description: 数据结构类

  • @author: willzhao E-mail: zq2599@gmail.com

  • @date: 2019/2/10 15:33

*/

public class PageInfo implements Serializable {

/**

  • 还原的url地址

*/

private String url;

/**

  • urldecode之后的三级域名

*/

private String name;

/**

  • 该三级域名的请求次数

*/

private int requestTimes;

/**

  • 该地址被请求的字节总数

*/

private long requestLength;

/**

  • 对应的原始字段

*/

private List raws = new LinkedList<>();

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public int getRequestTimes() {

return requestTimes;

}

public void setRequestTimes(int requestTimes) {

this.requestTimes = requestTimes;

}

public long getRequestLength() {

return requestLength;

}

public void setRequestLength(long requestLength) {

this.requestLength = requestLength;

}

public List getRaws() {

return raws;

}

public void setRaws(List raws) {

this.raws = raws;

}

public String getUrl() {

return url;

}

public void setUrl(String url) {

this.url = url;

}

}

  1. 对于前面提到的例子,“aa.b User_talk:Sevela.p 1 5786"对应的网址是"https://aa.wikibooks.org/wiki/User_talk:Sevela.p”,这个转换逻辑被做成了一个静态方法,这样就能把每一行记录对应的地址还原出来了,如下所示:

package com.bolingcavalry.sparkdemo.util;

import org.apache.commons.lang3.StringUtils;

/**

  • @Description: 常用的静态工具放置在此

  • @author: willzhao E-mail: zq2599@gmail.com

  • @date: 2019/2/16 9:01

*/

public class Tools {

/**

  • 域名的格式化模板

*/

private static final String URL_TEMPALTE = “https://%s/wiki/%s”;

/**

  • 根据项目名称和三级域名还原完整url,

  • 还原逻辑来自:https://wikitech.wikimedia.org/wiki/Analytics/Archive/Data/Pagecounts-raw

  • @param project

  • @param thirdLvPath

  • @return

*/

public static String getUrl(String project, String thirdLvPath){

//如果入参不合法,就返回固定格式的错误提示

if(StringUtils.isBlank(project) || StringUtils.isBlank(thirdLvPath)){

return “1. invalid param (” + project + “)(” + thirdLvPath + “)”;

}

//检查project中是否有"."

int dotOffset = project.indexOf(‘.’);

//如果没有".“,就用project+”.wikipedia.org"作为一级域名

if(dotOffset<0){

return String.format(URL_TEMPALTE,

project + “.wikipedia.org”,

thirdLvPath);

}

//如果有".“,就用”."之后的字符串按照不同的逻辑转换

String code = project.substring(dotOffset);

//".mw"属于移动端网页,统计的逻辑略微复杂,详情参考网页链接,这里不作处理直接返回固定信息

if(“.mw”.equals(code)){

return “mw page (” + project + “)(” + thirdLvPath + “)”;

}

String firstLvPath = null;

//就用"."之后的字符串按照不同的逻辑转换

switch(code){

case “.b”:

firstLvPath = “.wikibooks.org”;

break;

case “.d”:

firstLvPath = “.wiktionary.org”;

break;

case “.f”:

firstLvPath = “.wikimediafoundation.org”;

break;

case “.m”:

firstLvPath = “.wikimedia.org”;

break;

case “.n”:

firstLvPath = “.wikinews.org”;

break;

case “.q”:

firstLvPath = “.wikiquote.org”;

break;

case “.s”:

firstLvPath = “.wikisource.org”;

break;

case “.v”:

firstLvPath = “.wikiversity.org”;

break;

case “.voy”:

firstLvPath = “.wikivoyage.org”;

break;

case “.w”:

firstLvPath = “.mediawiki.org”;

break;

case “.wd”:

firstLvPath = “.wikidata.org”;

break;

}

if(null==firstLvPath){

return “2. invalid param (” + project + “)(” + thirdLvPath + “)”;

}

//还原地址

return String.format(URL_TEMPALTE,

project.substring(0, dotOffset) + firstLvPath,

thirdLvPath);

}

public static void main(String[] args){

String str = “abc.123456”;

System.out.println(str.substring(str.indexOf(‘.’)));

}

}

  1. 接下来是spark应用的源码,主要是创建PageInfo对象,以及map、reduce、排序等逻辑,代码中已有注释说明,就不再赘述:

package com.bolingcavalry.sparkdemo.app;

import com.bolingcavalry.sparkdemo.bean.PageInfo;

import com.bolingcavalry.sparkdemo.util.Tools;

import org.apache.commons.lang3.StringUtils;

import org.apache.spark.SparkConf;

import org.apache.spark.api.java.JavaPairRDD;

import org.apache.spark.api.java.JavaRDD;

import org.apache.spark.api.java.JavaSparkContext;

import org.apache.spark.api.java.function.Function;

import org.apache.spark.api.java.function.Function2;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import scala.Tuple2;

import java.net.URLDecoder;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.List;

/**

  • @Description: 根据wiki的统计来查找最高访问量的文章

  • @author: willzhao E-mail: zq2599@gmail.com

  • @date: 2019/2/8 17:21

*/

public class WikiRank {

private static final Logger logger = LoggerFactory.getLogger(WikiRank.class);

private static final int TOP = 100;

public static void main(String[] args) {

if(null==args

|| args.length<2

|| StringUtils.isEmpty(args[0])

|| StringUtils.isEmpty(args[1])) {

logger.error(“invalid params!”);

}

String hdfsHost = args[0];

String hdfsPort = args[1];

SparkConf sparkConf = new SparkConf().setAppName(“Spark WordCount Application (java)”);

JavaSparkContext javaSparkContext = new JavaSparkContext(sparkConf);

String hdfsBasePath = “hdfs://” + hdfsHost + “:” + hdfsPort;

//文本文件的hdfs路径

String inputPath = hdfsBasePath + “/input/*”;

//输出结果文件的hdfs路径

String outputPath = hdfsBasePath + “/output/”

  • new SimpleDateFormat(“yyyyMMddHHmmss”).format(new Date());

logger.info(“input path : {}”, inputPath);

logger.info(“output path : {}”, outputPath);

logger.info(“import text”);

//导入文件

JavaRDD textFile = javaSparkContext.textFile(inputPath);

logger.info(“do map operation”);

JavaPairRDD<String, PageInfo> counts = textFile

//过滤掉无效的数据

.filter((Function<String, Boolean>) v1 -> {

if(StringUtils.isBlank(v1)){

return false;

}

//分割为数组

String[] array = v1.split(" ");

/**

  • 以下情况都要过滤掉

    1. 名称无效(array[1])
    1. 请求次数无效(array[2)
    1. 请求总字节数无效(array[3)

*/

if(null==array

|| array.length<4

|| StringUtils.isBlank(array[1])

|| !StringUtils.isNumeric(array[2])

|| !StringUtils.isNumeric(array[3])){

logger.error(“find invalid data [{}]”, v1);

return false;

}

return true;

})

//将每一行转成一个PageInfo对象

.map((Function<String, PageInfo>) v1 -> {

String[] array = v1.split(" ");

PageInfo pageInfo = new PageInfo();

try {

pageInfo.setName(URLDecoder.decode(array[1], “UTF-8”));

}catch (Exception e){

//有的字符串没有做过urlencode,此时做urldecode可能抛出异常(例如abc%),此时用原来的内容作为name即可

pageInfo.setName(array[1]);

}

pageInfo.setUrl(Tools.getUrl(array[0], array[1]));

pageInfo.setRequestTimes(Integer.valueOf(array[2]));

pageInfo.setRequestLength(Long.valueOf(array[3]));

pageInfo.getRaws().add(v1);

return pageInfo;

})

//转成键值对,键是url,值是PageInfo对象

.mapToPair(pageInfo -> new Tuple2<>(pageInfo.getUrl(), pageInfo))

//按照url做reduce,将请求次数累加

.reduceByKey((Function2<PageInfo, PageInfo, PageInfo>) (v1, v2) -> {

v2.setRequestTimes(v2.getRequestTimes() + v1.getRequestTimes());

v2.getRaws().addAll(v1.getRaws());

return v2;

});

logger.info(“do convert”);

//先将key和value倒过来,再按照key排序

JavaPairRDD<Integer, PageInfo> sorts = counts

//key和value颠倒,生成新的map

.mapToPair(tuple2 -> new Tuple2<>(tuple2._2().getRequestTimes(), tuple2._2()))

//按照key倒排序

.sortByKey(false);

logger.info("take top " + TOP);

//取前10个

List<Tuple2<Integer, PageInfo>> top = sorts.take(TOP);

StringBuilder sbud = new StringBuilder("top “+ top + " word :\n”);

//打印出来

for(Tuple2<Integer, PageInfo> tuple2 : top){

sbud.append(tuple2._2().getName())

.append(“\t”)

.append(tuple2._1())

.append(“\n”);

}

logger.info(sbud.toString());

logger.info(“merge and save as file”);

//分区合并成一个,再导出为一个txt保存在hdfs

javaSparkContext

.parallelize(top)

.coalesce(1)

.map(

tuple2 -> new Tuple2<>(tuple2._2().getRequestTimes(), tuple2._2().getName() + " ### " + tuple2._2().getUrl() +" ### " + tuple2._2().getRaws().toString())

)

.saveAsTextFile(outputPath);

logger.info(“close context”);

//关闭context

javaSparkContext.close();

}

}

  1. 编码完成后,在pom.xml所在目录下编译构建jar包:

mvn clean package -Dmaven.test.skip=true

  1. 编译成功后,target目录下的sparkdemo-1.0-SNAPSHOT.jar就是应用jar包;

  2. 将sparkdemo-1.0-SNAPSHOT.jar提交到spark服务器上,我这里用的是docker环境,通过文件夹映射将容器的目录和宿主机目录对应起来,只要将文件放入宿主机的jars目录即可,您需要按照自己的实际情况上传;

提交任务

  1. 当前电脑上,维基百科网站的统计数据文件保存在目录/input_files/input

  2. 将维基百科网站的统计数据文件提交到hdfs,我这边用的是docker环境,提交命令如下:

docker exec namenode hdfs dfs -put /input_files/input /

  1. 提交成功后,在hdfs的web页面可见/input目录下的数据,如下:

在这里插入图片描述

  1. 将jar文件上传到spark服务再提交任务,我用的是docker环境,命令如下:

docker exec -it master spark-submit \

–class com.bolingcavalry.sparkdemo.app.WikiRank \

–executor-memory 1g \

–total-executor-cores 12 \

/root/jars/sparkdemo-1.0-SNAPSHOT.jar \

namenode \

8020

上述命令调动了12个executor,每个内存为1G,请您按照自己环境的实际情况来配置;

5. 由于本次要处理的文件较多(24个128兆的文件),因此耗时较长,需要耐心等待,您也可以减少上传文件数量来缩减处理时间,以下是web页面显示的处理情况:

在这里插入图片描述

6. 处理完成后,在控制台会打印简单的排名信息:

en 111840450

Main_Page 61148163

ja 20336203

es 18133852

Заглавная_страница 16997475

de 12537288

ru 10127971

fr 9296777

it 9011481

pt 5904807

id 3472100

tr 3089611

pl 3051718

ar 3023412

nl 2372696

zh 1987233

sv 1845525

fa 1687804

ko 1511408

commons 1138613

fi 1123291

th 1012375

vi 1007987

he 822433

Wikipedia:Hauptseite 767106

cs 750085

hu 687040

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

MySQL全家桶笔记

还有更多面试复习笔记分享如下

Java架构专题面试复习

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
3291

th 1012375

vi 1007987

he 822433

Wikipedia:Hauptseite 767106

cs 750085

hu 687040

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-mHWu4765-1712190805032)]

[外链图片转存中…(img-5KUY7Keq-1712190805032)]

[外链图片转存中…(img-QKtz0G49-1712190805032)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档

[外链图片转存中…(img-9l0ejUJC-1712190805032)]

还有更多面试复习笔记分享如下

[外链图片转存中…(img-7q0W8cW0-1712190805033)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值