如何使用Python和Apache Spark处理日志数据

当今,利用分析的最流行和有效的企业用例之一是日志分析。 如今,几乎每个组织都日复一日地运行着多个系统和基础架构。 为了有效地保持业务运行,这些组织需要知道其基础架构是否正在发挥最大的潜力。 查找涉及分析系统和应用程序日志,甚至可能对日志数据进行预测分析。 通常,所涉及的日志数据量很大,这取决于所涉及的组织基础结构的类型以及在其上运行的应用程序。

The log data processing pipeline by Doug Henschen.

日志数据处理管道。

由于计算限制,我们只能在一台机器上分析数据样本的日子已经一去不复返了。 在大数据,更好的分布式计算以及像Apache Spark这样的框架进行大数据处理和开源分析的支持下,我们每天可以对数十亿条日志消息执行可伸缩的日志分析。 本案例研究指南的目的是采用动手方法,展示我们如何利用Spark来对半结构化日志数据进行大规模的日志分析。 如果您对使用Spark的可伸缩SQL感兴趣,请随时使用Spark大规模查看SQL

尽管有许多出色的开源框架和工具可用于日志分析(例如Elasticsearch) ,但该分为两部分的教程的目的是展示如何利用Spark来大规模分析日志。 在现实世界中,您当然可以在分析日志数据时自由选择自己的工具箱。

让我们开始吧!

主要目标:NASA日志分析

Spark使您可以廉价地将日志转储并存储到磁盘上的文件中,同时仍提供丰富的API来进行大规模数据分析。 这个动手案例研究将向您展示如何在NASA的实际生产日志上使用Apache Spark,同时学习数据争用以及探索性数据分析的基本而强大的技术。 在这项研究中,我们将分析佛罗里达州NASA肯尼迪航天中心网络服务器上的日志数据集。

完整的数据集(包含对NASA肯尼迪航天中心的所有HTTP请求的价值,为两个月)可在此处免费下载。 或者,如果您更喜欢FTP:

接下来,如果要继续学习,请从我的GitHub下载该教程,并将这两个文件都放置在与该教程的Jupyter Notebook相同的目录中。

设置依赖

第一步是确保您有权访问Spark会话和集群。 对于此步骤,您可以使用自己的本地Spark设置或基于云的设置。 通常, 如今大多数云平台都提供Spark集群,并且您还有免费的选择,包括Databricks社区版 。 本教程假定您已经设置了Spark,因此我们不会花费额外的时间从头开始配置或设置Spark。

通常,在启动Jupyter Notebook服务器时,预配置的Spark设置通常已经预先加载了必要的环境变量或依赖项。 就我而言,我可以在笔记本中使用以下命令检查它们:

 spark 
Spark session

这些结果表明,我的集群目前正在运行Spark 2.4.0。 我们还可以使用以下代码检查sqlContext是否存在:

 sqlContext 

<pyspark.sql.context.SQLContext at 0x7fb1577b6400>

现在,如果您没有预先配置这些变量并出现错误,则可以使用以下代码加载和配置它们:


   
   
# configure spark variables
from pyspark. context import SparkContext
from pyspark. sql . context import SQLContext
from pyspark. sql . session import SparkSession
   
sc = SparkContext ( )
sqlContext = SQLContext ( sc )
spark = SparkSession ( sc )

# load up other dependencies
import re
import pandas as pd

我们还需要加载其他库以使用DataFrame正则表达式 。 使用正则表达式是解析日志文件的主要方面之一。 该工具提供了强大的模式匹配技术,可用于提取和查找半结构化和非结构化数据中的模式。

The Perl Problems strip from xkcd.

来自xkcd的Perl问题带。

正则表达式可能非常有效且功能强大,但是它们也可能使人感到困惑和困惑。 不过不用担心,通过实践,您可以真正利用它们的最大潜力。 以下示例展示了在Python中使用正则表达式的方法。 在这里,我们尝试查找给定输入句子中单词“ spark”的所有出现。


   
   
m = re . finditer ( r '.*?(spark).*?' , "I'm searching for a spark in PySpark" , re . I )
for match in m:
    print ( match , match. start ( ) , match. end ( ) )

<_sre.SRE_Match object; span=(0, 25), match=“I'm searching for a spark”> 0 25
<_sre.SRE_Match object; span=(25, 36), match=' in PySpark'> 25 36

让我们继续分析的下一部分。

加载和查看NASA日志数据集

鉴于我们的数据存储在以下路径中(以平面文件的形式),让我们将其加载到DataFrame中。 我们将分步进行。 以下代码加载磁盘的日志数据文件名:


   
   
import glob

raw_data_files = glob . glob ( '*.gz' )
raw_data_files

['NASA_access_log_Jul95.gz', 'NASA_access_log_Aug95.gz']

现在,我们将使用sqlContext.read.text()spark.read.text()读取文本文件。 这段代码产生一个带有单个字符串列的DataFrame,称为value


   
   
base_df = spark.read.text(raw_data_files)
base_df.printSchema()

root
|-- value: string (nullable = true)

此输出使我们能够查看将很快检查的日志数据模式的文本。 您可以使用以下代码查看保存日志数据的数据结构类型:

 type ( base_df ) 

pyspark.sql.dataframe.DataFrame

在整个教程中,我们使用Spark DataFrames。 但是,如果需要,还可以通过添加以下代码将DataFrame转换为Spark的原始数据结构()的弹性分布式数据集(RDD)


   
   
base_df_rdd = base_df. rdd
type ( base_df_rdd )

pyspark.rdd.RDD

现在让我们看一下DataFrame中的实际日志数据:

 base_df. show ( 10 , truncate = False ) 
The log data within the dataframe.

数据框中的日志数据。

此结果肯定看起来像标准的半结构服务器日志数据。 在此文件有用之前,我们肯定需要进行一些数据处理和整理。 请记住,从RDD访问数据略有不同,如下所示:

 base_df_rdd. take ( 10 ) 
Log data from the resilient distributed datasets.

从弹性分布式数据集中记录数据。

现在我们已经加载并查看了日志数据,让我们对其进行处理和纠缠。

数据整理

在本节中,我们将清理并解析日志数据集,以从每条日志消息中提取具有有意义信息的结构化属性。

日志数据理解

如果您熟悉Web服务器日志,您将认识到上面显示的数据是“ 通用日志格式” 。 这些字段是:   

remotehost rfc931 authuser [date] "request" status bytes

领域 描述
remotehost 远程主机名(如果DNS主机名不可用或DNSLookup关闭, 则为 IP号)。
rfc931 用户的远程登录名(如果存在)。
authuser 通过HTTP服务器验证后,远程用户的用户名。
[date] 请求的日期和时间。
“request” 完全来自浏览器或客户端的请求。
status 服务器发送回客户端的HTTP状态代码
bytes 传输到客户端的字节数( Content-Length )。

现在,我们需要从日志数据中解析,匹配和提取这些属性的技术。

使用正则表达式进行数据解析和提取

接下来,我们必须将半结构化日志数据解析为单独的列。 我们将使用特殊的内置regexp_extract()   函数进行解析。 此函数将列与具有一个或多个捕获 的正则表达式匹配,并允许您提取匹配的组之一。 对于要提取的每个字段,我们将使用一个正则表达式。

到目前为止,您一定已经听说或使用了很多正则表达式。 如果您发现正则表达式令人困惑(当然可以 ),并且想了解更多有关它们的信息,建议您访问RegexOne网站 。 您可能还会发现Goyvaerts和Levithan撰写的Regular Expressions Cookbook是有用的参考。

让我们看一下数据集中正在处理的日志总数:

 print ( ( base_df. count ( ) , len ( base_df. columns ) ) ) 

(3461613, 1)

看起来我们总共有大约346万条日志消息。 数目不小! 让我们提取并查看一些示例日志消息:


   
   
sample_logs = [ item [ 'value' ] for item in base_df. take ( 15 ) ]
sample_logs
Sample log messages.

样本日志消息。

提取主机名

让我们写一些正则表达式从日志中提取主机名:


   
   
host_pattern = r '(^ \S + \. [ \S + \. ]+ \S +) \s '
hosts = [ re . search ( host_pattern , item ) . group ( 1 )
            if re . search ( host_pattern , item )
            else 'no match'
            for item in sample_logs ]
hosts

['199.72.81.55',
'unicomp6.unicomp.net',
'199.120.110.21',
'burger.letters.com',
…,
…,
'unicomp6.unicomp.net',
'd104.aa.net',
'd104.aa.net']

提取时间戳

让我们使用正则表达式从日志中提取时间戳字段:


   
   
ts_pattern = r ' \[ ( \d {2}/ \w {3}/ \d {4}: \d {2}: \d {2}: \d {2} - \d {4})]'
timestamps = [ re . search ( ts_pattern , item ) . group ( 1 ) for item in sample_logs ]
timestamps

['01/Jul/1995:00:00:01 -0400',
'01/Jul/1995:00:00:06 -0400',
'01/Jul/1995:00:00:09 -0400',
…,
…,
'01/Jul/1995:00:00:14 -0400',
'01/Jul/1995:00:00:15 -0400',
'01/Jul/1995:00:00:15 -0400']

提取HTTP请求方法,URI和协议

现在,让我们使用正则表达式从日志中提取HTTP请求方法, URI和协议模式字段:


   
   
method_uri_protocol_pattern = r ' \" ( \S +) \s ( \S +) \s *( \S *) \" '
method_uri_protocol = [ re . search ( method_uri_protocol_pattern , item ) . groups ( )
                if re . search ( method_uri_protocol_pattern , item )
                else 'no match'
              for item in sample_logs ]
method_uri_protocol

[('GET', '/history/apollo/', 'HTTP/1.0'),
('GET', '/shuttle/countdown/', 'HTTP/1.0'),
…,
…,
('GET', '/shuttle/countdown/count.gif', 'HTTP/1.0'),
('GET', '/images/NASA-logosmall.gif', 'HTTP/1.0')]

提取HTTP状态代码

现在,让我们使用正则表达式从日志中提取HTTP状态代码:


   
   
status_pattern = r ' \s ( \d {3}) \s '
status = [ re . search ( status_pattern , item ) . group ( 1 ) for item in sample_logs ]
print ( status )

['200', '200', '200', '304', …, '200', '200']

提取HTTP响应内容大小

现在,让我们使用正则表达式从日志中提取HTTP响应内容的大小:


   
   
content_size_pattern = r ' \s ( \d +)$'
content_size = [ re . search ( content_size_pattern , item ) . group ( 1 ) for item in sample_logs ]
print ( content_size )

['6245', '3985', '4085', '0', …, '1204', '40310', '786']

放在一起

现在,让我们利用以前构建的所有正则表达式模式,并使用regexp_extract(...)方法构建DataFrame,并将所有日志属性整齐地提取到其单独的列中。


   
   
from pyspark. sql . functions import regexp_extract

logs_df = base_df. select ( regexp_extract ( 'value' , host_pattern , 1 ) . alias ( 'host' ) ,
                         regexp_extract ( 'value' , ts_pattern , 1 ) . alias ( 'timestamp' ) ,
                         regexp_extract ( 'value' , method_uri_protocol_pattern , 1 ) . alias ( 'method' ) ,
                         regexp_extract ( 'value' , method_uri_protocol_pattern , 2 ) . alias ( 'endpoint' ) ,
                         regexp_extract ( 'value' , method_uri_protocol_pattern , 3 ) . alias ( 'protocol' ) ,
                         regexp_extract ( 'value' , status_pattern , 1 ) . cast ( 'integer' ) . alias ( 'status' ) ,
                         regexp_extract ( 'value' , content_size_pattern , 1 ) . cast ( 'integer' ) . alias ( 'content_size' ) )
logs_df. show ( 10 , truncate = True )
print ( ( logs_df. count ( ) , len ( logs_df. columns ) ) )
Log dataframe extracted using regexp_extract(...)

使用regexp_extract(...)提取的日志数据帧

寻找缺失值

缺失值和空值是数据分析和机器学习的祸根。 让我们看看我们的数据解析和提取逻辑如何工作。 首先,让我们验证原始DataFrame中没有空行:


   
   
( base_df
    . filter ( base_df [ 'value' ]
                . isNull ( ) )
    . count ( ) )

0

都好! 现在,如果我们的数据解析和提取工作正常,则不应有任何行具有潜在的空值。 让我们尝试对其进行测试:


   
   
bad_rows_df = logs_df. filter ( logs_df [ 'host' ] . isNull ( ) |
                             logs_df [ 'timestamp' ] . isNull ( ) |
                             logs_df [ 'method' ] . isNull ( ) |
                             logs_df [ 'endpoint' ] . isNull ( ) |
                             logs_df [ 'status' ] . isNull ( ) |
                             logs_df [ 'content_size' ] . isNull ( ) |
                             logs_df [ 'protocol' ] . isNull ( ) )
bad_rows_df. count ( )

33905

哎哟! 看起来我们的数据中缺少超过33K的值! 我们可以处理吗?

请记住,这不是常规的熊猫(链接)DataFrame,您可以直接查询它并获取哪些列为空。 我们所谓的大数据集驻留在磁盘上,该磁盘可能存在于spark集群的多个节点中。 那么,我们如何找出哪些列具有潜在的空值?

查找空计数

我们通常可以使用以下技术来找出哪些列具有空值。

注意:此方法改编自StackOverflow的一个很好的答案


   
   
from pyspark. sql . functions import col
from pyspark. sql . functions import sum as spark_sum

def count_null ( col_name ) :
    return spark_sum ( col ( col_name ) . isNull ( ) . cast ( 'integer' ) ) . alias ( col_name )

# Build up a list of column expressions, one per column.
exprs = [ count_null ( col_name ) for col_name in logs_df. columns ]

# Run the aggregation. The *exprs converts the list of expressions into
# variable function arguments.
logs_df. agg ( *exprs ) . show ( )

Checking which columns have null values.

检查哪些列具有空值。

好吧,好像我们在status列中缺少一个值,而其他所有内容都在content_size列中。 让我们看看是否可以找出问题所在!

在HTTP状态下处理null

我们对状态列的原始解析正则表达式为:


   
   
regexp_extract ( 'value' , r ' \s ( \d {3}) \s ' , 1 ) . cast ( 'integer' )
                                          . alias ( 'status' )

可能还有更多的数字使我们的正则表达式错误吗? 还是数据点本身不好? 让我们找出答案。

注意 :在下面的表达式中,波浪号( ~ )表示“不是”。


   
   
null_status_df = base_df. filter ( ~ base_df [ 'value' ] . rlike ( r ' \s ( \d {3}) \s ' ) )
null_status_df. count ( )

1

让我们看看这个不良记录是什么样的:

 null_status_df.show(truncate=False) 
A bad record that's missing information.

错误的记录,缺少信息。

看起来像一条记录,其中包含很多缺少的信息。 让我们通过日志数据解析管道进行传递:


   
   
bad_status_df = null_status_df. select ( regexp_extract ( 'value' , host_pattern , 1 ) . alias ( 'host' ) ,
                                      regexp_extract ( 'value' , ts_pattern , 1 ) . alias ( 'timestamp' ) ,
                                      regexp_extract ( 'value' , method_uri_protocol_pattern , 1 ) . alias ( 'method' ) ,
                                      regexp_extract ( 'value' , method_uri_protocol_pattern , 2 ) . alias ( 'endpoint' ) ,
                                      regexp_extract ( 'value' , method_uri_protocol_pattern , 3 ) . alias ( 'protocol' ) ,
                                      regexp_extract ( 'value' , status_pattern , 1 ) . cast ( 'integer' ) . alias ( 'status' ) ,
                                      regexp_extract ( 'value' , content_size_pattern , 1 ) . cast ( 'integer' ) . alias ( 'content_size' ) )
bad_status_df. show ( truncate = False )
The full bad log record containing no information and two null entries.

完整的错误日志记录,不包含任何信息和两个空条目。

看起来记录本身是不完整的记录,没有有用的信息,最好的选择是按以下方式删除该记录:


   
   
logs_df = logs_df [ logs_df [ 'status' ] . isNotNull ( ) ]
exprs = [ count_null ( col_name ) for col_name in logs_df. columns ]
logs_df. agg ( *exprs ) . show ( )
The dropped record.

删除的记录。

处理HTTP内容大小中的null

根据先前的正则表达式,我们对content_size列的原始解析正则表达式为:


   
   
regexp_extract ( 'value' , r ' \s ( \d +)$' , 1 ) . cast ( 'integer' )
                                       . alias ( 'content_size' )

我们原始数据集中的数据可能会丢失吗? 让我们找出答案。 我们首先在基本DataFrame中找到具有潜在缺失内容大小的记录:


   
   
null_content_size_df = base_df. filter ( ~ base_df [ 'value' ] . rlike ( r ' \s \d +$' ) )
null_content_size_df. count ( )

33905

这个数字似乎与我们处理的DataFrame中缺少的内容大小值的数量匹配。 让我们看一下缺少内容大小的数据帧的前十条记录:

 null_content_size_df. take ( 10 ) 
The top 10 dataframe records with missing content sizes.

缺少内容大小的前10个数据框记录。

很明显,不良的原始数据记录与错误响应相对应,在错误响应中,没有内容被发送回,并且服务器为content_size字段发出了- 。 由于我们不想从分析中丢弃这些行,因此我们将其估算或填充为0。

使用空content_size修复行

最简单的解决方案是像我们前面讨论的那样,用0替换logs_df的空值。 Spark DataFrame API提供了一组专门用于处理空值的函数和字段,其中包括:

  • fillna () ,用指定的非空值填充空值。
  • na ,它返回一个DataFrameNaFunctions对象,该对象具有用于对空列进行操作的许多函数。

有几种方法可以调用此功能。 最简单的方法就是用已知值替换所有空列。 但是,为了安全起见,最好传递一个包含(column_name, value)映射的Python字典。 那就是我们要做的。 文档中的一个示例如下所示:


   
   
>>> df4. na . fill ( { 'age' : 50 , 'name' : 'unknown' } ) . show ( )
+---+------+-------+
|age|height|   name|
+---+------+-------+
| 10 |     80 |  Alice|
|   5 |  null|    Bob|
| 50 |  null|    Tom|
| 50 |  null|unknown|
+---+------+-------+

现在,我们使用此函数用0填充content_size字段中的所有缺失值:


   
   
logs_df = logs_df. na . fill ( { 'content_size' : 0 } )
exprs = [ count_null ( col_name ) for col_name in logs_df. columns ]
logs_df. agg ( *exprs ) . show ( )
The null values now replaced by zero.

空值现在替换为零。

看看,没有缺失的价值!

处理时间字段(时间戳)

现在,我们有了一个干净的,已解析的DataFrame,我们必须将timestamp字段解析为实际的时间戳。 通用日志格式时间有些不标准。 用户定义函数(UDF)是解析它的最直接方法:


   
   
from pyspark. sql . functions import udf

month_map = {
  'Jan' : 1 , 'Feb' : 2 , 'Mar' : 3 , 'Apr' : 4 , 'May' : 5 , 'Jun' : 6 , 'Jul' : 7 ,
  'Aug' : 8 ,   'Sep' : 9 , 'Oct' : 10 , 'Nov' : 11 , 'Dec' : 12
}

def parse_clf_time ( text ) :
    """ Convert Common Log time format into a Python datetime object
    Args:
        text (str): date and time in Apache time format [dd/mmm/yyyy:hh:mm:ss (+/-)zzzz]
    Returns:
        a string suitable for passing to CAST('timestamp')
    """

    # NOTE: We're ignoring the time zones here, might need to be handled depending on the problem you are solving
    return "{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}" . format (
      int ( text [ 7 : 11 ] ) ,
      month_map [ text [ 3 : 6 ] ] ,
      int ( text [ 0 : 2 ] ) ,
      int ( text [ 12 : 14 ] ) ,
      int ( text [ 15 : 17 ] ) ,
      int ( text [ 18 : 20 ] )
    )

现在让我们使用此函数来解析我们的DataFrame的time列:


   
   
udf_parse_time = udf ( parse_clf_time )

logs_df = ( logs_df. select ( '*' , udf_parse_time ( logs_df [ 'timestamp' ] )
                                  . cast ( 'timestamp' )
                                  . alias ( 'time' ) )
                  . drop ( 'timestamp' )
logs_df. show ( 10 , truncate = True )
The timestamp parsed with a User-Defined Function (UDF).

使用用户定义的函数(UDF)解析的时间戳。

情况看起来不错! 让我们通过检查DataFrame的架构来验证这一点:

 logs_df. printSchema ( ) 

root
|-- host: string (nullable = true)
|-- method: string (nullable = true)
|-- endpoint: string (nullable = true)
|-- protocol: string (nullable = true)
|-- status: integer (nullable = true)
|-- content_size: integer (nullable = false)

|-- time: timestamp (nullable = true)

现在让我们缓存logs_df因为我们将在本系列的第二部分中将其广泛用于数据分析部分。

 logs_df. cache ( ) 

结论

在任何端到端数据科学或Analytics用例中,获取,处理和整理数据都是最重要的步骤。 大规模处理半结构化或非结构化数据时,事情开始变得越来越困难。 本案例研究为您提供了逐步实践的方法,以利用Python和Spark等开源工具和框架的功能来大规模处理和处理半结构化NASA日志数据。 准备好干净的数据集后,我们最终可以开始使用它来获取有关NASA服务器的有用见解。 单击本系列的第二篇文章,获得有关使用Python和Apache Spark分析和可视化NASA日志数据的动手教程


这篇文章最初出现在Medium的Towards Data Science频道上,经许可重新发布。


翻译自: https://opensource.com/article/19/5/log-data-apache-spark

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值