DataStream编程模型之数据源、数据转换、数据输出

news2024/11/20 7:52:28

Flink之DataStream数据源、数据转换、数据输出(scala)

0.前言–数据源

在进行数据转换之前,需要进行数据读取。
数据读取分为4大部分:

(1)内置数据源;

又分为文件数据源;在这里插入图片描述
socket数据源;
在这里插入图片描述

集合数据源三类
在这里插入图片描述

(2)Kafka数据源

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
第二个参数用到的SimpleStringSchema对象是一个内置的DeserializationSchema对象,可以把字节数据反序列化程一个String对象。
另外,FlinkKafkaConsumer开始读取Kafka消息时,可以配置他的 读 起始位置,有如下四种。
在这里插入图片描述

import java.util.Properties
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.windowing.time.Time
object KafkaWordCount {
  def main(args: Array[String]): Unit = {
 
    val kafkaProps = new Properties()
    //Kafka的一些属性
    kafkaProps.setProperty("bootstrap.servers", "localhost:9092")
    //所在的消费组
    kafkaProps.setProperty("group.id", "group1")
    
    //获取当前的执行环境
    val evn = StreamExecutionEnvironment.getExecutionEnvironment
//创建Kafka的消费者,wordsendertest是要消费的Topic
    val kafkaSource = new FlinkKafkaConsumer[String]("wordsendertest",new SimpleStringSchema,kafkaProps)
    //设置从最新的offset开始消费
    kafkaSource.setStartFromLatest()
    //自动提交offset
kafkaSource.setCommitOffsetsOnCheckpoints(true)
    //绑定数据源
    val stream = evn.addSource(kafkaSource)
 
    //设置转换操作逻辑
    val text = stream.flatMap{ _.toLowerCase().split("\\W+")filter{ _.nonEmpty} }
      .map{(_,1)}
      .keyBy(0)
      .timeWindow(Time.seconds(5))
      .sum(1)
 
      //打印输出
      text.print()
 
      //程序触发执行
      evn.execute("Kafka Word Count")
  }
}

(3)HDFS数据源

在这里插入图片描述

(4)自定义数据源

在这里插入图片描述
一个例子:

import java.util.Calendar
import org.apache.flink.streaming.api.functions.source.RichSourceFunction
import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import scala.util.Random
 
case class StockPrice(stockId:String,timeStamp:Long,price:Double)
object StockPriceStreaming {
  def main(args: Array[String]) { 
    //设置执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
 
//设置程序并行度    
env.setParallelism(1)    
//股票价格数据流
    val stockPriceStream: DataStream[StockPrice] = env
      //该数据流由StockPriceSource类随机生成
      .addSource(new StockPriceSource)
 
    //打印结果
    stockPriceStream.print()
 
    //程序触发执行
    env.execute("stock price streaming")
  }
 class StockPriceSource extends RichSourceFunction[StockPrice]{ 
    var isRunning: Boolean = true
    val rand = new Random()
    //初始化股票价格
    var priceList: List[Double] = List(10.0d, 20.0d, 30.0d, 40.0d, 50.0d)
    var stockId = 0
    var curPrice = 0.0d
override def run(srcCtx: SourceContext[StockPrice]): Unit = {
      while (isRunning) {
        //每次从列表中随机选择一只股票
        stockId = rand.nextInt(priceList.size)
        val curPrice =  priceList(stockId) + rand.nextGaussian() * 0.05
        priceList = priceList.updated(stockId, curPrice)
        val curTime = Calendar.getInstance.getTimeInMillis
        //将数据源收集写入SourceContext
        srcCtx.collect(StockPrice("stock_" + stockId.toString, curTime, curPrice))
        Thread.sleep(rand.nextInt(10))
      }
} 
    override def cancel(): Unit = {
      isRunning = false
    }
  }
}

1.数据转换之map操作

1.数据转换算子的四种类型
基于单条记录:fliter、map
基于窗口:window
合并多条数据流:union,join,connect
拆分多条数据流:split

2.map(func)操作将一个DataStream中的每个元素传递到函数func中,并将结果返回为一个新的DataStream。输出的数据流DataStream[OUT]类型可能和输入的数据流DataStream[IN]不同
理解:一 一对应的关系,一个x得到一个y

val dataStream = env.fromElements(1,2,3,4,5)
val mapStream = dataStream.map(x=>x+10)

在这里插入图片描述
3.演示代码

import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
 
case class StockPrice(stockId:String,timeStamp:Long,price:Double) 
object MapFunctionTest {
  def main(args: Array[String]): Unit = {
 
    //设定执行环境
   val env = StreamExecutionEnvironment.getExecutionEnvironment
 
    //设定程序并行度
   env.setParallelism(1)

   //创建数据源
   val dataStream: DataStream[Int] = env.fromElements(1, 2, 3, 4, 5, 6, 7)
 
    //设置转换操作逻辑
    val richFunctionDataStream = dataStream.map {new MyMapFunction()}

 //打印输出
    richFunctionDataStream.print()
 
//程序触发执行
    env.execute("MapFunctionTest")
  }
 
  //自定义函数,继承RichMapFunction
  class MyMapFunction extends RichMapFunction[Int, String] {
    override def map(input: Int): String =
      ("Input : " + input.toString + ", Output : " + (input * 3).toString)
  }
}

2.数据转换之flatMap操作

1.flatMap和map相似,每个输入元素都可以映射到0或多个输出结果。

val dataStream = env.fromElements("Hadoop is good","Flink is fast","Flink is better")
val flatMapStream = dataStream.flatMap(line => line.split(" "))

在这里插入图片描述
可以理解为flatMap比map多了flat操作。如图。map是将输入数据映射成数组,flat是将数据拍扁,成为一个个元素。把元素映射成了多个。

2.代码演示

import org.apache.flink.api.common.functions.FlatMapFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.util.Collector
 
case class StockPrice(stockId:String,timeStamp:Long,price:Double) 
object FlatMapFunctionTest {
  def main(args: Array[String]): Unit = {
 
    //设定执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
 
    //设定程序并行度
env.setParallelism(1)
//设置数据源
val dataStream: DataStream[String] = 
      env.fromElements("Hello Spark", "Flink is excellent“) 
    //针对数据集的转换操作逻辑
val result = dataStream.flatMap(new WordSplitFlatMap(15)) 
    //打印输出
result.print() 
//程序触发执行
    env.execute("FlatMapFunctionTest")
  } 
  //使用FlatMapFunction实现过滤逻辑,只对字符串长度大于threshold的内容进行切词
  class WordSplitFlatMap(threshold: Int) extends FlatMapFunction[String, String] {
    override def flatMap(value: String, out: Collector[String]): Unit = {
      if (value.size > threshold) {
        value.split(" ").foreach(out.collect)
      }
    }
  }
}

预计输出:

Flink
is
excellent

这里只对字符长度超过15的做切割。threshold是阈值,少于15的不做切割。

3.数据转换之filter和keyBy操作

1.filter(func)操作会筛选出满足函数func的元素,并返回一个新的数据集
2.代码举例

val dataStream = env.fromElements("Hadoop is good","Flink is fast","Flink is better")
val filterStream = dataStream.filter(line => line.contains("Flink"))

如图所示
在这里插入图片描述

3.keyBy(注意方法里k小写B大写):将相同Key的数据放置在相同的分区中。
keyBy算子根据元素的形状对数据进行分组,相同形状的元素被分到了一起,可被后续算子统一处理

比如在词频统计时:

				hello flink 
				hello hadoop
				hello zhangsan

这里 词频(hello,1),(hello,1),(hello,1)统计出来之后,通过keyBy,就可以聚合,放在了相同的分区里进行统一计算。

在这里插入图片描述
通过聚合函数后又可以吧KeyedStream转换成DataStream。

4.在使用keyBy算子时,需要向keyBy算子传递一个参数, 可使用数字位置来指定Key
比如刚才词频统计时,keyBy(0)就是hello这个单词。

val dataStream: DataStream[(Int, Double)] =
    env.fromElements((1, 2.0), (2, 1.7), (1, 4.9), (3, 8.5), (3, 11.2))
//使用数字位置定义Key 按照第一个字段进行分组
val keyedStream = dataStream.keyBy(0)

这里keyby 是第一个字段1或者2或者3分组(分类)。

5.keyBy代码举例:

import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
 
//声明一个样例类,包含三个字段:股票ID、交易时间、交易价格
case class StockPrice(stockId:String,timeStamp:Long,price:Double)
 
object KeyByTest{
  def main(args: Array[String]): Unit = {
 
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
 
    //设置程序并行度
    env.setParallelism(1)
//创建数据源
    val stockList = List(
      StockPrice("stock_4",1602031562148L,43.4D),
      StockPrice("stock_1",1602031562148L,22.9D),
      StockPrice("stock_0",1602031562153L,8.2D),
      StockPrice("stock_3",1602031562153L,42.1D),
      StockPrice("stock_2",1602031562153L,29.2D),
      StockPrice("stock_0",1602031562159L,8.1D),
      StockPrice("stock_4",1602031562159L,43.7D),
      StockPrice("stock_4",1602031562169L,43.5D)
    )
    val dataStream = env.fromCollection(stockList) 
    //设定转换操作逻辑
    val keyedStream = dataStream.keyBy("stockId“) 
    //打印输出
    keyedStream.print() 
    //程序触发执行
    env.execute("KeyByTest")
  }
}

在这里插入图片描述
这里看起来没什么变换 ,因为没进行聚合操作,所以什么变化都没有,原样输出。
我加上聚合函数,看起来就有变化了。

//简写上面的代码 加上聚合函数
    val keyedStream = dataStream.keyBy("stockId")
    val aggre = keyedStream.sum(2) //这里相加的是价格price(第三个字段)

   // keyedStream.print()
    aggre.print()//聚合后打印

结果
在这里插入图片描述
对比上面哪里变化了呢?
stcok_id顺序,4-1-0-3-2-0(这里之前也有0,就会加上之前的0,变为16.299,后面的4也在累加前面的price了

4.数据转换之reduce操作和聚合操作

1.reduce:reduce算子将输入的KeyedStream通过传入的用户自定义函数滚动地进行数据聚合处理,处理以后得到一个新的DataStream,如下实例

import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
 
//声明一个样例类,包含三个字段:股票ID、交易时间、交易价格
case class StockPrice(stockId:String,timeStamp:Long,price:Double)
 
object ReduceTest{
  def main(args: Array[String]): Unit = {
 
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
 
    //设置程序并行度
    env.setParallelism(1)
    //创建数据源
    val stockList = List(
      StockPrice("stock_4",1602031562148L,43.4D),
      StockPrice("stock_1",1602031562148L,22.9D),
      StockPrice("stock_0",1602031562153L,8.2D),
      StockPrice("stock_3",1602031562153L,42.1D),
      StockPrice("stock_2",1602031562153L,29.2D),
      StockPrice("stock_0",1602031562159L,8.1D),
      StockPrice("stock_4",1602031562159L,43.7D),
      StockPrice("stock_4",1602031562169L,43.5D)
    )
    val dataStream = env.fromCollection(stockList)

    //设定转换操作逻辑
    val keyedStream = dataStream.keyBy("stockId")
    val reduceStream = keyedStream
      .reduce((t1,t2)=>StockPrice(t1.stockId,t1.timeStamp,t1.price+t2.price))
 
    //打印输出
    reduceStream.print()
 
    //程序触发执行
    env.execute("ReduceTest")
  }
}

reduce结果和上面的一样,就是累加
在这里插入图片描述

2.flink也支持自定义的reduce函数

import org.apache.flink.api.common.functions.ReduceFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
 
//声明一个样例类,包含三个字段:股票ID,交易时间,交易价格
case class StockPrice(stockId:String,timeStamp:Long,price:Double)
 
object MyReduceFunctionTest{
  def main(args: Array[String]): Unit = {
 
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
 
    //设置程序并行度
    env.setParallelism(1)

    //创建数据源
    val stockList = List(
      StockPrice("stock_4",1602031562148L,43.4D),
      StockPrice("stock_1",1602031562148L,22.9D),
      StockPrice("stock_0",1602031562153L,8.2D),
      StockPrice("stock_3",1602031562153L,42.1D),
      StockPrice("stock_2",1602031562153L,29.2D),
      StockPrice("stock_0",1602031562159L,8.1D),
      StockPrice("stock_4",1602031562159L,43.7D),
      StockPrice("stock_4",1602031562169L,43.5D)
    )
    val dataStream = env.fromCollection(stockList) 


//设定转换操作逻辑
    val keyedStream = dataStream.keyBy("stockId")
    val reduceStream = keyedStream.reduce(new MyReduceFunction)
 
    //打印输出
    reduceStream.print()
 
    //程序触发执行
    env.execute("MyReduceFunctionTest")
  }
  class MyReduceFunction extends ReduceFunction[StockPrice] {
    override def reduce(t1: StockPrice,t2:StockPrice):StockPrice = {
      StockPrice(t1.stockId,t1.timeStamp,t1.price+t2.price)
    }
  }
}

主要不同的就是创建了MyReduceFunction ().
3.聚合算子
在这里插入图片描述
和excel一样。
代码举例:

import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
 
//声明一个样例类,包含三个字段:股票ID、交易时间、交易价格
case class StockPrice(stockId:String,timeStamp:Long,price:Double) 
object AggregationTest{
  def main(args: Array[String]): Unit = {
 
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
 
    //设置程序并行度
    env.setParallelism(1)    
//创建数据源
    val stockList = List(
      StockPrice("stock_4",1602031562148L,43.4D),
      StockPrice("stock_1",1602031562148L,22.9D),
      StockPrice("stock_0",1602031562153L,8.2D),
      StockPrice("stock_3",1602031562153L,42.1D),
      StockPrice("stock_2",1602031562153L,29.2D),
      StockPrice("stock_0",1602031562159L,8.1D),
      StockPrice("stock_4",1602031562159L,43.7D),
      StockPrice("stock_4",1602031562169L,43.5D)
    )
    val dataStream = env.fromCollection(stockList)
 
    //设定转换操作逻辑
    val keyedStream = dataStream.keyBy("stockId")
    val aggregationStream = keyedStream.sum(2)  //区别在这里   sum聚合 2表示第三个字段
  
    //打印输出
    aggregationStream.print()
 
    //执行操作
    env.execute(" AggregationTest")
  }
}

运行结果
在这里插入图片描述

5.数据输出

1.基本数据输出包括:文件输出,客户端输出,socket网络端口输出。
文件输出具体代码

val dataStream = env.fromElements("hadoop","spark","flink")
//文件输出
dataStream.writeAsText("file:///home/hadoop/output.txt")
//hdfs输出

//把数据写入HDFS
dataStream.writeAsText("hdfs://localhost:9000/output.txt“) 

//通过writeToSocket方法将DataStream数据集输出到指定socket端口
dataStream.writeToSocket(outputHost,outputPort,new SimpleStringSchema())

2.输出到kafka
代码举例:

import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer
 
object SinkKafkaTest{
  def main(args: Array[String]): Unit = {
 
    //获取执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    
//加载或创建数据源
    val dataStream = env.fromElements("hadoop","spark","flink")
    //把数据输出到Kafka
dataStream.addSink(new FlinkKafkaProducer [String]("localhost:9092", "sinkKafka", new SimpleStringSchema()))
    
//程序触发执行
    env.execute()
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2243909.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

爬虫开发工具与环境搭建——使用Postman和浏览器开发者工具

第三节:使用Postman和浏览器开发者工具 在网络爬虫开发过程中,我们经常需要对HTTP请求进行测试、分析和调试。Postman和浏览器开发者工具(特别是Network面板和Console面板)是两种最常用的工具,能够帮助开发者有效地捕…

vue2侧边导航栏路由

<template><div><!-- :default-active"$route.path" 和index对应其路径 --><el-menu:default-active"active"class"el-menu-vertical-demo"background-color"#545c64"text-color"#fff"active-text-col…

时代变迁对传统机器人等方向课程的巨大撕裂

2020年之后&#xff0c;全面转型新质课程规划&#xff0c;传统课程规划全部转为经验。 农耕-代表性生产关系-封建分配制度主要生产力-人力工业-代表性生产关系-资本分配制度工业分为机械时代&#xff0c;电气时代&#xff0c;信息时代&#xff1b;主要生产力-人力转为人脑&…

JVM类加载过程-Loading

一、Class对象的生命周期 .class文件是如何加载到内存中:.class文件是ClassLoader通过IO将文件读到内存,再通过双亲委派的模式进行Loading,再Linking、以及Initializing,代码调用等一系列操作后,进行GC,组成完整的生命周期; 二、双亲委派模式(Loading的过程): 1、类…

BERT--公认的里程碑

前言 如果说&#xff0c;让我选Transformer架构的哪个模型最深入人心&#xff0c;我将毫不犹豫的选择BERT&#xff01; BERT 的意义在于&#xff0c;从大量无标记的数据集中训练得到的深度模型&#xff0c;可以限制提高各项自然语言处理任务的准确率。 BERT 在当时&#xff0…

<项目代码>YOLOv8 瞳孔识别<目标检测>

YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0c;YOLOv8具有更高的…

每日OJ题_牛客_天使果冻_递推_C++_Java

目录 牛客_天使果冻_递推 题目解析 C代码 Java代码 牛客_天使果冻_递推 天使果冻 描述&#xff1a; 有 n 个果冻排成一排。第 i 个果冻的美味度是 ai。 天使非常喜欢吃果冻&#xff0c;但她想把最好吃的果冻留到最后收藏。天使想知道前 x个果冻中&#xff0c;美味…

果韵 2.0.1| 听歌神器,双端支持,支持下载歌曲和歌词

果韵是一款支持Windows和安卓双端的音乐播放器&#xff0c;支持自定义音源&#xff0c;界面简洁。用户可以通过缓存下载歌曲和歌词。为了使用这些功能&#xff0c;需要先进行音源导入。通过设置中的存储设置&#xff0c;将缓存文件夹移动到download目录下&#xff0c;之后缓存的…

Allegro从.brd文件中导出器件ball map

Step 1&#xff08;可选&#xff09;&#xff1a;设置网络颜色 Step2&#xff1a;File->Export->Symbol Spreadsheet\ Step3&#xff1a;Primary text选择Net Name Step 4&#xff1a;在.brd所有文件夹下生成一个ball map文件&#xff0c;其中网络颜色与Step 1一致。ba…

Docker入门之Windows安装Docker初体验

在之前我们认识了docker的容器&#xff0c;了解了docker的相关概念&#xff1a;镜像&#xff0c;容器&#xff0c;仓库&#xff1a;面试官让你介绍一下docker&#xff0c;别再说不知道了 之后又带大家动手体验了一下docker从零开始玩转 Docker&#xff1a;一站式入门指南&#…

家庭网络常识:猫与路由器

这张图大家应该不陌生——以前家庭网络的连接方式。 图1 家庭网络连接示意图 来说说猫/光猫&#xff1a; 先看看两者的图片。 图2 猫 图3 光猫 这个东西因为英文叫“modem”&#xff0c;类似中文的“猫”&#xff0c;所以简称“猫”。 猫和光猫的区别就是&#xff0c;一…

三种复制只有阅读权限的飞书网络文档的方法

大家都知道&#xff0c;飞书是一款功能强大的在线协作工具&#xff0c;可以帮助团队更高效地协作和沟通。越来越多的资料都在使用飞书文档&#xff0c;在使用飞书的过程中&#xff0c;发现很多文档没有复制权限&#xff0c;如果想要摘抄笔记&#xff0c;只能一个字一个字地敲出…

elasticsearch的倒排索引是什么?

大家好&#xff0c;我是锋哥。今天分享关于【elasticsearch的倒排索引是什么&#xff1f;】面试题。希望对大家有帮助&#xff1b; elasticsearch的倒排索引是什么&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 倒排索引&#xff08;Inverted Index&a…

基于Java Springboot甘肃旅游管理系统

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据…

uniApp项目运行到鸿蒙手机,应用图标一直是H,应用名一直是HBuilder问题

项目运行到鸿蒙手机&#xff0c;应用图标一直是H,应用名一直是HBuilder问题 应用运行到鸿蒙手机和鸿蒙模拟器&#xff0c;应用图标一直是H,应用名一直是HBuilder&#xff0c;在自动生成的harmony-configs文件夹下也没有配置的文件&#xff0c; 这时候需要你将DevEco Studio 下…

Python3.11.9+selenium,获取图片验证码以及输入验证码数字

Python3.11.9+selenium,获取图片验证码以及输入验证码数字 1、遇到问题:登录或修改密码需要验证码 2、解决办法: 2.1、安装ddddocr pip install ddddocr 2.2、解析验证码函数 import ddddocr def get_capcha_text():#获取验证码图片ele_pic = driver.find_element(By.XPAT…

中伟视界:AI智能分析算法如何针对非煤矿山的特定需求,提供定制化的安全生产解决方案

非煤矿山智能化改造&#xff0c;除了政策文件&#xff0c;上级监管单位需要安装的AI智能分析算法功能之外的&#xff0c;矿方真正关心的&#xff0c;能解决矿方安全生产隐患的AI智能分析算法功能有哪些呢&#xff1f; 经过与矿方的现场交流沟通&#xff0c;收集第一现场人员对安…

【论文速读】| 迈向自动化渗透测试:引入大语言模型基准、分析与改进

基本信息 原文标题&#xff1a;Towards Automated Penetration Testing: Introducing LLM Benchmark, Analysis, and Improvements 原文作者&#xff1a;Isamu Isozaki, Manil Shrestha, Rick Console, Edward Kim 作者单位&#xff1a;Drexel University, Independent 关键…

【Visual Studio系列教程】如何在 VS 上编程?

上一篇博客中&#xff0c;我们介绍了《什么是 Visual Studio&#xff1f;》。本文&#xff0c;我们来看第2篇《如何在 VS 上编程&#xff1f;》。阅读本文大约10 分钟。我们会向文件中添加代码&#xff0c;了解 Visual Studio 编写、导航和了解代码的简便方法。 本文假定&…

项目进度计划表:详细的甘特图的制作步骤

甘特图&#xff08;Gantt chart&#xff09;&#xff0c;又称为横道图、条状图&#xff08;Bar chart&#xff09;&#xff0c;是一种用于管理时间和任务活动的工具。 甘特图由亨利劳伦斯甘特&#xff08;Henry Laurence Gantt&#xff09;发明&#xff0c;是一种通过条状图来…