实验目的
利用pyspark开发streamingContext程序,统计实时网络字节流数据,实现中英文分词统计,并将统计结果持久化保存到文本文件中
实现分词效果如下:
实验步骤
1. 开发datasourcesocket.py工具模拟生成socket字节流
2. 开发辅助类工具进行日志记录myLogger.py
3. 开发pysparkStreaming程序NetworkWordCountStatuefulText.py,实现中英文词频分词统计累加统计,并保存为文本
项目的代码结果如下:
(pyspark2024-py3.9) (base) pblh123@LeginR7:~/PycharmProjects/pyspark2024$ tree
.
├── datas
│ ├── checkpoint
│ ├── stopwords
│ │ ├── baidu_stopwords.txt
│ │ ├── cn_all_stopwords.txt
│ │ ├── cn_stopwords.txt
│ │ ├── hit_stopwords.txt
│ │ └── scu_stopwords.txt
│ ├── streaming
│ │ └── logfile
│ │ └── ollama_install.sh
│ ├── streamingoutput
│ │ ├── output-20241207-214320
│ │ │ ├── part-00000
│ │ │ └── _SUCCESS
│ │ ├── output-20241207-214340
│ │ │ ├── part-00000
│ │ │ └── _SUCCESS
│ │ ├── output-20241207-214400
│ │ │ ├── part-00000
│ │ │ └── _SUCCESS
│ │ ├── output-20241207-214420
│ │ │ ├── part-00000
│ │ │ └── _SUCCESS
│ │ ├── output-20241207-214440
│ │ │ ├── part-00000
│ │ │ └── _SUCCESS
│ │ ├── output-20241207-214500
│ │ │ ├── part-00000
│ │ │ └── _SUCCESS
│ │ └── output-20241207-214520
│ │ ├── part-00000
│ │ └── _SUCCESS
├── logs
└── src
├── charpter7
│ ├── datasourcesocket.py
│ ├── NetworkWordCountStatuefulText.py
├── __init__.py
├── __pycache__
│ └── __init__.cpython-39.pyc
└── utils
├── configparse.ini
├── __init__.py
├── myLogger.py
1. 开发datasourcesocket.py工具模拟生成socket字节流
用pycharm开发datasourcesocket.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
==================================================================
File Name: datasourcesocket.py$
Creation Date: 2024/12/7$ 21:34$
Author: John <pblh123@126.com>
Remarks: $
文件名称: datasourcesocket.py$
创建时间: 2024/12/7$ 21:34$
作 者: 李先生 <pblh123@126.com>
联系方式: your.phone.number
备 注: $
==================================================================
"""
import random
import socket
import threading
import time
from src.utils.myLogger import *
class DataSourceSocket:
def __init__(self, host='localhost', port=9999):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5) # 设置监听队列长度为5
logger.info("Server started, waiting for connections...")
def send_data(self, conn, addr):
data_container = self._prepare_data()
try:
while True:
random_item = random.choice(data_container)
logger.info(f"Sending to {addr}: {random_item}")
conn.sendall(random_item.encode('utf-8') + b'\n')
time.sleep(0.1) # 控制发送间隔时间
except Exception as e:
logger.error(f"Client {addr} disconnected unexpectedly: {e}")
finally:
conn.close()
logger.info(f"Connection with {addr} closed.")
def _prepare_data(self):
data_container = []
chinese_data = [
"你好,世界", "今天天气真好", "学习是一件快乐的事", "分享知识,传递快乐",
"探索未知的世界", "坚持就是胜利", "努力不懈,梦想终会实现", "失败乃成功之母",
"平凡造就非凡", "相信自己,你是最棒的", "I like Spark", "I like Flink",
"I like Hadoop", "大数据分析", "机器学习", "深度学习", "人工智能", "云计算",
"分布式系统", "区块链技术", "网络安全", "物联网应用", "Python编程", "Java开发",
"C++语言", "JavaScript框架", "React Native移动开发", "Vue.js前端开发",
"Docker容器化", "Kubernetes集群管理", "Git版本控制", "Agile敏捷开发",
"DevOps文化", "持续集成与部署", "性能优化技巧", "数据库设计原则",
"算法与数据结构", "操作系统原理", "计算机网络基础", "软件工程实践",
"项目管理技能", "团队协作精神", "创新思维培养", "职业发展规划"
]
for i in range(100):
data_container.append(f"Random number: {random.randint(0, 1000)}")
data_container.append(f"Special chars: !@#$%^&*()_+{i}")
data_container.extend(chinese_data)
return data_container
def accept_connections(self):
try:
while True:
conn, addr = self.server.accept()
logger.info(f"Connected by {addr}")
threading.Thread(target=self.send_data, args=(conn, addr), daemon=True).start()
except Exception as e:
logger.error(f"An error occurred while accepting connections: {e}")
def start(self):
try:
self.accept_connections()
except KeyboardInterrupt:
print("\nShutting down the server.")
finally:
self.server.close()
logger.info("Server socket closed.")
if __name__ == "__main__":
log_directory = "logs/sparkstreaming"
log_filename = "DataSourceSocket.log"
log_level = logging.DEBUG # 可以根据需要调整日志级别
# 初始化日志设置
logger = setup_logging(log_directory, log_filename, log_level)
# 启动数据源套接字服务
data_source_socket = DataSourceSocket()
data_source_socket.start()
2. 开发辅助类工具进行日志记录myLogger.py
# coding=utf8
import os
import logging
from logging.handlers import RotatingFileHandler
def setup_logging(log_dir, log_filename, log_level=None):
"""
设置日志记录,同时输出到控制台和文件。如果日志目录不存在则创建,支持日志滚动,日志文件名自定义。
参数:
log_dir (str): 日志文件所在的目录。
log_filename (str): 日志文件的名称(不包括路径)。
log_level (int or str, optional): 日志级别,默认为logging.INFO。
"""
logger = logging.getLogger()
# 如果已经存在处理器,则不再添加,避免重复日志
if not logger.handlers:
# 设置日志级别
if log_level is not None:
logger.setLevel(log_level)
else:
logger.setLevel(logging.INFO) # 设置最低日志级别
# 创建格式化器
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# 检查并创建日志目录(如果不存在)
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 构造日志文件路径
log_file_path = os.path.join(log_dir, log_filename)
# 创建文件处理器,支持日志滚动
file_handler = RotatingFileHandler(
log_file_path,
maxBytes=1024 * 1024 * 1024, # 每个日志文件的最大字节数为1GB
backupCount=5, # 最多保留5个备份日志文件
encoding='utf-8',
delay=False # 确保日志即时写入
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logging.info(f"Logging initialized with log file at {log_file_path}")
return logger
# 示例用法
if __name__ == "__main__":
log_directory = "logs/logdir" # 自定义日志目录
log_file_name = "app.log" # 自定义日志文件名
log_level = logging.DEBUG # 自定义日志级别
# 初始化日志设置,并获取日志记录器
logger = setup_logging(log_directory, log_file_name, log_level)
# 使用日志记录器记录日志信息
logger.info("This is an info message.")
logger.error("This is an error message.")
logger.debug("This is a debug message.")
3. 开发pysparkStreaming程序NetworkWordCountStatuefulText.py,实现中英文词频分词统计累加统计,并保存为文本
# coding:utf8
import argparse
from pyspark import SparkConf, SparkContext
from pyspark.streaming import StreamingContext
import jieba
import logging
import os
import re
from functools import lru_cache
from src.utils.myLogger import setup_logging
import configparser
# 提前编译正则表达式以提高性能
CHINESE_PATTERN = re.compile(r'[\u4e00-\u9fff]+')
ENGLISH_PATTERN = re.compile(r'[a-zA-Z0-9_]+')
# 创建一个ConfigParser对象并读取配置文件
config = configparser.ConfigParser()
config.read('/home/pblh123/PycharmProjects/pyspark2024/src/utils/configparse.ini')
try:
# 从配置文件中读取[Environment]部分的JAVA_HOME和SPARK_HOME
JAVA_HOME = config.get('Environment', 'JAVA_HOME')
SPARK_HOME = config.get('Environment', 'SPARK_HOME')
if not JAVA_HOME or not SPARK_HOME:
raise configparser.NoOptionError('JAVA_HOME or SPARK_HOME', 'Environment')
# 设置环境变量
os.environ['JAVA_HOME'] = JAVA_HOME
os.environ['SPARK_HOME'] = SPARK_HOME
except (configparser.NoSectionError, configparser.NoOptionError) as e:
raise EnvironmentError("Configuration file is missing necessary sections or options: " + str(e))
print(f"JAVA_HOME set to: {os.getenv('JAVA_HOME')}")
print(f"SPARK_HOME set to: {os.getenv('SPARK_HOME')}")
@lru_cache(maxsize=1)
def load_stopwords(file_path):
"""
从指定文件或文件夹中加载停用词列表。
参数:
file_path (str): 停用词文件或文件夹的路径。
返回:
frozenset: 包含停用词的集合。
"""
stopwords = set()
try:
if os.path.isfile(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
stopwords.update(line.strip() for line in f)
elif os.path.isdir(file_path):
for filename in os.listdir(file_path):
file_full_path = os.path.join(file_path, filename)
if os.path.isfile(file_full_path):
with open(file_full_path, 'r', encoding='utf-8') as f:
stopwords.update(line.strip() for line in f)
else:
raise ValueError(f"The path {file_path} is neither a file nor a directory.")
except Exception as e:
logging.error(f"Failed to load stopwords from {file_path}: {e}")
return frozenset(stopwords)
def split_words(line, broadcast_stopwords):
"""
分割文本并过滤掉停用词。
参数:
line (str): 输入文本行。
broadcast_stopwords (Broadcast[frozenset]): 广播变量包含停用词集合。
返回:
list: 分词后的单词列表。
"""
try:
stopwords = broadcast_stopwords.value
# 清理文本,移除非字母数字字符和汉字
clean_line = re.sub(r'[^\w\s\u4e00-\u9fff]', '', line)
# 分离中英文段落
chinese_segments = CHINESE_PATTERN.findall(clean_line)
english_segments = ENGLISH_PATTERN.findall(clean_line)
# 对中文段落进行分词
chinese_words = [word for segment in chinese_segments
for word in jieba.lcut(segment)
if len(word) > 1 and word not in stopwords]
# 对英文段落进行分割,保留原样
english_words = [word for word in english_segments
if len(word) > 1 and word not in stopwords]
# 合并结果
combined_words = chinese_words + english_words
return combined_words
except Exception as e:
logging.error(f"Error processing line: {line}, Error: {e}", exc_info=True)
return []
def create_spark_contexts(app_name, batch_interval, master_url=None):
"""
创建 SparkContext 和 StreamingContext。
参数:
app_name (str): 应用程序名称。
batch_interval (int): 批处理间隔时间(秒)。
master_url (str, optional): Spark Master URL,默认为 None 表示本地模式。
返回:
tuple: 包含 SparkContext 和 StreamingContext 的元组。
"""
conf = SparkConf().setAppName(app_name)
if master_url:
conf.setMaster(master_url)
sc = SparkContext(conf=conf)
ssc = StreamingContext(sc, batch_interval)
return sc, ssc
def update_word_counts(new_values, last_sum):
"""
更新单词计数的状态。
参数:
new_values (list): 新的值列表。
last_sum (int): 上一次的总和。
返回:
int: 更新后的总和。
"""
return sum(new_values) + (last_sum or 0)
def log_output(rdd, logger):
"""
使用日志记录器记录 RDD 中的内容。
参数:
rdd (RDD): 要记录内容的 RDD。
logger (logging.Logger): 日志记录器实例。
"""
collected = rdd.collect()
for item in collected:
logger.info(f"Count: {item}")
def save_counts_to_text_files(time, rdd, output_dir, num_partitions, logger):
"""
控制输出文件的分区数并保存结果到文本文件。
参数:
time (datetime.datetime): 当前批次的时间戳。
rdd (RDD): 要保存的 RDD。
output_dir (str): 输出文件夹路径。
num_partitions (int): 输出文件的分区数。
logger (logging.Logger): 日志记录器实例。
"""
try:
if not rdd.isEmpty():
# 根据需要调整分区数
repartitioned_rdd = rdd.coalesce(num_partitions) if num_partitions > 0 else rdd
output_path = os.path.join(output_dir, f"output-{time.strftime('%Y%m%d-%H%M%S')}")
repartitioned_rdd.saveAsTextFile(output_path)
logger.info(f"Saved results to {output_path}")
except Exception as e:
logger.error(f"Error saving counts to text files at time {time}: {e}", exc_info=True)
def start_spark_streaming(hostname, port, checkpoint_dir, stopwords_path, output_dir, num_partitions, logger,
master_url=None):
"""
启动 Spark Streaming 应用程序。
参数:
hostname (str): 主机名。
port (int): 端口号。
checkpoint_dir (str): 检查点目录路径。
stopwords_path (str): 停用词文件或目录路径。
output_dir (str): 输出文件夹路径。
num_partitions (int): 输出文件的分区数。
master_url (str, optional): Spark Master URL,默认为 None 表示本地模式。
"""
try:
# 设置检查点目录,确保该目录存在并且可写
if not os.path.exists(checkpoint_dir) and not checkpoint_dir.startswith("hdfs"):
os.makedirs(checkpoint_dir)
# 创建 SparkContext 和 StreamingContext
sc, ssc = create_spark_contexts("WindowedWordCounts", 20, master_url)
ssc.checkpoint(checkpoint_dir)
# 从指定的主机和端口读取数据流
lines = ssc.socketTextStream(hostname, port)
# 广播停用词列表
stopwords = load_stopwords(stopwords_path)
broadcast_stopwords = sc.broadcast(stopwords)
# 对每一行进行分词,计算每个单词的出现次数,并更新状态
counts = lines.flatMap(lambda line: split_words(line, broadcast_stopwords)) \
.map(lambda word: (word, 1)) \
.updateStateByKey(update_word_counts)
# 使用日志记录器记录结果
counts.foreachRDD(lambda time, rdd: log_output(rdd, logger))
# 控制输出文件的分区数并保存结果到文本文件
counts.foreachRDD(lambda time, rdd: save_counts_to_text_files(time, rdd, output_dir, num_partitions, logger))
# 启动流处理
ssc.start()
try:
ssc.awaitTermination()
except KeyboardInterrupt:
logger.warning("Caught keyboard interrupt. Gracefully stopping...")
except Exception as e:
logger.error(f"An error occurred while running Spark Streaming: {e}", exc_info=True)
finally:
ssc.stop(stopSparkContext=True)
except Exception as e:
logger.error(f"An error occurred while starting Spark Streaming: {e}", exc_info=True)
if __name__ == "__main__":
# 解析命令行参数
parser = argparse.ArgumentParser(description="Run Spark Streaming with specified parameters.")
parser.add_argument('hostname', type=str, help='Hostname to connect to.')
parser.add_argument('port', type=int, help='Port number to connect to.')
parser.add_argument('checkpoint_dir', type=str, help='Path to the checkpoint directory.')
parser.add_argument('stopwords', type=str, help='Path to the stopwords file or directory.')
parser.add_argument('output_dir', type=str, help='Directory to save output files.')
parser.add_argument('--partitions', type=int, default=1,
help='Number of partitions for output files. Default is 1.')
parser.add_argument('--mode', type=str, default='local',
help='Execution mode: "local" for local execution or a Spark Master URL for cluster execution.')
args = parser.parse_args()
master_url = args.mode.lower() != 'local' and args.mode or None
# 配置日志级别为 ERROR,以减少不必要的输出信息
logDir = "logs"
logfilename = "spark_streaming.log"
logLevel = logging.ERROR
mylogger = setup_logging(logDir, logfilename, logLevel)
start_spark_streaming(args.hostname, args.port, args.checkpoint_dir, args.stopwords, args.output_dir,
args.partitions, mylogger, master_url)
验证实验
运行datasourcesocket.py
运行NetworkWordCountStatuefulText.py
1. 配置运行参数
依据自己的环境修改配置路径
2. 运行程序
运行前检查路径,目标目录是空的
启动运行程序
退出后,检查持久化的结果文件信息,查看中英混合分词统计
更多技术文档分享,讨论,欢迎关注微信公众号: