(1)思路
(2)单独使用一个Topic
2.在Spark Streaming中接收和清洗数据
3. 在Spark Streaming中调用一个时间窗口
4. 调用MLlib的协同过滤算法
使用Python读取Online Retail数据,模拟实时订单信息发送给Kafka,在Spark Streaming中提交数据,。
在Python中读取Online Retail.txt,按行遍历,每秒送入十个订单给Kafka(一个订单可能有多行数据)。
使用time.sleep函数控制送入订单的速度
模拟场景:用户浏览某个商品时,后台网站为其计算相关推荐商品。打开一个Kafka消息发送窗口,单独使用一个Topic,发送用户ID和他浏览的商品ID给Spark Streaming,程序应考虑到用户的并发访问,要支持多个用户ID同时发送和处理。
2.实现数据接收、提取和清洗:
在Spark Streaming中接收和清洗数据,去掉空数据和非法数据等。
3.计算评分:
在Spark Streaming中调用一个时间窗口(如:1小时,4小时)的数据计算用户对商品的评分
(选做)可只提取包含了用户ID和浏览商品ID的订单数据计算评分,以减少计算量。
调用近期时间窗口文章的方法:
- 将清洗后的订单按时间序列存储,在Spark Streaming中读取近期订单
- 使用DStream窗口算子
4.执行推荐算法:
调用MLlib的协同过滤算法,输入第(3)步中的评分数据,将用户ID对应的推荐结果返回给Kafka。
打开接收端接收返回每个用户的推荐商品。
(1)在这里我的思路是按行读取存储,然后判断相邻两行的订单号是否相等,在发送完相同订单的最后一个订单后,等待0.1秒发送下一个订单(具体过程细节在代码注释里边)
(2)单独使用一个Topic,发送用户ID和他浏览的商品ID给Spark Streaming
2.在Spark Streaming中接收和清洗数据,去掉空数据和非法数据等。
(由于发送的数据有历史的也有预测的,所以这里同时接收不同topic内容进行不同处理)
(1)设计正则表达式进行清洗
(2) 进行清洗
(分词后的数据要转化为list不然是结构化数据,会报错)
(3)将清洗后的订单按时间序列存储
结果展示:
3.在Spark Streaming中调用一个时间窗口的数据计算用户对商品的评分,评分规则自行制定 (1)使用DStream窗口算子
(在使用的过程中出现了这个错误,在网上搜寻和询问老师后发现是使用窗口算子前未对数据进行序列化造成的)
我在代码里使用map进行序列化后解决
(2)把数据转化为dataFrame,去除清洗后为空的数据
(3)对数据进行处理计算评分并保存
结果展示:
4.调用MLlib的协同过滤算法,输入第(3)步中的评分数据,将用户ID对应的推荐结果返回给Kafka。
(1)在另外一个代码里边调用算法对评分数据进行训练,把训练好的模型覆盖保存
(这里也报了序列化的错误,网上搜了很久很久最后看到一篇文章根据官网上发现的是scala版本的问题,scala版本要在2.12.8以上修改后得以解决)
结果展示:
(2)在流里边接收处理模拟场景的数据进行转换(dataFrame)和处理
(3)在流里边调用训练好的模型对处理好的数据进行实时推荐
(4)新建一个生产端口,把推荐好的数据返回到虚拟机的消费端
import json
import time
import traceback
from kafka.errors import kafka_errors
from kafka import KafkaProducer
#发送数据
# 实例化一个KafkaProducer示例,用于向Kafka投递消息
producer = KafkaProducer(
key_serializer=lambda k: json.dumps(k).encode('utf-8'),
value_serializer=lambda v: json.dumps(v).encode('utf-8')
, bootstrap_servers=['hadoop102:9092'])
# 读取文件数据
f = open('Online Retail.txt', encoding='utf-8')
txt = []
for line in f:
# 把每一行的数据分词后存储在一个数组里
txt.append(line.strip())
for i in range(len(txt)):
# 发送数据,topic为'Online_Retail'
data = txt[i]
# 相同订单号不设置时间快速发送,最后一个相同的订单设置为0.1秒发送
name = txt[i][0]
if name != txt[i+1][0]:
time.sleep(0.1) # 每隔0.1秒发送一个订单数据
future = producer.send('Online_Retail', key='Online', value=data)
else:
future = producer.send('Online_Retail', key='Online', value=data)
try:
future.get(timeout=10) # 监控是否发送成功
except kafka_errors: # 发送失败抛出kafka_errors
traceback.format_exc()
package org.example
import java.util.Properties
import org.apache.kafka.clients.producer.{Callback, KafkaProducer, ProducerRecord, RecordMetadata}
import org.apache.kafka.common.serialization.StringSerializer
import org.apache.spark.sql.SparkSession
import scala.io.Source
object Send_commend {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.master("local[*]").appName("Spark SQL").getOrCreate()
val kafkaProp = new Properties()
kafkaProp.put("bootstrap.servers", "hadoop102:9092")
//设置
kafkaProp.put("acks", "1")
// 请求失败重试次数
kafkaProp.put("retries", "3")
// 指定key和value的序列化方式
kafkaProp.put("key.serializer", classOf[StringSerializer].getName)
kafkaProp.put("value.serializer", classOf[StringSerializer].getName)
val producer = new KafkaProducer[String, String](kafkaProp)
val lines = Source.fromFile("D:/python/实践课/Online Retail.txt").getLines()
while (lines.hasNext) {
val line = lines.next()
//按照分隔符分割成列表
val word = line.split("\t")
//发送发送用户ID和他浏览的商品ID给Spark Streaming
val record = new ProducerRecord[String, String]("user_topic", word(6)+" "+word(1))
//这里采用的是异步发送回调的方式,保证数据传输的可靠性
producer.send(record, new Callback {
override def onCompletion(metadata: RecordMetadata, exception: Exception): Unit = {
if (metadata != null) {
println("发送成功")
}
if (exception != null) {
println("消息发送失败")
}
}
})
}
producer.close()
}
}
package org.example
import org.apache.kafka.common.serialization.{StringDeserializer, StringSerializer}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import java.util.{Date, Properties}
import org.apache.commons.lang.StringEscapeUtils
import org.apache.kafka.clients.producer.{Callback, KafkaProducer, ProducerRecord, RecordMetadata}
import org.apache.spark.ml.recommendation.{ALS, ALSModel}
import org.apache.spark.sql.{Row, SparkSession}
import org.apache.spark.sql.types.{DateType, DoubleType, IntegerType, StringType, StructField, StructType}
import java.io.{File, PrintWriter}
import java.text.SimpleDateFormat
object Recommendation_system {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("Direct")
.setMaster("local") //提交模式
.set("spark.streaming.receiver.writeAheadLog.enable", "true")
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "hadoop102:9092", //Kafka broker地址
"group.id" -> "spark-streaming", //Kafka consumer名称
// latest:表示如果记录了偏移量则从记录的位置开始读取数据,如果没有记录则从最新/最后的位置开始消费
// earliest:表示如果记录了偏移量则从记录的位置开始读取,如果没有记录则从最开始的位置开始消费
// none:表示如果记录了偏移量则从记录的位置开始读取,如果没有则报错
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean), // 是否自动提交偏移量
// 消息实际是键值对结构,传送前需要序列化为二进制序列,接收后需要将二进制反序列化还原为键值对
"key.deserializer" -> classOf[StringDeserializer], // Key反序列化类型
"value.deserializer" -> classOf[StringDeserializer] // Value反序列化类型
)
val ssc = new StreamingContext(conf, Seconds(10)) // 微批时间间隔
ssc.checkpoint("file:///D:/root/spark_streaming_checkpoint") //Windows中运行时采用Windows风格的路径
//同时接收多个topic
val topic = Array("Online_Retail","user_topic") //Kafka主题
//创建DStream
val input = KafkaUtils.createDirectStream[Int, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[Int, String](topic, kafkaParams))
//过滤出要不同处理的topic内容进行处理
val input_data = input.filter(x=>x.topic()=="Online_Retail")
//用map进行序列化
.map(x=>x.value())
//设置时间窗口,滑动时间必须是微批时间的倍数
.window(Seconds(3600),Seconds(30))
//处理预测的数据
val input_predict = input.filter(x=>x.topic()=="user_topic")
//用map进行序列化
.map(x=>x.value())
//设置时间窗口,滑动时间必须是微批时间的倍数
.window(Seconds(3600),Seconds(30))
val spark = SparkSession.builder()
.master("local[*]").appName("Spark SQL").getOrCreate()
//定义DStream处理方法
input_data.foreachRDD((rdd, time) => {
val count = rdd.count()
//设计过滤非法数据的正则表达式
val intRegx = "^\\d+$".r
val doubleRegx = "^\\d+(\\.\\d+)?$".r
val timeRegx = "^\\d{1,2}/\\d{1,2}/\\d{4} \\d{1,2}:\\d{1,2}$".r
val timeFormat = new SimpleDateFormat("M/d/yyyy h:m")
//若接收到数据,则开始处理
if (count > 0) {
//计算逻辑写在此
//对数据转化为字节序列重新编码成为中文
val data_set = rdd.map(x =>new String(StringEscapeUtils.unescapeJava(x).getBytes("UTF-8")))
//用分隔符切分各个部分,这里必须转化为list不然打印出来是结构,会报错
.map(x=>x.split("\\t").toList)
//去除长度不够有缺失的值
.filter(x=>x.length>=8)
.map(x=>(x(0), x(1),x(2), x(3),x(4), x(5),x(6), x(7)))
//用正则表达式清洗处理各个非法数据
.filter(x => !intRegx.findFirstIn(x._4).isEmpty && //以整型规则过滤Quantity列
!timeRegx.findFirstIn(x._5).isEmpty && //以日期规则过滤InvoiceDate列
!doubleRegx.findFirstIn(x._6).isEmpty //以浮点型规则过滤UnitPrice列
//转换数据类型方便后续计算评分
).map(x => (x._1, x._2, x._3, x._4.toInt,
new java.sql.Date(timeFormat.parse(x._5).getTime), x._6.toDouble, x._7, x._8))
.sortBy(x=>x._5.getTime)
// 将清洗后的订单按时间序列存储
data_set.saveAsTextFile("file:///D:/spark_streaming_kafka/stream_time_results/"
+ topic.toList(0) + "/" + new Date().getTime.toString + "_" + count)
//计算评分,把清洗后的数据转化为df
//Schema的设置
val s = StructType(Array(StructField("InvoiceNo", StringType, false),
StructField("StockCode", StringType, false),
StructField("Description", StringType, false),
StructField("Quantity", IntegerType, false),
StructField("InvoiceDate", DateType, false),
StructField("UnitPrice", DoubleType, false),
StructField("CustomerID", StringType, false),
StructField("Country", StringType, false)))
val df_final = spark.createDataFrame(data_set.map(x => Row.fromTuple(x)), s)
//去除空数据,所有字段不能为空,可将所有包含空值行或无法做数据类型转换的行视为无效行去掉。
.na.drop()
//计算整个窗口的评分并保存
//按用户ID和商品ID共同分组查询,并统计用户购买该商品的次数(不是数量)
//评分逻辑:将用户对商品的购买和重复购买行为视为对商品的评分
val df_set = df_final.groupBy("CustomerID", "StockCode").count
// 将用户ID、商品ID和用户购买商品次数分别投影到userID、itemID和rating字段
.selectExpr("hash(CustomerID) as userID",
" hash (StockCode) as itemID", "count as rating")
df_set.write.json("file:///D:/spark_streaming_kafka/stream_time_json/")
}
})
input_predict.foreachRDD((rdd, time) => {
val count = rdd.count()
//若接收到数据,则开始处理
if (count > 0) {
//计算逻辑写在此
//对数据转化为字节序列重新编码成为中文
val dt_set = rdd.map(x =>new String(StringEscapeUtils.unescapeJava(x).getBytes("UTF-8")))
//过滤出要处理的topic内容进行处理
//用分隔符切分各个部分,这里必须转化为list不然打印出来是结构,会报错
.map(x=>x.split(" ").toList)
.map(x=>(x(0), x(1))
//转换数据类型方便后续计算评分
).map(x => (x._1, x._2))
//存储数据
dt_set.saveAsTextFile("file:///D:/spark_streaming_kafka/stream_time_results/"
+ topic.toList(1) + "/" + new Date().getTime.toString + "_" + count)
//计算评分,把清洗后的数据转化为df
//Schema的设置
val s = StructType(Array(
StructField("StockCode", StringType, false),
StructField("CustomerID", StringType, false)))
val df_commend = spark.createDataFrame(dt_set.map(x => Row.fromTuple(x)), s)
//去除空数据,所有字段不能为空,可将所有包含空值行或无法做数据类型转换的行视为无效行去掉。
.na.drop()
//计算整个窗口的评分并保存
//按用户ID和商品ID共同分组查询,并统计用户购买该商品的次数(不是数量)
//评分逻辑:将用户对商品的购买和重复购买行为视为对商品的评分
val df_set = df_commend.groupBy("CustomerID", "StockCode").count
// 将用户ID、商品ID和用户购买商品次数分别投影到userID、itemID和rating字段
.selectExpr("hash(CustomerID) as userID",
" hash (StockCode) as itemID", "count as rating")
//df_set.write.json("file:///D:/spark_streaming_kafka/stream_time_json/")
//读取训练好的模型,对传入的数据进行预测
val Mymodel = ALSModel.load("D:/mymodel/")
//传入要预测的数据,为每个用户返回三个推荐商品
val userRecs = Mymodel.recommendForUserSubset(df_set,3)
userRecs.selectExpr("userID", "explode(recommendations.itemID) as itemID")
//由于结果是dataframe无法直接传输,这里我的思路是转成rdd读取每行再进行传输
val context = userRecs.rdd.collect()
//新建一个发送端口
val kafkaProp = new Properties()
//设置接收端口
kafkaProp.put("bootstrap.servers", "hadoop102:9092")
kafkaProp.put("acks", "1")
// 请求失败重试次数3
kafkaProp.put("retries", "3")
// 指定key和value的序列化方式
kafkaProp.put("key.serializer", classOf[StringSerializer].getName)
kafkaProp.put("value.serializer", classOf[StringSerializer].getName)
val producer = new KafkaProducer[String, String](kafkaProp)
for (i<-context){
val record = new ProducerRecord[String, String]("user",i.toString() )
//这里采用的是异步发送回调的方式,保证数据传输的可靠性
producer.send(record, new Callback {
override def onCompletion(metadata: RecordMetadata, exception: Exception): Unit = {
if (metadata != null) {
println("发送成功")
}
if (exception != null) {
println("消息发送失败")
}
}
})
}
producer.close()
}
})
//启动Spark Streaming
ssc.start()
ssc.awaitTermination()
}
}
package org.example
import org.apache.spark.ml.recommendation.ALS
import org.apache.spark.sql.SparkSession
object Kafka_Send {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.master("local[*]").appName("Spark SQL").getOrCreate()
val df = spark.read.json("file:///D:/spark_streaming_kafka/stream_time_json/")
// 按8:2的比例将第(2)步的结果数据划分为训练集和测试集
val Array(training, test) = df.randomSplit(Array(0.8, 0.2))
//创建一个协同过滤模型,并将用户列、商品列和评分列分别设为第(2)步中的列名,并设定其他的模型参数
val my_als = new ALS()
.setUserCol("userID") //设置用户字段名
.setItemCol("itemID") //设置商品字段名
.setRatingCol("rating") //设置评分字段名
.setMaxIter(5) //设置最大迭代次数
.setRegParam(0.01) //设置惩罚系数
//送入训练集数据训练模型
val model = my_als.fit(training)
//将测试集数据送入模型进行模型评估,将均方根误差保存至本地文件
model.setColdStartStrategy("drop") //冷启动策略设为drop以免出现nan值
//覆盖保存我的模型
model.write.overwrite().save("D:/mymodel/")
}
}
本次任务是做一个实时的推荐系统,模拟用户场景对浏览商品的用户进行推荐商品。
在本次任务中遇到的几个问题大多都是需要序列化,这也让我对于流处理数据的转化过程有了更多的了解,需要序列化。其次就是在答辩的时候老师提出的我不在流里边进行推荐发送和文件应该覆盖存储的问题,由于时间关系我解决了第一个,思路是离线训练模型,在线调用训练好的模型进行推荐处理。
最后经过这两周的实践课,自己感觉对于流处理实时处理的流程,大致细节有了更多的了解,对于自己以后的学习也会更加的努力和坚定。