收款定制开发大数据系列 | 全国职业院校技能大赛大数据应用技术赛项笔记分享-离线抽取模块

收款定制开发离线数据抽取

写在前面:
收款定制开发此笔记是本人在备战2022收款定制开发年赛项整理出来的,收款定制开发不涉及国赛涉密内容,收款定制开发如点赞收藏理想,收款定制开发我将会把所有模块的笔收款定制开发记开源分享出来,收款定制开发如有想询问国赛经验的收款定制开发可以关注私聊我,收款定制开发我会一一回复。

1. Scala

Scala简介

收款定制开发是一门满足现代软件工收款定制开发程师需求的语言;收款定制开发它是一门静态类型语言,收款定制开发支持混合范式;收款定制开发它也是一门运行在 JVM 收款定制开发之上的语言,语法简洁、优雅、灵活。Scala 收款定制开发拥有一套复杂的类型系统,Scala收款定制开发方言既能用于编写简短收款定制开发的解释脚本,收款定制开发也能用于构建大型复杂系统。

Scala基础

1. 数据类型

2. 收款定制开发变量和常量的声明

  • 收款定制开发定义变量或者常量的时候,收款定制开发也可以写上返回的类型,一般省略,如:val a:Int = 10
  • 收款定制开发常量不可再赋值
  /**        * 收款定制开发定义变量和常量        * 变量 :用 var 定义 ,可修改         * 常量 :用 val 定义,不可修改        */       var name = "zhangsan"       println(name)       name ="lisi"       println(name)       val gender = "m"   //    gender =   "m"//错误,收款定制开发不能给常量再赋值   注意:scala收款定制开发有个原则就是极简原则,不用写的东西一概不写。   定义变量有两种形式   一种是像上面那样用val修饰另一种是var进行修饰   val 定义的变量不可变相当与java中的final   用表达式进行赋值   Val x=1   Val y=if(1>0) 1 else -1   混和表达式   Val a =if (x>0) 1 else “jay”   需要注意的是any是所有的父类,相当于java里的object   else缺失的表达式   val   p=if (x>5) 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

3. 类和对象

创建类

   class Person{     val name   = "zhangsan"     val age = 18     def sayName() = {       "my name is "+ name     }   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

创建对象

   object Lesson_Class {      def main(args: Array[String]):   Unit = {       val person = new Person()       println(person.age);       println(person.sayName())     }   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

apply方法

/**  * object 单例对象中不可以传参,  * 如果在创建Object时传入参数,那么会自动根据参数的个数去Object中寻找相应的apply方法  */   object Lesson_ObjectWithParam {  def apply(s:String) = {    println("name is "+s)  }  def apply(s:String,age:Int) = {    println("name is "+s+",age = "+age)  }  def main(args: Array[String]): Unit = {    Lesson_ObjectWithParam("zhangsang")    Lesson_ObjectWithParam("lisi",18)  } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

伴生类和伴生对象

class Person(xname :String , xage :Int){     var name = Person.name     val age = xage     var gender = "m"     def this(name:String,age:Int,g:String){       this(name,age)       gender = g     }          def sayName() = {       "my name is "+ name     }      }      object Person {     val name = "zhangsanfeng"          def main(args: Array[String]):   Unit = {       val person = new Person("wagnwu",10,"f")       println(person.age);       println(person.sayName())       println(person.gender)     }   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

注意点:

  • 建议类名首字母大写 ,方法首字母小写,类和方法命名建议符合驼峰命名法。
  • scala 中的object是单例对象,相当于java中的工具类,可以看成是定义静态的方法的类。object不可以传参数。另:Trait不可以传参数
  • scala中的class类默认可以传参数,默认的传参数就是默认的构造函数。

重写构造函数的时候,必须要调用默认的构造函数。

  • class 类属性自带getter ,setter方法。
  • 使用object时,不用new,使用class时要new ,并且new的时候,class中除了方法不执行(不包括构造),其他都执行。
  • 如果在同一个文件中,object对象和class类的名称相同,则这个对象就是这个类的伴生对象,这个类就是这个对象的伴生类。可以互相访问私有变量。

4. This

https://blog.csdn.net/qq_39521554/article/details/81045826?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

5. if else

​ /** * if else */ val age =18 if (age < 18 ){ println(“no allow”) }else if (18<=age&&age<=20){ println(“allow with other”) }else{ println(“allow self”) }

6. for ,while,do…while

to和until 的用法(不带步长,带步长区别)

/**        * to和until        * 例:        * 1 to 10 返回1到10的Range数组,包含10        * 1 until 10 返回1到10 Range数组 ,不包含10        */              println(1 to 10 )//打印 1, 2, 3, 4, 5, 6, 7, 8, 9, 10       println(1.to(10))//与上面等价,打印 1, 2, 3, 4, 5, 6, 7, 8, 9, 10              println(1 to (10 ,2))//步长为2,从1开始打印 ,1,3,5,7,9       println(1.to(10, 2))               println(1 until 10 ) //不包含最后一个数,打印 1,2,3,4,5,6,7,8,9       println(1.until(10))//与上面等价          println(1 until (10 ,3 ))//步长为2,从1开始打印,打印1,4,7      在scala中,Range代表的是一段整数的范围,官方有关range的api:   http://www.scala-lang.org/api/current/index.html#scala.collection.immutable.Range      这些底层其实都是Range,Range(1,10,2)1是初始值,10是条件,2是步长,步长也可以为负值,递减。   until和Range是左闭右开,1是包含的,10是不包含。而to是左右都包含。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

for循环

      /**        * for 循环        *         */       for( i <- 1 to 10 ){         println(i)   }         //for循环数组   val arr=Array(“a”,”b”,”c”)   for(i<-arr)   println(i)         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

创建多层for循环(高级for循环)

       //可以分号隔开,写入多个list赋值的变量,构成多层for循环       //scala中 不能写count++   count-- 只能写count+       var count = 0;       for(i <- 1 to 10; j <- 1 until 10){         println("i="+ i +", j="+j)         count += 1       }       println(count);              //例子: 打印小九九       for(i <- 1 until 10 ;j <- 1 until 10){         if(i>=j){          print(i +" *   " + j + " = "+ i*j+"    ")                    }         if(i==j ){           println()         }                }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  1. for循环中可以加条件判断,可以使用分号隔开,也可以不使用分号(使用空格)

           //可以在for循环中加入条件判断       for(i<- 1 to 10 ;if (i%2) == 0 ;if (i == 4) ){         println(i)   }
    • 1
    • 2
    • 3
    • 4
  2. scala中不能使用count++,count—只能使用count = count+1 ,count += 1

  3. for循环用yield 关键字返回一个集合(把满足条件的i组成一个集合)

    val result = for(i <- 1 to 100 if(i>50) if(i%2==0)) yield i

    println(result)

  4. while循环,while(){},do {}while()

             //将for中的符合条件的元素通过yield关键字返回成一个集合       val list = for(i <- 1 to 10  ; if(i > 5 )) yield i        for( w <- list ){         println(w)   }         /**        * while 循环        */       var index = 0        while(index < 100 ){        println("第"+index+"次while 循环")         index += 1        }       index = 0        do{        index +=1         println("第"+index+"次do while 循环")   }while(index <100 )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

加深练习

需求说明:定义一个数组val a1=Array(1,2,3,4,5,6,7,8,9)把其中的偶数取出。

   def main(args: Array[String]): Unit = {     var a1=Array.range(1,10)     for(i<-a1 if(i%2==0)) {       println(i)     }   }   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

7. 懒加载

Val lazyVal={println(“I am too lazy”);1}

lazy val lazyVal={println(“I am too lazy”);1}

8. Scala方法与函数

Scala方法的定义

有参方法

无参方法

   def fun (a: Int , b: Int ) :   Unit = {      println(a+b)    }   fun(1,1)          def fun1 (a : Int , b : Int)= a+b       println(fun1(1,2))  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意点:

  • 方法定义语法 用def来定义
  • 可以定义传入的参数,要指定传入参数的类型
  • 方法可以写返回值的类型也可以不写,会自动推断,有时候不能省略,必须写,比如在递归方法中或者方法的返回值是函数类型的时候。
  • scala中方法有返回值时,可以写return,也可以不写return,会把方法中最后一行当做结果返回。当写return时,必须要写方法的返回值。
  • 如果返回值可以一行搞定,可以将{}省略不写
  • 传递给方法的参数可以在方法中使用,并且scala规定方法的传过来的参数为val的,不是var的。
  • 如果去掉方法体前面的等号,那么这个方法返回类型必定是Unit的。这种说法无论方法体里面什么逻辑都成立,scala可以把任意类型转换为Unit.假设,里面的逻辑最后返回了一个string,那么这个返回值会被转换成Unit,并且值会被丢弃。

方法与函数

定义一个方法:

def method(a:Int,b:Int) =a*b val a =2

method(3,5)

定义一个函数:

Val f1=(x:Int,y:Int)=>x+y

f1 (1,2)

匿名函数

(x:Int,y:Int)=>x+y

在函数式编程语言中,函数是“头等公民”,它可以像任何其他数据类型一样被传递和操作,函数可以在方法中传递。

递归方法

/**        * 递归方法         * 5的阶乘        */       def fun2(num :Int) :Int=   {         if(num ==1)           num         else            num * fun2(num-1)       }       print(fun2(5))   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

参数有默认值的方法

  • 默认值的函数中,如果传入的参数个数与函数定义相同,则传入的数值会覆盖默认值。
  • 如果不想覆盖默认值,传入的参数个数小于定义的函数的参数,则需要指定参数名称。
  /**        * 包含默认参数值的函数        * 注意:        * 1.默认值的函数中,如果传入的参数个数与函数定义相同,则传入的数值会覆盖默认值        * 2.如果不想覆盖默认值,传入的参数个数小于定义的函数的参数,则需要指定参数名称        */       def fun3(a :Int = 10,b:Int) = {         println(a+b)       }       fun3(b=2)   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

可变参数的方法

  • 多个参数用逗号分开
   /**        * 可变参数个数的函数        * 注意:多个参数逗号分开        */       def fun4(elements   :Int*)={         var sum = 0;         for(elem <- elements){           sum += elem         }         sum       }       println(fun4(1,2,3,4))   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

匿名函数

  1. 有参匿名函数
  2. 无参匿名函数
  3. 有返回值的匿名函数
  • 可以将匿名函数返回给val定义的值
  /**        * 匿名函数        * 1.有参数匿名函数        * 2.无参数匿名函数        * 3.有返回值的匿名函数        * 注意:        * 可以将匿名函数返回给定义的一个变量        */       //有参数匿名函数       val value1: (Int)=>Unit = (a : Int) => {         println(a)       }       value1(1)       //无参数匿名函数       val value2 = ()=>{         println("我爱学习")       }       value2()       //有返回值的匿名函数       val value3 = (a:Int,b:Int) =>{         a+b       }       println(value3(4,4))    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

嵌套方法

    /**        * 嵌套方法        * 例如:嵌套方法求5的阶乘        */       def fun5(num:Int)={         def fun6(a:Int,b:Int):Int={           if(a == 1){             b           }else{             fun6(a-1,a*b)           }         }         fun6(num,1)       }       println(fun5(5))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

偏应用函数

偏应用函数是一种表达式,不需要提供函数需要的所有参数,只需要提供部分,或不提供所需参数。

 /**        * 偏应用函数        */       def log(date :Date, s :String)= {         println("date is "+ date +",log is "+ s)       }              val date = new Date()       log(date ,"log1")       log(date ,"log2")       log(date ,"log3")              //想要调用log,以上变化的是第二个参数,可以用偏应用函数处理       val logWithDate = log(date,_:String)       logWithDate("log11")       logWithDate("log22")       logWithDate("log33")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

高阶函数

函数的参数是函数,或者函数的返回类型是函数,或者函数的参数和函数的返回类型是函数的函数。

  • 函数的参数是函数
  • 函数的返回是函数
  • 函数的参数和函数的返回是函数
   /**        * 高阶函数        * 函数的参数是函数     或者函数的返回是函数        或者函数的参数和返回都是函数        */       //函数的参数是函数       def hightFun(f : (Int,Int)   =>Int, a:Int ) : Int = {         f(a,100)       }       def f(v1 :Int,v2:   Int):Int  = {         v1+v2       }       println(hightFun(f, 1))       //函数的返回是函数       //1,2,3,4相加       def hightFun2(a : Int,b:Int) :   (Int,Int)=>Int = {         def f2 (v1: Int,v2:Int) :Int =   {           v1+v2+a+b         }         f2       }       println(hightFun2(1,2)(3,4))       //函数的参数是函数,函数的返回是函数       def hightFun3(f : (Int ,Int)   => Int) : (Int,Int) => Int = {         f       }        println(hightFun3(f)(100,200))       println(hightFun3((a,b) =>{a+b})(200,200))       //以上这句话还可以写成这样       //如果函数的参数在方法体中只使用了一次 那么可以写成_表示       println(hightFun3(_+_)(200,200))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

柯里化函数

  • 高阶函数的简化
  • 定义
  • 柯里化(Currying)指的是把原来接受多个参数的函数变换成接受一个参数的函数过程,并且返回接受余下的参数且返回结果为一个新函数的技术。
 scala柯里化风格的使用可以简化主函数的复杂度,提高主函数的自闭性,提高功能上的可扩张性、灵活性。可以编写出更加抽象,功能化和高效的函数式代码。   //柯理化   object KLH {     def main(args:   Array[String]): Unit = {       def   klh(x:Int)(y:Int) =x*y       val res=klh(3)(_)      println(res(4))     }   }   /**        * 柯里化函数        */       def fun7(a :Int,b:Int)(c:Int,d:Int) = {         a+b+c+d       }       println(fun7(1,2)(3,4))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

2. Spark

spark简介

Spark 是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。

在YARN上运行Spark

配置

大部分为

Spark on YARN
  • 1

模式提供的配置与其它部署模式提供的配置相同。下面这些是为

Spark on YARN
  • 1

模式提供的配置。

Spark属性

Property NameDefaultMeaning
spark.yarn.applicationMaster.waitTries10ApplicationMaster等待Spark master的次数以及SparkContext初始化尝试的次数
spark.yarn.submit.file.replicationHDFS默认的复制次数(3)上传到HDFS的文件的HDFS复制水平。这些文件包括Spark jar、app jar以及任何分布式缓存文件/档案
spark.yarn.preserve.staging.filesfalse设置为true,则在作业结束时保留阶段性文件(Spark jar、app jar以及任何分布式缓存文件)而不是删除它们
spark.yarn.scheduler.heartbeat.interval-ms5000Spark application master给YARN ResourceManager发送心跳的时间间隔(ms)
spark.yarn.max.executor.failuresnumExecutors * 2,最小为3失败应用程序之前最大的执行失败数
spark.yarn.historyServer.address(none)Spark历史服务器(如host.com:18080)的地址。这个地址不应该包含一个模式( UI到Spark历史服务器UI的连接时,这个地址从YARN ResourceManager得到
spark.yarn.dist.archives(none)提取逗号分隔的档案列表到每个执行器的工作目录
spark.yarn.dist.files(none)放置逗号分隔的文件列表到每个执行器的工作目录
spark.yarn.executor.memoryOverheadexecutorMemory * 0.07,最小384分配给每个执行器的堆内存大小(以MB为单位)。它是VM开销、interned字符串或者其它本地开销占用的内存。这往往随着执行器大小而增长。(典型情况下是6%-10%)
spark.yarn.driver.memoryOverheaddriverMemory * 0.07,最小384分配给每个driver的堆内存大小(以MB为单位)。它是VM开销、interned字符串或者其它本地开销占用的内存。这往往随着执行器大小而增长。(典型情况下是6%-10%)
spark.yarn.queuedefault应用程序被提交到的YARN队列的名称
spark.yarn.jar(none)Spark jar文件的位置,覆盖默认的位置。默认情况下,Spark on YARN将会用到本地安装的Spark jar。但是Spark jar也可以HDFS中的一个公共位置。这允许YARN缓存它到节点上,而不用在每次运行应用程序时都需要分配。指向HDFS中的jar包,可以这个参数为"hdfs:///some/path"
spark.yarn.access.namenodes(none)你的Spark应用程序访问的HDFS namenode列表。例如,spark.yarn.access.namenodes=hdfs://nn1.com:8032,hdfs://nn2.com:8032,Spark应用程序必须访问namenode列表,Kerberos必须正确配置来访问它们。Spark获得namenode的安全令牌,这样Spark应用程序就能够访问这些远程的HDFS集群。
spark.yarn.containerLauncherMaxThreads25为了启动执行者容器,应用程序master用到的最大线程数
spark.yarn.appMasterEnv.[EnvironmentVariableName](none)添加通过EnvironmentVariableName指定的环境变量到Application Master处理YARN上的启动。用户可以指定多个该设置,从而设置多个环境变量。在yarn-cluster模式下,这控制Spark driver的环境。在yarn-client模式下,这仅仅控制执行器启动者的环境。

在YARN上启动Spark

确保

HADOOP_CONF_DIR
  • 1

YARN_CONF_DIR
  • 1

指向的目录包含Hadoop集群的(客户端)配置文件。这些配置用于写数据到dfs和连接到YARN ResourceManager。

有两种部署模式可以用来在YARN上启动Spark应用程序。在yarn-cluster模式下,Spark driver运行在application master进程中,这个进程被集群中的YARN所管理,客户端会在初始化应用程序之后关闭。在yarn-client模式下,driver运行在客户端进程中,application master仅仅用来向YARN请求资源。

和Spark单独模式以及Mesos模式不同,在这些模式中,master的地址由"master"参数指定,而在YARN模式下,ResourceManager的地址从Hadoop配置得到。因此master参数是简单的

yarn-client
  • 1

yarn-cluster
  • 1

在yarn-cluster模式下启动Spark应用程序。

./bin/spark-submit --class path.to.your.Class --master yarn-cluster [options] <app jar> [app options]
  • 1

例子:

$ ./bin/spark-submit --class org.apache.spark.examples.SparkPi \    --master yarn-cluster \    --num-executors 3 \    --driver-memory 4g \    --executor-memory 2g \    --executor-cores 1 \    --queue thequeue \    lib/spark-examples*.jar \    10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

以上启动了一个YARN客户端程序用来启动默认的 Application Master,然后SparkPi会作为Application Master的子线程运行。客户端会定期的轮询Application Master用于状态更新并将更新显示在控制台上。一旦你的应用程序运行完毕,客户端就会退出。

在yarn-client模式下启动Spark应用程序,运行下面的shell脚本

$ ./bin/spark-shell --master yarn-client 
  • 1

添加其它的jar

在yarn-cluster模式下,driver运行在不同的机器上,所以离开了保存在本地客户端的文件,

SparkContext.addJar
  • 1

将不会工作。为了使

SparkContext.addJar
  • 1

用到保存在客户端的文件,在启动命令中加上

--jars
  • 1

选项。

$ ./bin/spark-submit --class my.main.Class \    --master yarn-cluster \    --jars my-other-jar.jar,my-other-other-jar.jar    my-main-jar.jar    app_arg1 app_arg2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 注意事项

在Hadoop 2.2之前,YARN不支持容器核的资源请求。因此,当运行早期的版本时,通过命令行参数指定的核的数量无法传递给YARN。在调度决策中,核请求是否兑现取决于用哪个调度器以及如何配置调度器。

Spark executors使用的本地目录将会是YARN配置(yarn.nodemanager.local-dirs)的本地目录。如果用户指定了

spark.local.dir
  • 1

,它将被忽略。

--files
  • 1

--archives
  • 1

选项支持指定带 # 号文件名。例如,你能够指定

--files localtest.txt#appSees.txt
  • 1

,它上传你在本地命名为

localtest.txt
  • 1

的文件到HDFS,但是将会链接为名称

appSees.txt
  • 1

。当你的应用程序运行在YARN上时,你应该使用

appSees.txt
  • 1

去引用该文件。

如果你在yarn-cluster模式下运行

SparkContext.addJar
  • 1

,并且用到了本地文件,

--jars
  • 1

选项允许

SparkContext.addJar
  • 1

函数能够工作。如果你正在使用 HDFS, HTTP, HTTPS或FTP,你不需要用到该选项。

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发