bin点云转pcd格式:自动驾驶数据集预处理与性能优化实战指南 简介面向3D点云处理、自动驾驶与机器人感知开发者这份资源聚焦LiDAR原始bin数据到PCD格式的转换并提供相机标定相关配置解决多传感器融合前数据格式统一与坐标对齐问题。压缩包共265个文件、约4.45MB文件以101个JSON配置与100个TXT说明/数据文本为主体包含Python转换脚本、示例BIN/PCD点云、辅助文件与Git仓库元数据目录结构便于按需提取。已有2122人学习适合具备一定Python基础的激光雷达数据工程师、自动驾驶感知算法初学者作为格式转换与标定对齐的参考。通过Python转换脚本可掌握二进制点云读取、PCD头信息生成、ASCII与二进制编码选择以及坐标变换的完整流程配合标定配置生成工具能直接应用到实车或实验平台的数据预处理中省去从零搭建转换流程的时间对于理解bin与PCD格式差异、点云元数据结构和多传感器联合标定也有直接帮助。 做自动驾驶和机器人三维数据处理的朋友应该都撞上过这样的尴尬数据集和传感器驱动给你的是一堆裸奔的.bin格式点云文件可你要跑分割、聚类或者可视化的时候常用库却只认 PCL 的.pcd格式。查半天资料发现网上不是讲得太玄就是压根跑不通只能硬着头皮读原始字节流。这篇文章我就直接把我自己项目里用过的转换方案、踩过的坑和优化过的代码给你摆出来顺便把 bin 格式里那几个容易要命的细节彻底讲透。先把核心场景说清楚所谓的 bin 转 pcd绝大多数情况下我们面对的是 KITTI、Waymo 或者 nuScenes 这类自动驾驶数据集里存储的原始点云二进制文件少数情况是工业相机或者自研传感器按自定义结构体写出来的点云。转换的本质不是文件后缀改名而是把二进制流里的字节按照既定的点字段顺序重新解析再封装成具备完整头信息点数、字段类型、宽度高度、视点等的标准点云格式。这篇文章不只是给你一段能跑通的代码更重要的是帮你搞清楚为什么这么写遇到异常字段、字节顺序、密度异常时该往哪个方向排查。适合刚入门点云处理或者被数据集格式折腾到想摔键盘的开发者参考。1. bin 格式和 pcd 格式核心差异与转换思路1.1 bin 格式的本质远不止是“没有头文件”的裸数据先说.bin点云文件。它本质上就是把内存里的点云数组直接按字节写进文件不存点数、不存坐标系、不存每个字段的类型。正因为如此没有元数据描述之外的外部知识bin 文件天生就是不可读的。很多刚入门的同学拿记事本打开一个 bin 文件看到一堆乱码就开始怀疑文件损坏这里直接给你断了这个念头这太正常了里面本来就是浮点数的机器表示。那么 KITTI 数据集里常见的 bin 文件具体长什么样它每个点的存储结构是float32 x点云的三维 X 坐标float32 y点云的三维 Y 坐标float32 z点云的三维 Z 坐标float32 intensity反射强度范围通常是 0~1也可能是 0~255 的整数取决于传感器型号和预处理流程也就是说四个 float32 一个点每个点 16 字节。总字节数除以 16就能得到点数。这也是为什么 bin 文件体积往往比同点数 pcd 文件小得多——没有任何冗余头信息字段紧凑本身还压缩了存储。而 Waymo dataset 里也常能见到 bin 文件但它的点结构又不一样包含了一堆标签字段。核心的物理坐标值依然是 x、y、z 在前面后面跟的可能是 intensity、elongation 这些。处理之前如果不弄清楚字段个数和类型拿错偏移量去解析点云就能直接错得离谱。注意拿到一个 bin 文件第一步永远是确认每个点的字节长度和字段顺序。这是最高优先级的步骤没有之一。1.2 pcd 格式为什么是更友好的“中间格式”pcdPoint Cloud Data是 PCL 库最早定义并推广的格式现在基本上成了点云处理领域的“通用语”。它的好处在于头文件把关键信息写清楚了FIELDS x y z intensity定义了每个点的字段名SIZE 4 4 4 4定义了每个字段占的字节数TYPE F F F F定义了字段类型WIDTH和HEIGHT定义了点的组织结构如果是无组织点云HEIGHT1WIDTH点数POINTS明明白白告诉你总点数。因此pcd 文件光靠自身头信息就可以被 PCL、Open3D、CloudCompare 等工具直接正确加载不需要外部任何额外说明。这也是我们费劲做转换的根因bin 是纯二进制pcd 是自带说明的二进制或者 ASCII能让下游工具“一眼看懂”。pcd 的存储有 ASCII 和二进制两种模式ASCII 模式每个点以文本形式写成一行x y z intensity换行。优点是任何文本编辑器打开都能直接看到坐标值方便调试和人工抽查缺点是文件体积膨胀严重读写速度也慢点多了写半天。二进制模式DATA ascii 改写成 DATA binary点数据部分直接按二进制紧凑排列文件更小读写显著更快。在转 pcd 的场景里尤其是大数据集处理我强烈建议你用二进制模式。它跟读 bin 一样是纯字节操作省去了大量格式化和文本解析开销性能差距能到 10 倍以上。1.3 转换的核心思路拆包字节流再按 pcd 规范重组转换思路简单概括就是两头对接中间桥接读取端打开 bin 文件按二进制方式读取全部字节知道点数 N 和每点字节数 S 的前提下把整个文件解释成 N 行、S/4 列的 float32 数组。字段截取前 3 个字段一定是 x、y、z。第 4 个往后可能是 intensity也有可能是别的一概视情况处理。不需要的字段可以丢弃需要的字段原样保留。写入端把 NumPy 数组分字段写入 pcd。写头信息要严格按 ASCII 格式组织字段名、字节大小、类型、维度一个都不能错。数据部分要么逐行写 ASCII要么用tofile一次性把二进制数组写进去注意字节序保持小端。这个思路说起来不复杂真正烦人的地方在于 bin 文件的字段不统一。我有一次拿到客户传感器给的 bin 文件头部还带了个自定义的magic number和 4 字节点数直接按纯点云解析出去连坐标都是垃圾数据。所以第一步永远是要搞到数据说明没有说明就先用对比法试错看点数合不合理、坐标范围是否符合常理。2. 环境准备与最简可运行代码实现2.1 环境搭建只需要 Python NumPy别学一堆库存着实现 bin 转 pcd不需要上 PCL C 那套重依赖除非你要做实时流处理。Python 环境 NumPy 足够。有 NumPy 我们再下载个 open3d 或者 pypcd 只是为了验证转出来的文件能不能读。如果你不想装任何额外库完全可以用纯 Python 的struct模块解析但效率低、代码冗余不推荐。先安装基础依赖pip install numpy为了后续验证建议顺手装一个 Open3Dpip install open3dOpen3D 能直接加载 pcd验证转换结果准确率比用 CloudCompare 手动看效率高得多。同时它也可以直接加载 bin 文件并可视化用来对比转换前后点云是否一致。实测下来 Open3D 是最省心的验证工具。2.2 核心转换脚本从 bin 到 ASCII 格式的 pcd下面直接给你一个能用的脚本同时支持 4 字段和 3 字段的 bin 文件输出的 pcd 是 ASCII 模式方便文本查看和校验import numpy as np import struct import sys import os def bin_to_pcd_ascii(bin_file, pcd_file, has_intensityTrue): # 读取 bin 文件为 numpy 数组 # 每个点为 4 个 float32: x, y, z, intensity若没有 intensity 则为 3 个 float32 point_dims 4 if has_intensity else 3 data np.fromfile(bin_file, dtypenp.float32).reshape(-1, point_dims) # 提取坐标 xyz data[:, :3] if has_intensity: intensity data[:, 3] # 写 pcd 头 n_points xyz.shape[0] header f# .PCD v0.7 - Point Cloud Data file format VERSION 0.7 FIELDS x y z intensity SIZE 4 4 4 4 TYPE F F F F COUNT 1 1 1 1 WIDTH {n_points} HEIGHT 1 VIEWPOINT 0 0 0 1 0 0 0 POINTS {n_points} DATA ascii # 拼接点数据 if has_intensity: points np.hstack([xyz, intensity.reshape(-1, 1)]) else: points xyz with open(pcd_file, w) as f: f.write(header) for p in points: f.write(f{p[0]:.6f} {p[1]:.6f} {p[2]:.6f} {p[3]:.6f}\n if has_intensity else f{p[0]:.6f} {p[1]:.6f} {p[2]:.6f}\n) if __name__ __main__: bin_file sys.argv[1] pcd_file sys.argv[2] has_intensity len(sys.argv) 4 or sys.argv[3] ! no_intensity bin_to_pcd_ascii(bin_file, pcd_file, has_intensity) print(f转换完成: {bin_file} - {pcd_file}, 点数: {np.fromfile(bin_file, dtypenp.float32).shape[0] // (4 if has_intensity else 3)})运行方式python bin2pcd.py input.bin output.pcd如果 bin 文件只有 xyz 没有强度字段python bin2pcd.py input.bin output.pcd no_intensity这段代码有几个设计要点需要说明下用np.fromfile一次性把文件读进内存默认按 float32 切分不像struct.unpack一个点一个点地解速度完全不在一个量级。这个文件读法在大点云场景是必备的。先把数组 reshape 成二维行数代表点数列数代表每个点的字段数。如果文件本身损坏或字节数不是 point_dims*4 的整数倍reshape 的时候会抛异常这也是个天然的校验手段。ASCII 写入部分用 f-string 拼字符串加:.6f保留 6 位小数点。有些点云工具对这个精度要求高我只写 6 位是为了控制文件体积如果要做精细配准建议保留 9 位小数或直接用二进制模式。ASCII 模式适合点云规模在 100 万点以内的情况。超过这个量级文件能膨胀到几百 MB写盘时间肉眼可见地煎熬这时候就该换二进制了。2.3 高性能方案直接写二进制 pcd如果你要处理的是几十万上百万点的文件比如 KITTI 里的 Velodyne 数据一帧动辄 12 万个点ASCII 转起来慢得让人抓狂。做自动驾驶数据集的预处理我用的是下面这个二进制版本速度能快 20 倍以上import numpy as np import sys def bin_to_pcd_binary(bin_file, pcd_file, has_intensityTrue): point_dims 4 if has_intensity else 3 data np.fromfile(bin_file, dtypenp.float32).reshape(-1, point_dims).astype(np.float32) n_points data.shape[0] # pcd binary 头 if has_intensity: fields_line FIELDS x y z intensity size_line SIZE 4 4 4 4 type_line TYPE F F F F count_line COUNT 1 1 1 1 else: fields_line FIELDS x y z size_line SIZE 4 4 4 type_line TYPE F F F count_line COUNT 1 1 1 header f# .PCD v0.7 - Point Cloud Data file format VERSION 0.7 {fields_line} {size_line} {type_line} {count_line} WIDTH {n_points} HEIGHT 1 VIEWPOINT 0 0 0 1 0 0 0 POINTS {n_points} DATA binary with open(pcd_file, wb) as f: f.write(header.encode(ascii)) # 直接写入二进制数组内容 data.tofile(f) if __name__ __main__: bin_file sys.argv[1] pcd_file sys.argv[2] has_intensity len(sys.argv) 4 or sys.argv[3] ! no_intensity bin_to_pcd_binary(bin_file, pcd_file, has_intensity) print(f二进制模式转换完成: {bin_file} - {pcd_file}, 点数: {np.fromfile(bin_file, dtypenp.float32).shape[0] // (4 if has_intensity else 3)})二进制模式的差异主要在于 DATA 是binary数据体部分直接在文件末尾连续写入 float32 数组。头文件的写入用encode(ascii)转换成字节因为wb模式不接受字符串。数据部分用tofile直写效率极高。这里有个容易疏忽的点pcd 二进制模式要求数据体从头信息之后的第一个字节开始紧凑排列不能有任何额外的对齐填充字节。NumPy 的默认 float32 数组正好满足要求但如果你做了一些列拼接操作导致数组变成非紧凑连续存储tofile之前必须先np.ascontiguousarray()转换一下否则写出去的数据内存布局完全不是你想象的顺序。3. 实操过程与核心环节实现3.1 实操前的文件体检别让坏文件浪费你一下午转换之前先做一次“体检”能省掉后面一堆排查时间。先看文件大小算一算预期的点数是否合理import os import numpy as np file_path 000000.bin file_size os.path.getsize(file_path) # 按 4 字段 float32 计算点数 n_points file_size / (4 * 4) # 每个点 16 字节 print(f文件大小: {file_size} 字节, 预计点数: {n_points}) # 按 3 字段计算 n_points_3d file_size / (3 * 4) print(f如果是 3 字段, 预计点数: {n_points_3d}) # 快速读取检查 data np.fromfile(file_path, dtypenp.float32) print(f实际读取 float32 个数: {data.shape[0]})如果文件大小不是 16 的倍数也不是 12 的倍数那说明这个 bin 文件就不是标准的 xyz(intensity) 结构要么带了额外字段要么带了文件头要么本身就是别的编码。这时候千万不能继续套标准流程得先弄清楚真实结构。我遇到过最离谱的一次文件大小是 16 的倍数但是前 4 个 float32 的值是20240501这种明显不可能出现在点云坐标里的数字查了协议才知道这是个时间戳字段。遇到这种就往文件开头偏移几个字节再试比如np.fromfile(file, dtypenp.float32, offset4).reshape(-1, 4)偏移量逐个试总能试出来。3.2 批量转换一整个数据集几万帧 bin 怎么处理项目里处理 KITTI 数据集的时候training 集就有 7481 帧 bin不可能一帧一帧手动跑。批量转换脚本核心就是遍历目录提取序号逐文件处理import os import glob import numpy as np def batch_convert_bin_to_pcd(bin_dir, pcd_dir, has_intensityTrue): os.makedirs(pcd_dir, exist_okTrue) bin_files sorted(glob.glob(os.path.join(bin_dir, *.bin))) for i, bin_path in enumerate(bin_files): basename os.path.basename(bin_path).replace(.bin, .pcd) pcd_path os.path.join(pcd_dir, basename) data np.fromfile(bin_path, dtypenp.float32) point_dims 4 if has_intensity else 3 if data.shape[0] % point_dims ! 0: print(f[跳过] {bin_path}: 字节数不是 {point_dims*4} 的整数倍) continue points data.reshape(-1, point_dims) n_points points.shape[0] # 写头信息 fields FIELDS x y z intensity if has_intensity else FIELDS x y z sizes SIZE 4 4 4 4 if has_intensity else SIZE 4 4 4 types TYPE F F F F if has_intensity else TYPE F F F counts COUNT 1 1 1 1 if has_intensity else COUNT 1 1 1 header ( f# .PCD v0.7 - Point Cloud Data file format\n fVERSION 0.7\n f{fields}\n f{sizes}\n f{types}\n f{counts}\n fWIDTH {n_points}\n fHEIGHT 1\n fVIEWPOINT 0 0 0 1 0 0 0\n fPOINTS {n_points}\n fDATA binary\n ) with open(pcd_path, wb) as f: f.write(header.encode(ascii)) points.tofile(f) if (i 1) % 1000 0: print(f已转换 {i 1} 帧...) print(f批量转换完成共处理 {len(bin_files)} 个文件输出目录: {pcd_dir}) if __name__ __main__: batch_convert_bin_to_pcd(path/to/bin_dir, path/to/pcd_dir)这里有个取舍要讲清楚批量转换为什么也推荐二进制模式因为如果一个数据集有 2 万帧ASCII 每帧 12 万点每帧文件体积从 2 MB 膨胀到 30 MB2 万帧就是 600 GB这下连存储都扛不住了。而二进制模式从 2 MB 变成约 2.4 MB增加量仅来自头部信息。如果你是为了上传到某些点云标注平台或者给别人分发数据请尽量用二进制模式毕竟没人喜欢几百 GB 的 ASCII 文本文件。批量脚本里我加了字节数校验和断点提示批量转几万帧时真的会有若干坏文件不做校验就会让整个流程中途崩掉。这个校验逻辑虽然简单但对跑数据集的人来说价值比转换本身还大。3.3 转换结果验证怎么确认转出来的 pcd 是对的装 Open3D 的目的就在这儿。转出来的 pcd 我建议至少做两层验证第一层验证是加载检查。打开转换后的文件确认点数对不对得上源文件import open3d as o3d pcd o3d.io.read_point_cloud(output.pcd) print(f加载点数: {len(pcd.points)}) print(f坐标范围 X: [{pcd.get_min_bound()[0]:.3f}, {pcd.get_max_bound()[0]:.3f}]) print(f坐标范围 Y: [{pcd.get_min_bound()[0]:.3f}, {pcd.get_max_bound()[0]:.3f}]) print(f坐标范围 Z: [{pcd.get_min_bound()[0]:.3f}, {pcd.get_max_bound()[0]:.3f}]) # 如果不带强度字段的 pcd加载后 points 数量一致即校验通过 # 如果带强度字段pcd.colors 和 pcd.normals 字段均为空强度不是颜色信息第二层验证是数据一致性抽查。随机抽几个点对比原始 bin 里的坐标和 pcd 里的坐标是否完全一致import numpy as np # 原始 bin raw_data np.fromfile(000000.bin, dtypenp.float32).reshape(-1, 4) # pcd ascii 转换后用 load_ascii 类似的逻辑读取前几行对比 # 这里直接加载 pcd binary 模式做对比 pcd_data np.asarray(pcd.points) indices np.random.choice(len(raw_data), 5, replaceFalse) for idx in indices: print(f原始 bin 第 {idx} 点: {raw_data[idx][:3]}) print(fpcd 第 {idx} 点: {pcd_data[idx]})我开发过程中曾经犯过一次低级错误坐标全转对了但是文件头里 WIDTH 写成了点数加 1导致 pcd 数据整体往后错了一个点整个点云形状直接崩坏。所以肉眼观察点云形状也很重要如果发现点云看起来像被拉伸了或者异常稀疏绝大多数情况都是头信息和数据长度对不上。用可视化确认一次比任何代码检查都直观。必须要明白的误区pcd 里的颜色字段和强度字段不是一回事。Open3D 加载 pcd 时默认解析 xyz强度需要在point[intensity]这样的通道里读取。如果你在写 pcd 头时把 intensity 字段保留下来在 Open3D 读完之后记得检查pcd.point[intensity]是否存在如果缺失说明头信息里FIELDS x y z intensity中的 intensity 被简化掉了。4. 常见问题与排查技巧实录4.1 Open3D 读不了转换后的 pcd显示 0 点这个太常见了十个转换的九个倒霉蛋都会遇到。通常的症状是转换脚本正常退出没有任何报错但用 Open3D 一读点数就是 0。排查方向依次检查头文件格式是否严格符合规范pcd 头文件的字段顺序是固定的VERSION - FIELDS - SIZE - TYPE - COUNT - WIDTH - HEIGHT - VIEWPOINT - POINTS - DATA不能乱序。我曾经见过有工具把VIEWPOINT写在WIDTH前面结果读不了。DATA 之后是否多了一个换行二进制模式下DATA binary\n之后必须紧跟数据不能再有其他分隔符。如果你用文本编辑器打开看过二进制 pcd 再保存恭喜你文件大概率废了。字节序pcd 标准默认小端little-endian。大部分 bin 文件也是小端但如果传感器是 ARM 大端模式或者你用了非标准工具需要先把 NumPy 数组转成小端再写。Open3D 加载时有个隐藏行为如果文件头说DATA binary但实际数据里混入了其他字符它不会立刻报错而是默默返回 0 点。这种最烦人因为没有任何错误提示。排查时需要把 pcd 文件的末尾几百字节 dump 出来确认 XX 是不是标准的 float32。4.2 转换后点云坐标爆炸或整体偏移如果你看到点云坐标值变成几千万、几亿或者 x、y 坐标范围明显异常最大嫌疑是字段读取错位。常见三种情况把 intensity 当成了 z 坐标z 的范围应该是传感器安装高度附近一般不会超过 100 米但你看到 z 值充满 0 到 1 之间就好理解了这不就是强度值吗文件头部有自定义信息没跳过bin 文件可能带了 4 字节或 8 字节的 magic number、时间戳。读取时用np.fromfile(..., offset4)跳过。维度搞错bin 实际是 5 通道多了 label 字段你按 4 通道 reshape坐标就全错了。先算文件大小判断是 12 的倍数、16 的倍数还是 20 的倍数就知道有几个通道。我排查固定问题时的标准流程如下先打印原始 bin 的前 100 个 float32 值肉眼观察数值分布。正常的点云坐标x、y、z 值是“有规律的乱”值在合理范围内波动如果第 1、2、3 个值之间相差几个数量级或者一堆 0 分布字段结构肯定不对。这个笨办法往往比写解析器还快。4.3 转换大文件内存溢出或者速度慢点云文件动辄几百 MB如果一次性np.fromfile再reshape再写内存峰值可能超过 1 GB。在 8GB 内存的机器上批量跑几万个文件分分钟 OOM。如果你不想动 C 路线Python 里可以用分块处理的方式。先读取整个文件的字节数设定一个块大小比如 100 万点循环处理分块并追加写入。但说实话这种场景我更推荐直接用第二章的二进制版本配合mmap换成内存映射读取。下面是一个简单的分块转换方案import numpy as np import os def bin_to_pcd_chunked(bin_file, pcd_file, chunk_points1000000, has_intensityTrue): point_dims 4 if has_intensity else 3 file_size os.path.getsize(bin_file) total_points file_size // (point_dims * 4) # 先写头信息 n_points total_points header ( f# .PCD v0.7 - Point Cloud Data file format\n fVERSION 0.7\n fFIELDS x y z {intensity if has_intensity else }\n fSIZE 4 4 4 {4 if has_intensity else }\n fTYPE F F F {F if has_intensity else }\n fCOUNT 1 1 1 {1 if has_intensity else }\n fWIDTH {n_points}\nHEIGHT 1\nVIEWPOINT 0 0 0 1 0 0 0\n fPOINTS {n_points}\nDATA binary\n ) with open(pcd_file, wb) as out_f: out_f.write(header.encode(ascii)) with open(bin_file, rb) as in_f: offset 0 while offset file_size: chunk np.fromfile(in_f, dtypenp.float32, countchunk_points * point_dims) if chunk.size 0: break chunk chunk.reshape(-1, point_dims) chunk.tofile(out_f) offset chunk.nbytes del chunk print(f分块转换完成总点数 {n_points})这里的核心是np.fromfile可以从当前文件句柄位置持续读取指定数量的 float32然后直接写入输出文件。内存占用稳定在几 MB 级别速度也只比一次性读全量慢 20% 左右是大文件转换的首选方案。4.4 文件扩展名问题pcd 文件打不开或者被当成文本文件最后唠叨一个细节当你在 Windows 上手动创建 pcd 文件时如果扩展名没有正确关联到点云处理软件可能会默认用记事本打开。这不算 bug但要注意别在记事本里“顺手保存”一下带二进制的 pcd否则记事本会把二进制数据按某种编码转成 Unicode 字符文件字节结构直接损坏。处理点云文件只有 ASCII 模式的 pcd 可以放心用文本编辑器打开二进制模式下绝对不要在文本编辑器中保存。遇到这种损坏的重建办法很简单回到源 bin 重新跑一遍转换脚本即可。这也提醒我们在项目里保留原始 bin 文件很重要pcd 是过程产物坏了随时能重建。不要为了省磁盘空间把 bin 删了到时候重建成本远超你省下那点空间。5. 从 pragmatism 出发的几条实操经验总结转换 bin 到 pcd 这件事代码本身 50 行以内就能搞定真正的复杂度永远在“弄清楚你的 bin 文件到底是什么结构”上。我在实际项目里反复踩过的几个坑最后再给你画一下重点第一永远不要根据文件后缀猜测格式。拿到 bin 文件第一步必须确认。文件大小能提供最重要的线索标准 xyzintensity 结构是 16 字节/点纯 xyz 是 12 字节/点五字段是 20 字节/点。看文件大小能被谁整除基本上八九不离十。第二在批量处理之前先挑三个文件测试三个都转完并且可视化确认没问题再上全量脚本。不要在几千个文件转完之后才发现字段结构错了重新转一遍的时间足够你喝好几杯咖啡。第三如果你和我一样需要在 Python 和 C 之间来回倒腾数据建议统一使用二进制模式的 pcd 作为中间格式。ASCII pcd 虽然调试友好但同一份数据在 Python 写的和 C 写的解析器里处理流程差异很大容易引入不必要的不一致。二进制模式直接按字节读两边看到的都是完全一样的内存布局。按照这个方案我自己在项目里一次性转换过两万帧 KITTI 格式的 bin 点云过程中只遇到十几帧坏文件字节数不符全部被脚本自动跳过并打日志。单帧 12 万点的 bin 转二进制 pcd 耗时大约 20 毫秒转换速度超过了后续标注和数据增强的速度完全够用。你按照这篇文的思路走一遍应该能少走很多弯路。本文还有配套的精品资源点击获取