
简介围绕Spark初级编程实践整理的课程实验报告文档面向大数据、Spark与Hadoop初学者及需要完成课程实验的高校学生。内容以Ubuntu Kylin 16.04、Hadoop 3.1.3、JDK 1.8和Eclipse环境为背景覆盖Hadoop与Spark安装配置、spark-shell启动、读取本地文件与HDFS文件并统计行数以及用Scala编写统计行数、数据去重、平均成绩计算三类独立应用配合sbt打包和spark-submit提交运行串联典型数据处理场景。包内共1个docx文件约1.9MB以图文实验报告形式记录操作过程、运行结果和问题排查包含本地路径书写、HDFS目录定位、URL空格导致异常等排错思路。已有8351人学习适合对照复现实验步骤、理解Spark基本编程流程并查漏补缺。1. Spark 读文件连环报错三个异常八成全栽在路径字符串上跑大数据课程实验、照着指导书一行行敲 Spark 命令的人八成都在同一个地方栽过sc.textFile(...)括号里那串路径。我在 Hadoop 3.1.3 Spark 的 Ubuntu 虚拟机上带过几轮实验最高频的三个报错是java.lang.IllegalArgumentException、org.apache.hadoop.mapred.InvalidInputException: Input path does not exist和java.net.URISyntaxException: Illegal character in scheme name at index 0。它们长得完全不一样堆栈一个比一个长但根因高度集中在同一件事上scheme 写没写对、斜杠够不够、字符串前面有没有多余空格。下面按「报错长什么样 → 为什么是它 → 怎么改 → 下次怎么一眼认出来」拆开讲每条都能直接抄着用。2. IllegalArgumentExceptionfile:/// 少写一个斜杠Spark 就找不到本地文件2.1 报错现场统计本地文件行数第一条命令就崩在 spark-shell 里读 Linux 本地文件并统计行数是 Spark 上手的第一条命令也是第一次出错的地方。回车之后没有返回数字先甩出一大段红色堆栈最顶上那行是java.lang.IllegalArgumentException。往下翻Caused by里往往还嵌着一句java.net.URISyntaxException提示某个位置有非法字符或者 scheme 不对。很多人第一反应是 Spark 装坏了、Hadoop 没起来于是重装环境一晚上就没了。其实堆栈已经交代得很清楚出问题的是sc.textFile()括号里那个字符串不是集群。实验里实际踩到的就是这一条——本地路径的 URL 少打了一个/改成三斜杠就通了。// 场景统计 /usr/local/hadoop/1.txt 的行数 // 错误写法file: 后面只有两个斜杠usr 会被当成主机名 // val textFile sc.textFile(file://usr/local/hadoop/1.txt) // 正确写法file:/// 三斜杠authority 为空才表示本机 val textFile sc.textFile(file:///usr/local/hadoop/1.txt) textFile.count() // 返回 Long 类型的行数这里的逻辑是textFile接收的是一个 URI 字符串Spark 内部用new Path(...)解析它再交给FileInputFormat去匹配文件。字符串本身不合法解析阶段就会抛异常压根走不到读文件那一步。所以这类报错统一先怀疑字符串别急着怀疑环境。2.2 根因URI 里的 authority 被你的路径首段顶替了URI 的通用格式是scheme://authority/path。拆开看两种写法file:///usr/local/hadoop/1.txtscheme 是fileauthority 为空path 是/usr/local/hadoop/1.txt。file://usr/local/hadoop/1.txtscheme 是fileauthority 变成了usrpath 只剩/local/hadoop/1.txt。Hadoop 的Path在构造时就用new URI(...)解析authority 一旦被usr占位后续FileInputFormat拿到的路径就变成/local/hadoop/1.txt本地磁盘上根本没这个文件。如果此时路径里还带空格或中文没做转义比如/home/hadoop/my data/1.txtnew URI会直接抛URISyntaxException: Illegal character in path at index NSpark 在textFile入口把它包一层再抛出来于是你在栈顶看到的是「非法参数」真正的原因藏在Caused by里。这就是很多人对着IllegalArgumentException一脸茫然的原因。关于只写一个斜杠的file:/usr/...Java URI 是允许的authority 为空path 以/开头Hadoop 多数场景也认。但它一旦和new Path(parent, child)拼接、或者拿去和FileSystem.get()返回的 fs 做比较就容易蹦出Wrong FS: hdfs://... expected: file:///这种更难懂的报错。新手期我建议统一写三斜杠别给自己留判断空间。2.3 解决三种本地路径写法与对应场景写法解析出的路径什么时候能用file:///usr/local/hadoop/1.txt/usr/local/hadoop/1.txt推荐本地文件通用写法file:/usr/local/hadoop/1.txt同上合法但不建议拼接场景容易出Wrong FS/usr/local/hadoop/1.txt取决于fs.defaultFS单机默认配置可用配了 HDFS 后会指向 HDFS1.txt相对路径相对于进程工作目录本地 spark-shell 能跑提交到集群必然失败改完代码先用 shell 确认文件真的在别在代码里猜# 确认文件存在和大小 ls -l /usr/local/hadoop/1.txt # 用系统命令数一遍行数和 Spark 的 count() 对一下 wc -l /usr/local/hadoop/1.txt参数说明wc -l统计的是换行符\n的个数如果文件最后一行没有换行符它会比 Spark 的count()少 1。这不是谁算错了是两种统计口径的差别对不上时先想想这个。2.4 相对路径为什么在 spark-shell 能跑、提交就废sc.textFile(test.txt)在./bin/spark-shell本地模式下相对的是你敲命令时所在的目录所以能读到看起来一切正常。但spark-submit --master local[*]换个目录启动就找不到提交到 standalone 或 YARN 之后代码在 executor 的容器里执行工作目录是容器分配的临时目录相对路径指的是那儿必然报路径不存在。这就是为什么指导书上明明写的是/home/hadoop/test.txt这种绝对路径还是有人图省事写成test.txt——本地跑通就以为没事了打包提交那一刻才发现问题。血泪经验就一句只要代码要打成 JAR 交上去跑路径一律写绝对路径本地文件加file:///HDFS 文件加完整 scheme。3. HDFS 路径不存在InvalidInputException 背后是「~ 不是 HDFS 的根」3.1 报错现场Input path does not exist前缀却是 file:本地那关过了接着要在 spark-shell 里读 HDFS 上的/user/hadoop/test.txt。如果这个文件压根没传上去count()会抛org.apache.hadoop.mapred.InvalidInputException: Input path does not exist: file:/user/hadoop/test.txt注意这行报错的后半段前缀是file:而不是hdfs:。这是整篇里最有价值的一条线索——它说明 Spark 根本没往 HDFS 上找它找的是本地磁盘的/user/hadoop/test.txt。不少人盯着「路径不存在」五个字反复在 HDFS 上ls看到文件明明在然后彻底懵掉。区别就在于你找的是哪套文件系统。3.2 根因一HDFS 里没有~家目录还得自己建在 Linux 终端敲cd ~会进/home/hadoop那是 shell 帮你展开的本地路径。HDFS 是另一套命名空间hadoop fs -ls /看到的才是它的根。实验指导里写的/user/hadoop是约定俗成的用户目录但 Hadoop 启动时并不会自动帮你mkdir。所以「HDFS 文件系统的根目录不是~而应该是/user/hadoop」这句结论的准确含义是你在 HDFS 里的默认工作目录是/user/你的用户名而且在单机伪分布式环境里这个目录经常需要手动创建。先建目录再传文件顺序反了就会一路报不存在# 建目录-p 表示父目录不存在时一并创建 hadoop fs -mkdir -p /user/hadoop # 从本地推到 HDFS-f 表示目标已存在时覆盖 hadoop fs -put -f /home/hadoop/test.txt /user/hadoop/ # 确认文件确实在 HDFS 上而不是本地某个同名目录 hadoop fs -ls /user/hadoop # 看内容并核对行数 hadoop fs -cat /user/hadoop/test.txt | wc -l参数说明-p一次建多层-f覆盖同名文件不加会报File existshadoop fs与hdfs dfs等价用哪个都行。3.3 根因二fs.defaultFS 没配成 HDFS/user/hadoop 被当成本地目录还有一种更隐蔽的情况路径写对了hadoop fs -ls /user/hadoop也能看到文件但sc.textFile(/user/hadoop/test.txt)依然报不存在报错前缀还是file:。问题出在core-site.xml的fs.defaultFS。如果它还是默认的file:///那么不带 scheme 的绝对路径会被解释成本地文件系统路径。/user/hadoop/test.txt在 HDFS 上有在本地磁盘上没有Spark 找了后者当然找不到。一行命令就能验明正身# 看当前生效的默认文件系统 hdfs getconf -confKey fs.defaultFS输出应该是hdfs://localhost:9000这种形式如果是file:///说明配置没生效回去改core-site.xml再重启。顺带提醒Hadoop 3.x 的 NameNode RPC 端口有的环境配 9000有的是 8020以你自己配置文件里的值为准照抄别人的命令很容易对不上。3.4 解决显式写全 scheme让路径不依赖配置最稳的写法是代码里把 scheme 写死不赌fs.defaultFS的当前值// 显式带上 hdfs:// 前缀避免被当成 file: 路径 val hdfsFile sc.textFile(hdfs://localhost:9000/user/hadoop/test.txt) hdfsFile.count()参数说明localhost:9000必须和fs.defaultFS一字不差包括主机名写法localhost和127.0.0.1在部分配置下不等价。如果写全 scheme 能跑、不写就跑不了那八成就是 3.3 里说的配置问题而不是路径拼错了。再补一个容易忽略的点权限。hadoop fs -ls /user看一下目录属主如果/user/hadoop的属主不是你当前登录的用户put上去会报Permission denied: userxxx, accessWRITE。先whoami确认自己是谁再决定是改目录属主还是换个路径。本地文件系统和 HDFS 的命令容易混做个对照记一下操作本地文件系统HDFS列目录ls -l /home/hadoophadoop fs -ls /user/hadoop上传cp a bhadoop fs -put a /user/hadoop/看内容cat fhadoop fs -cat /user/hadoop/f删除rm -f fhadoop fs -rm -r /user/hadoop/f独立应用比如统计行数的SimpleApp打包提交之后走的是同一套逻辑所以先在 spark-shell 里把路径跑通再写进 Scala 代码能省掉大量「到底是环境问题还是代码问题」的扯皮。4. URISyntaxException: Illegal character in scheme name at index 0第 0 位就是那个空格4.1 报错解读index 0 说明第一个字符就不是字母java.net.URISyntaxException: Illegal character in scheme name at index 0URI 的 scheme 有硬性规则首字符必须是字母后续字符只能是字母、数字、、-、.。index 从 0 开始数报「scheme name at index 0」等于直接告诉你字符串的第一个字符不合法。那第一个字符可能是什么半角空格、Tab、全角空格、不间断空格、BOM、换行符。这些在终端里全是看不见的在编辑器里也就是一格空白人眼几乎分辨不出来。实验里做数据去重那个独立应用打包提交后运行就报了这个异常翻来覆去看代码都觉得没问题最后发现是 HDFS 地址 URL 前面多了一个空格。4.2 根因复制粘贴和多行字符串缩进来源基本就两个。一是从网页、PDF、聊天窗口复制路径带进了不可见字符二是 Scala 里手写字符串拼接手滑多敲一个空格// 错误拼接时多了一个空格 hdfs 让 scheme 首字符变成空格 val base hdfs://localhost:9000 val path base /user/hadoop/test.txt val rdd sc.textFile(path) // URISyntaxException: Illegal character in scheme name at index 0 // 正确路径整体作为一个常量拼接处不放空格 val HDFS_BASE hdfs://localhost:9000 val rdd2 sc.textFile(s$HDFS_BASE/user/hadoop/test.txt)s...插值、stripMargin的多行字符串、从 IDE 里整段复制过来带缩进的代码块都是重灾区。尤其多行字符串用|做边距时行首缩进如果没被stripMargin吃掉就会原样进到字符串里。4.3 解决三步定位并清掉看不见的字符# 1. 让不可见字符显形行尾显示 $Tab 显示为 ^I cat -A RemDup.scala | grep -n textFile # 2. 还看不出来就直接看字节空格是 040Tab 是 \tBOM 是 357 273 277 sed -n 15p RemDup.scala | od -c | head -5 # 3. 清掉行首空白和 Windows 换行符改之前先备份 cp RemDup.scala RemDup.scala.bak sed -i s/^[[:space:]]*//; s/\r$// RemDup.scala参数说明cat -A等价于-vET把不可打印字符转义显示od -c按字符打印字节sed -i原地修改文件所以先cp一份[[:space:]]覆盖空格和 Tab\r用来处理在 Windows 下编辑过的文件。这里有个坑要注意sed只清行首空白别顺手把字符串内部的空格也清了。路径本身含空格时比如/home/hadoop/my data/正确做法是给目录改名或者把空格转义成%20而不是删掉它。4.4 预防路径写成常量编辑器打开显示空白几个习惯能挡掉这类问题。路径在文件顶部定义成常量拼接时用字符串插值别手敲IDEA 里打开 View → Active Editor → Show Whitespaces空格和 Tab 一眼可见从浏览器复制路径后先在纯文本编辑器里过一遍再粘进代码。另外Illegal character in scheme name at index 0和Illegal character in path at index N是同一家族区别只在 index 落在 scheme 段还是 path 段定位手法完全一样——看 index 附近的字节。它们的共同点是报错信息其实非常精确精确到第几个字符只是那个字符你看不见。5. sbt 打包与 spark-submit 常见问题排查打包成功不等于能跑起来5.1 现象ClassNotFoundException找不到主类提交之后报java.lang.ClassNotFoundException: SimpleApp但sbt package明明显示成功。原因通常是两个一是代码里写了package com.demo.app--class却只写了SimpleApp少了包名二是 JAR 里压根没有这个类比如build.sbt里的name和源码目录结构对不上或者打包时编译失败但被后面的输出刷过去了。解决先看 JAR 里到底有什么再决定--class怎么写。# 列出 JAR 内的 class 文件确认类名和包路径 jar tf target/scala-2.11/a-simple-hdfs-test_2.11-1.0.jar | grep -i simpleapp # 有包名时--class 必须写全限定名 /usr/local/spark/bin/spark-submit \ --class com.demo.app.SimpleApp \ target/scala-2.11/a-simple-hdfs-test_2.11-1.0.jar参数说明jar tf只列目录不解析内容--class后面跟的是全限定类名一行一个字的差别都会导致找不到。5.2 现象Error: Unable to access jarfile命令里的 JAR 路径找不到文件。原因target/scala-2.11/里的2.11是 Scala 二进制版本改了scalaVersion就变成scala-2.12JAR 文件名由name和version拼出来项目名一改文件名跟着变。照着指导书抄路径项目名却不一样就会对不上。解决别猜直接找。# 找当前项目下所有 jar确认真实文件名 find . -name *.jar -path */target/*写build.sbt时依赖的 scope 也要写对否则会把 Spark 自身的 jar 一起打进包里运行期和集群自带的冲突name : Simple Project version : 1.0 scalaVersion : 2.11.8 // 必须和 spark-shell 用的 Scala 版本一致 libraryDependencies org.apache.spark %% spark-core % 你的Spark版本 % provided参数说明%%会自动在 artifact 名后补 Scala 二进制版本后缀provided表示编译时需要、运行时不打包Spark 集群本身已经提供。scalaVersion可以用spark-shell里执行scala.util.Properties.versionNumberString查出来。5.3 现象FileAlreadyExistsException输出目录已存在org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory hdfs://localhost:9000/user/hadoop/output already exists原因saveAsTextFile要求输出目录事先不存在这是 MapReduce 输出提交机制决定的避免重跑时把上一次的结果混进去。HDFS 没有「覆盖写入」这个语义。解决换一个带时间戳的目录名或者先删再跑。# 删之前先确认目录内容-rm -r 不可恢复 hadoop fs -ls /user/hadoop/output hadoop fs -rm -r /user/hadoop/output参数说明-r递归删子目录删 HDFS 目录必须带-skipTrash可以跳过回收站直接删实验环境里通常不需要。5.4 现象NoSuchMethodError 或 NoClassDefFoundError任务在 executor 上崩报java.lang.NoSuchMethodError: org.apache.spark...或NoClassDefFoundError: scala/...。原因spark-core、scala-library没有标provided被打进 JAR 里运行期和集群自带的版本打架或者本地用 Scala 2.12 编译集群跑的是 2.11字节码对不上。解决依赖加 本文还有配套的精品资源点击获取