PySpark3.4.4_基于StreamingContext实现网络字节流中英文分词词频累加统计结果保存到文本中

实验目的

利用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. 运行程序

运行前检查路径,目标目录是空的

启动运行程序

退出后,检查持久化的结果文件信息,查看中英混合分词统计

更多技术文档分享,讨论,欢迎关注微信公众号:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值