Pyspark框架下调用HanLP自然语言处理开发库操作指南

古翠码翁 2022-08-21 20:41:21

声明:本文系作者原创,同文也发表在作者微信公众号,转载请注明出处。

一、前言

最近有一个项目,要对HDFS上存储的数十亿量级的文本数据进行处理,包括分词、词性标注和文本替换等。

利用计算机集群在Spark大数据计算引擎框架中处理这个任务是一个比较合适的选择。Spark本身使用Scala语言开发且使用Java虚拟机运行,而HanLP也是一个Java原生的,以中文为主的自然语言处理开发库。都是基于Java环境,Spark和HanLP协同工作是比较方便。但是,本人平日较少使用Java工具,面对需要快速交付的大数据任务,更习惯用易上手的Python处理,因而较多使用Spark框架的Python版,即Pyspark。

HanLP虽然提供了Python接口,但现有参考例子都集中在单机版本,在Spark集群环境中,还是需要将其核心的Java库广播到各个计算节点加载运行。因此,我面临的问题是Java不想用,而HanLP的Python接口又不直接支持Spark环境中调用。那能否在Pyspark的环境中直接使用Java库呢?

经过一番调研,我发现较多资料集中在介绍使用Java或者Scala做开发工具在Spark环境中调用HanLP,或者是在Python单机环境中使用HanLP的Python接口完成任务。于是经过思考和实践,我自己摸索了一个在Pyspark环境中用Python调用HanLP的Java包的方法。虽然并非原创,但鉴于类似参考资料较少,还是记录下来,一则作为备忘,二可以提供参考,方便后来者少走弯路。

二、环境准备

本文示例的工作在如下环境中完成:

  1. Hadoop集群:3.1.3

  1. Spark计算框架:2.3.1

  1. HanLP自然语言库:hanlp-portable-1.7.6.jar

三、操作指南

3.1 IoAdapter

根据HanLP在Spark集群上运行操作指南,为了让Spark各个计算节点能够正常加载放置在分布式文件系统上的字典数据文件,需要实现一个IoAdapter接口。实现这个接口后,就可以让Spark各个节点在运行HanLP库时,能够正常访问并加载放置在HDFS上面的各类数据文件,比如各种字典等。

恰巧手头有以前实现好的IoAdapter类,打包在tools.jar中。如果需要自己实现,可以参考这篇文章直接复制源代码编译打包即可:https://developer.aliyun.com/article/688264

3.2 Jar包上传到Hadoop集群

在开始工作前,需要将必要的文件上传到计算机集群。Spark系统可以直接访问HDFS文件系统,这里将必要Jar包和数据字典文件都上传到HDFS上面。

这里用到两个Jar文件包,一个是hanlp-portable-1.7.6.jar,一个是tools.jar,其中tools.jar是自己实现的jar包,里面包括了HadoopFileIoAdapter的实现,就是HanLP提供的IIoAdapter接口的实现。

配置文件hanlp.properties。这个配置文件非常重要,通过读取配置文件的信息,可以让HanLP库找到对应的IO接口类,并正常加载字典。hanlp.properties配置文件内容如下所示:


root=webhdfs://xxx.xxx.xxx.xxx:port/work
IOAdapter=tools.HadoopFileIoAdapter
CustomDictionaryPath=spark-data/nlp-data/custom_dictionary.txt

配置文件中定义了HDFS根目录包括IP地址和端口,HanLP中读取的外部输入都以此为根路径,IOAdapter指明要加载的IO适配器类型,这里面指向我们tools包里面的编写的HadoopFileIoAdapter,CustomDictionaryPath指明自定义字典路径,使用时候HanLP会自动配上根目录前缀。

将两个jar包和一个属性文件hanlp.properties一起打包压缩到一个zip里面,命名为nlp-tools.zip,这么做是为了省事,否则在Spark脚本启动时候,还要将每个jar包通过命令行参数传递,这里就传递一个zip包。

自定义字典文件名字是custom_dictionary.txt,存放了分析器需要用到的字典数据。

假设上传后,工具包路径为hdfs:///work/spark-jars/nlp-tools.zip;自定义字典路径为hdfs:///work/spark-data/nlp-data/custom_dictionary.txt

3.3 编写Pyspark代码

Pyspark的编码方法与Python一致,只不过是在Spark环境中运行。比较重要的一点是如何在Python环境中与Java虚拟机交互。这里使用了JPype库,根据介绍,JPype是一个Python模块,提供的功能可以让用户在Python环境中使用Java的所有特性。与Jython不同,JPype并不是用Java把Python的虚拟机实现了一遍,而是在Python和Java两个环境的虚拟机之间提供了一个适配接口,让二者可以正常交互。

示例代码如下:


from pyspark import SparkContext
import jpype


def word_segment(partition):
    if not jpype.isJVMStarted():
        jpype.startJVM("-Dhanlp_properties_path=./nlp-tools/hanlp.properties", convertStrings=False, classpath=['./nlp-tools/hanlp-portable-1.7.6.jar','./nlp-tools/tools.jar'])
    HanLP = jpype.JClass('com.hankcs.hanlp.HanLP')
    Collectors = jpype.JClass('java.util.stream.Collectors')
    Nature = jppype.JClass('com.hankcs.hanlp.corpus.tag.Nature')
    segment = HanLP.newSegment().enableNameRecognize(True)
    ret = list()
    for s in partition:
         segs = segment.seg(JString(s))
         term_list = segs.stream().distinct().collect(collectors.toList())
         res_list = list()
         for term in term_list:
             res_list.append(str(term.word) + ":" + str(term.nature.toString()))     
         yield res_list   

# spark main flow
sc = SparkContext(appName="HanLpExample")
sc.setLogLevel("WARN")
data_root = "hdfs:///work/spark-data/text-data"
rdd = sc.textFile(data_root)\
        .mapPartitions(word_segment)
res = rdd.takeSample(false, 1000)
for words in range res:
    print(words)          

22 - 24行,没有太多好解释,常规初始化操作,设定spark context属性等,设置日志输出级别为警告级,然后指定输入文本路径为 hdfs:///work/spark-data/text-data,这里存放了待处理的几十亿条文本信息。

25 - 26 行,通过textFile从路径中读入数据。然后使用mapPartitions进行map操作。这里一定使用mapPartitions,一个是按照HanLP官方指南操作,另一个深层原因是,使用mapPartitions,对应的方法中调用的HanLP库会在各个节点本地加载。如果使用map,这个调用的库实例会从主节点拉取,但因为这个库实例比较大,多半会报序列化错误(而且即使成功通讯代价也较大),而将库实例声明成广播变量应该也会有序列化问题(深层原因这里也不深入展开,我猜测可能因为序列化协议兼容问题)。mapPartitions中传入一个函数word_segment,这个就是在Spark各个节点中直接运行的处理函数。下面进入到word_segment中分析。

6 - 7行,这里判断节点的Java虚拟机是否启动,如果没有启动,要启动虚拟机,并加载两个jar包,这两个jar包就是3.2节打包上传到集群文件系统的压缩包,节点加载时候,需要先自行解压,然后放置路径就是nlp-tools中。其中第7行还指明了加载外部配置文件的路径,通过hanlp_properties_path=./nlp-tools/hanlp.properties环境变量定义,HanLP在启动时候会解析这里指明的属性文件,进而正确加载文件中配置的根路径、IO接口类和自定义词典。

8 - 10行,加载需要的java类,通过jpype.JClass类,将java类直接加载进来在python环境中使用。其中HanLP是HanLP自然语言处理库的主类,Nature是词性类。

11行,声明分词器且进行实体识别。

13 - 19 行,迭代遍历数据分区,每一个s变量存放的是一个中文文本句子。第14行将Python字符串转换成Java字符串然后进行分词处理。第15行将处理结果放入一个列表。第17 - 18行,迭代遍历处理结果,将分词后的单词和词性组合起来并返回。这就是整个分词处理流程。

3.4 任务启动脚本

完成所有环境、数据准备事项并且编码结束后,下一步在集群上启动脚本运行。运行前,再次检查逐项工作是备妥:

  1. 必需的jar包和属性配置文件已经打包并上传到HDFS的对应路径中;

  2. 自定义字典已经上传到HFDS对应路径中;

  1. pyspark脚本开发完毕,且无语法错误,这里脚本命名为segment_word.py

运行以下命令:

 spark-submit --master yarn --deploy-mode client --num-executors 4 --executor-cores 4  --executor-memory 8G  --conf spark.yarn.dist.archives=hdfs:///work/spark-jars/nlp-tools.zip#nlp-tools  segment_word.py

简单解释一下用到的各项参数

--master yarn  使用yarn作为调度器调度集群资源

--deploy-mode client 使用client模式部署任务,spark任务部署一般有cluster和client两种模式。client模式下,spark任务的drive节点和master节点在同一台机器,而cluster模式下,drive节点和master节点不是同一台机器,此时,如果运行时用到的某些资源在master节点,需要提前部署到各个工作节点。client模式要求master节点和工作节点在一个局域网内,方便资源传递,使用相对简单。

--num-executors --executor-cores 和 --executor-memory 都是指定计算资源,根据任务需求和资源实际状况调整。

--conf spark.yarn.dist.archives=hdfs:///work/spark-jars/nlp-tools.zip#nlp-tools 这个参数比较重要,是告诉spark集群,有一个压缩资源包位于hdfs的文件系统中,运行时需要各个节点读取这个压缩包并解压。我们的工作任务用到的jar包,配置文件等内容都打包在这个压缩包中,工作节点会自动找到。后面跟随#nlp-tools 是指明解压后目录名,默认是放置在在工作节点当前的工作目录下。

四、结语

本文介绍了一种在Pyspark环境下运行HanLP自然语言处理库的方法,需要指出的是,文中给出的代码示例是示意代码,为了便于介绍,在实际工作代码基础上进行了简化和改造,省略了一些细节,不能保证直接复制到其他环境就可以运行。如果直接将本文给出代码作为应用例子,可能会碰到一些技术问题。

在Pyspark框架下成功运行HanLP,关键点不在于Python和Java的交互,因为Python和Java互相调用已经有比较成熟的解决方案。这里解决问题的重点还是能够一定程度上理解Spark的工作机制,并根据这个工作机制做好资源的配置和参数设置(运行库文件的设置、加载、保证节点能否正确访问到需要的资源等)。

由于成文仓促,本文可能有很多表述不够准确,甚至错误地方,欢迎指正。

【参考文档】

  1. hanLP 参考文档:https://javadoc.io/doc/com.hankcs/hanlp/portable-1.7.6/index.html

  2. 2.jpype参考文档:https://jpype.readthedocs.io/en/latest/

  3. spark参考文档:https://spark.apache.org/docs/2.3.1/

...全文
431 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

46

社区成员

发帖
与我相关
我的任务
社区描述
这里是CSDN讨论Web产品技术和产业发展的大本营, 欢迎所有Web3业内和关心Web3的朋友们.
社区管理员
  • Web3天空之城
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧