FEATURED · 精选文章

Unity图集切割实战:基于Texture2D.SetPixel的离线资源处理工具

发布时间 / 2026/8/4 19:52:28
来源 / 创域科博编辑部
栏目 / 资讯中心
Unity图集切割实战:基于Texture2D.SetPixel的离线资源处理工具 1. 项目概述为什么我们需要手动切割图集在Unity项目开发中尤其是UI和2D游戏领域Sprite Atlas精灵图集是优化性能的标配。它把大量零散的小图打包成一张大图能显著减少Draw Call提升渲染效率。官方提供的SpriteAtlas资源用起来很方便打包、引用、运行时加载一气呵成。但最近我在一个老项目迁移和资源处理工具链开发中遇到了一个棘手的需求需要将已经打包好的Sprite Atlas按原始小图的边界重新切割成独立的Texture2D或Sprite资源。你可能会问Unity不是有现成的SpriteAtlas.GetSprite方法吗没错在运行时如果你知道精灵的名字可以动态获取。但我的场景是离线处理我需要写一个编辑器工具将图集资源批量还原用于资源审计、第三方渠道分包、或者为不支持Sprite Atlas的老版本引擎或特定平台准备资源。这时候直接访问图集纹理并手动切割就成了唯一的选择。核心思路很直接获取到图集对应的Texture2D对象然后根据图集中每个精灵Sprite的纹理坐标UV和像素尺寸信息从这张大纹理上把对应的像素块“抠”出来生成一张新的小纹理。Texture2D.SetPixel方法正是实现这个“抠图”操作的基础API。听起来简单但实操中从UV计算像素坐标、处理边缘、管理内存、确保颜色准确每一步都有坑。这篇文章我就结合一个完整的编辑器工具实现带你走一遍流程并附上我踩过的那些坑和解决方案。2. 核心原理与准备工作2.1 Sprite Atlas与纹理坐标系统要切割首先得知道切哪里。Unity的Sprite Atlas在打包后会生成一个.spriteatlas文件和一个纹理文件如Atlas.png。这个纹理文件就是我们要操作的Texture2D。图集中的每个原始精灵都对应这个大纹理上的一个矩形区域。关键信息存储在哪里在Sprite对象里。即使精灵被打包进图集其Sprite.rect属性仍然记录着它在原始纹理即图集纹理上的像素位置和大小。更重要的是Sprite.uv属性它返回一个Vector2数组通常是4个顶点表示这个精灵的四个角在图集纹理上的归一化纹理坐标。像素坐标 (rect): 例如(x: 128, y: 256, width: 64, height: 64)表示这个精灵位于图集纹理的第128列、第256行以左下角为原点宽高各64像素。纹理坐标 (uv): 范围在[0, 1]之间。例如左下角UV可能是(0.25, 0.5)右上角是(0.375, 0.625)。它们是通过像素坐标除以纹理总尺寸得到的。我们的切割算法本质就是利用rect信息在图集纹理上定位一个矩形区域然后将这个区域内的所有像素读取出来。uv可以用来验证计算是否正确尤其是在处理纹理边缘或旋转/裁剪过的精灵时。2.2 Texture2D.SetPixel 与 GetPixel 的工作机制Texture2D.SetPixel(x, y, color)和Texture2D.GetPixel(x, y)是处理纹理像素最直观的方法。GetPixel(int x, int y): 读取纹理在整数坐标(x, y)处的像素颜色。坐标原点(0,0)在纹理的左下角。这一点非常重要和常见的图像处理库原点在左上角不同是第一个容易混淆的点。SetPixel(int x, int y, Color color): 将指定颜色设置到纹理的(x, y)坐标处。为什么选择它们因为它们概念简单兼容性好在编辑器脚本中对于一次性处理任务足够直观。当然它们效率不高因为每次调用都涉及托管到原生代码的转换。对于超大图集或批量处理应该使用GetPixels()/SetPixels()这类批量方法。但为了清晰地展示原理和步骤我们从SetPixel/GetPixel开始理解了之后优化到批量方法就水到渠成了。注意在编辑器脚本中操作纹理必须确保纹理的Read/Write Enabled属性在导入设置中已勾选否则GetPixel会失败。这是第一个大坑。2.3 工具环境与目标设定我们将创建一个Editor Window工具。目标功能如下允许用户拖拽或选择一个Sprite Atlas资源。解析该图集列出其中包含的所有精灵及其信息名称、位置、尺寸。允许用户选择输出目录。点击按钮将图集中的每个精灵切割并保存为独立的PNG图片并可选地生成对应的Sprite资产。我们将处理两种常见的精灵类型标准矩形精灵最简单rect就是精确的像素边界。九宫格Sliced精灵或网格Grid精灵这类精灵的rect可能只定义了有效区域但uv可能映射到图集的更复杂区域如果打包时进行了旋转或裁剪。我们的基础版本先处理标准矩形后续会讨论复杂情况的处理。3. 实战步骤从图集到独立纹理3.1 第一步获取图集纹理与精灵列表首先我们需要获取到图集对应的纹理和里面所有的精灵。这里不能直接加载.spriteatlas文件因为它是配置数据。我们需要获取它关联的Texture2D。using UnityEngine; using UnityEditor; using System.IO; using System.Collections.Generic; public class AtlasSplitterWindow : EditorWindow { private SpriteAtlas targetAtlas; private Texture2D atlasTexture; private ListSprite spritesInAtlas new ListSprite(); private string outputPath Assets/SplitSprites; void OnGUI() { // 1. 选择图集 EditorGUI.BeginChangeCheck(); targetAtlas (SpriteAtlas)EditorGUILayout.ObjectField(目标图集, targetAtlas, typeof(SpriteAtlas), false); if (EditorGUI.EndChangeCheck()) { UpdateAtlasData(); } // ... 其他UI和按钮 } void UpdateAtlasData() { spritesInAtlas.Clear(); atlasTexture null; if (targetAtlas null) return; // 关键获取图集打包后产生的纹理 // 方法获取图集的所有打包资源对象 UnityEngine.Object[] packedAssets targetAtlas.GetPackables(); // 通常第一个Texture2D类型的对象就是图集纹理 foreach (var obj in packedAssets) { if (obj is Texture2D tex) { atlasTexture tex; break; } } if (atlasTexture null) { Debug.LogError(无法从选中的SpriteAtlas中获取纹理请确保图集已打包。); return; } // 获取图集中的所有精灵 // 注意SpriteAtlas.GetSprites() 在Editor模式下可能无法直接使用或返回空。 // 更可靠的方法是从打包资源中寻找类型为Sprite的对象。 // 但更通用的方法是通过图集纹理来获取关联的Sprite。 // 我们可以通过AssetDatabase找到所有引用此纹理的Sprite资源。 string texturePath AssetDatabase.GetAssetPath(atlasTexture); string[] allAssetPaths AssetDatabase.GetAllAssetPaths(); foreach (var assetPath in allAssetPaths) { if (assetPath.EndsWith(.png) || assetPath.EndsWith(.jpg) || assetPath.EndsWith(.asset)) { // 加载资源并检查是否是Sprite且其纹理是否是我们找到的图集纹理 Object[] assets AssetDatabase.LoadAllAssetsAtPath(assetPath); foreach (var asset in assets) { if (asset is Sprite sprite) { // 比较精灵关联的纹理和我们的图集纹理 if (sprite.texture ! null sprite.texture atlasTexture) { if (!spritesInAtlas.Contains(sprite)) { spritesInAtlas.Add(sprite); } } } } } } Debug.Log($找到图集纹理: {atlasTexture.name}, 尺寸: {atlasTexture.width}x{atlasTexture.height}); Debug.Log($找到精灵数量: {spritesInAtlas.Count}); } }这段代码的核心是UpdateAtlasData方法。它首先通过GetPackables()尝试获取图集纹理然后遍历项目资源找到所有纹理引用为此图集纹理的Sprite对象。这个方法在编辑器下比较可靠但效率不是最高。对于已知精灵来源的项目可以有更优解。避坑指南1获取精灵列表的陷阱网上很多教程会直接用Resources.FindObjectsOfTypeAllSprite()然后过滤纹理这在编辑器场景下会返回场景中和所有预制件里的Sprite组件数量巨大且包含很多你不想要的。而AssetDatabase.FindAssets和LoadAssetAtPath的组合虽然准确但需要你知道精灵资源的确切存储路径。上面提供的遍历方法是一种“广撒网”的可靠方法适合通用工具但如果是处理特定目录下的图集建议优化为搜索特定文件夹。3.2 第二步计算精灵的精确像素区域拿到Sprite对象后sprite.rect给了我们像素矩形。但这里有个细节rect是Rect结构其x,y是浮点数。纹理像素坐标是整数。我们需要将这个矩形转换为一个整数像素范围的循环。Rect spriteRect sprite.rect; // 将Rect的浮点坐标转换为整数像素范围的起始点和结束点 int startX Mathf.FloorToInt(spriteRect.x); int startY Mathf.FloorToInt(spriteRect.y); int endX Mathf.CeilToInt(spriteRect.x spriteRect.width); int endY Mathf.CeilToInt(spriteRect.y spriteRect.height); // 确保坐标在纹理范围内 startX Mathf.Clamp(startX, 0, atlasTexture.width); startY Mathf.Clamp(startY, 0, atlasTexture.height); endX Mathf.Clamp(endX, 0, atlasTexture.width); endY Mathf.Clamp(endY, 0, atlasTexture.height); int width endX - startX; int height endY - startY;这里使用FloorToInt和CeilToInt是为了处理可能的浮点误差确保整个精灵区域都被包含进来。Clamp是安全措施防止计算错误导致索引越界。3.3 第三步使用GetPixel和SetPixel逐像素复制这是最核心的循环。我们为每个精灵创建一个新的Texture2D然后从图集纹理中复制像素。Texture2D newTexture new Texture2D(width, height, atlasTexture.format, false); newTexture.name sprite.name; // 关键的双重循环遍历新纹理的每一个像素位置 for (int x 0; x width; x) { for (int y 0; y height; y) { // 计算在图集纹理中的对应坐标 int sourceX startX x; int sourceY startY y; // 注意纹理坐标原点在左下角我们的循环从下往上y从小变大是符合的。 // 从图集获取像素颜色 Color pixelColor atlasTexture.GetPixel(sourceX, sourceY); // 设置到新纹理的对应位置 newTexture.SetPixel(x, y, pixelColor); } } // 必须调用Apply才能使SetPixel的更改生效 newTexture.Apply();循环的细节外层x循环遍历宽度内层y循环遍历高度。对于新纹理上的每个点(x, y)我们在图集纹理上找到对应的点(startX x, startY y)。这里y坐标的方向在Texture2D的坐标系中是一致的左下角原点所以直接相加即可。如果你用其他图像库如System.Drawing可能会遇到Y轴翻转的问题。避坑指南2Texture2D的格式与Alpha通道创建新纹理时我使用了atlasTexture.format来保持格式一致。这通常是正确的但要注意如果源图集是压缩格式如DXT1, DXT5Texture2D构造函数可能不支持。在编辑器脚本中为了确保兼容性我通常使用TextureFormat.RGBA32或TextureFormat.ARGB32这种未压缩格式。另外如果精灵有透明边缘GetPixel获取的颜色可能包含预乘Alpha或边缘滤波的颜色直接复制过去可能导致边缘出现杂色。对于要求极高的项目可能需要更复杂的处理比如结合sprite.vertices和sprite.uv进行精确采样。3.4 第四步保存纹理为资产并生成Sprite像素复制完成后我们需要将Texture2D对象保存到磁盘成为.png或.jpg文件并为其创建Sprite资产。// 将Texture2D编码为PNG字节 byte[] pngData newTexture.EncodeToPNG(); // 定义输出路径 string spriteFileName ${sprite.name}.png; string fullOutputPath Path.Combine(outputPath, spriteFileName); // 确保目录存在 Directory.CreateDirectory(Path.GetDirectoryName(fullOutputPath)); // 写入文件 File.WriteAllBytes(fullOutputPath, pngData); // 重要刷新AssetDatabase让Unity识别新文件 AssetDatabase.Refresh(); // 可选为新纹理文件设置导入设置并创建Sprite string assetPath fullOutputPath.Replace(Application.dataPath, Assets); TextureImporter importer AssetImporter.GetAtPath(assetPath) as TextureImporter; if (importer ! null) { importer.textureType TextureImporterType.Sprite; // 如果你想保持原始精灵的Pivot和MeshType可以从原Sprite中读取并赋值 // importer.spritePivot sprite.pivot; // importer.spriteImportMode SpriteImportMode.Single; // 或 Multiple // 如果需要还可以设置spriteBorder九宫格 EditorUtility.SetDirty(importer); importer.SaveAndReimport(); } Debug.Log($已保存精灵: {assetPath});关键操作EncodeToPNG(): 将内存中的Texture2D转换为PNG格式的字节流。File.WriteAllBytes: 将字节流写入磁盘。注意路径是系统路径。AssetDatabase.Refresh():至关重要。写入文件后必须刷新资源数据库Unity才会加载这个新文件。TextureImporter: 通过AssetImporter获取纹理的导入器将其类型设置为Sprite这样才能作为精灵使用。你还可以在这里精细配置Pivot中心点、Mesh Type等以匹配原始精灵的设置。避坑指南3文件路径与AssetDatabase使用File.WriteAllBytes时路径是操作系统绝对路径如C:\Project\Assets\...。而AssetDatabase和AssetImporter的接口需要的是相对于项目Assets文件夹的路径如Assets/SplitSprites/icon.png。Application.dataPath返回的是Assets文件夹的绝对路径。进行转换时务必小心错误的路径会导致AssetImporter.GetAtPath返回null。一个常见的错误是使用/还是\在Unity中使用Path.Combine和相对路径Assets/...是更安全的选择。4. 性能优化从SetPixel到GetPixels32上面的逐像素GetPixel/SetPixel循环在切割一个64x64的精灵时需要4096次函数调用。如果图集有上百个精灵性能会非常低下。优化方法就是使用批量像素操作方法。Texture2D.GetPixels32()和Texture2D.SetPixels32()一次性读取或设置整个纹理或一个矩形区域的所有像素到一个Color32数组中。Color32是Color的整数版本每个通道0-255处理速度更快且避免了浮点运算。优化后的切割循环// 1. 一次性读取图集纹理的全部像素数据 Color32[] allAtlasPixels atlasTexture.GetPixels32(); // 对于每个精灵... Rect spriteRect sprite.rect; int startX (int)spriteRect.x; int startY (int)spriteRect.y; int width (int)spriteRect.width; int height (int)spriteRect.height; // 2. 创建一个空的Color32数组用于存放切割出的像素 Color32[] extractedPixels new Color32[width * height]; // 3. 计算并复制像素块 for (int y 0; y height; y) { for (int x 0; x width; x) { // 计算在图集一维数组中的索引 int atlasIndex (startY y) * atlasTexture.width (startX x); // 计算在提取数组中的索引 int extractIndex y * width x; extractedPixels[extractIndex] allAtlasPixels[atlasIndex]; } } // 4. 创建新纹理并批量设置像素 Texture2D newTex new Texture2D(width, height, TextureFormat.RGBA32, false); newTex.SetPixels32(extractedPixels); newTex.Apply();性能对比假设图集是2048x2048切割一个64x64的精灵。GetPixel/SetPixel方案4096次独立调用每次调用都有开销。GetPixels32/SetPixels32方案1次读取整个图集约400万像素然后进行4096次内存数组拷贝极快最后1次SetPixels32调用。后者速度有数量级的提升。对于编辑器工具尤其是批量处理务必使用批量像素接口。避坑指南4数组索引与行列顺序在一维数组中模拟二维像素布局时索引计算公式是index y * width x。这里width是纹理的宽度每行的像素数。在嵌套循环中外层循环y内层循环x这种访问顺序是按行访问对CPU缓存友好效率更高。如果搞反了顺序外层x内层y就是按列访问可能会造成大量的缓存未命中在处理大纹理时性能差异明显。5. 处理复杂情况与边界问题5.1 旋转与裁切的精灵如果精灵在图集打包时被允许旋转Pack Settings中的Rotation选项那么sprite.rect定义的矩形可能并不是精灵像素数据的轴对齐边界框。旋转后的精灵其sprite.uv定义的纹理坐标会映射到图集纹理的一个旋转后的区域。我们上面基于rect的轴对齐矩形切割方法会切出一个包含精灵的外接矩形里面会包含大量透明像素四个角并且精灵本身是斜的。如何检测和处理检查sprite.packed和sprite.packingRotation属性。如果sprite.packingRotation不是SpritePackingRotation.None则说明精灵被旋转了通常是90度的倍数。处理旋转的简易方法适用于90/180/270度旋转仍然按rect切割出外接矩形纹理。根据packingRotation的值对切割出的纹理进行旋转。旋转后你需要计算精灵实际内容的新边界可能不再是矩形这变得非常复杂。更实用的建议对于需要处理旋转精灵的通用工具简单的像素复制方法就不够了。一个更健壮但不完美的方法是利用sprite.vertices和sprite.uv。vertices是精灵网格在局部空间的顶点uv是对应的纹理坐标。你可以为精灵创建一个新的网格并使用uv从图集纹理中进行纹理采样。这涉及到在像素级别进行双线性插值超出了GetPixel的范畴需要用到Texture2D.GetPixelBilinear(u, v)或者更底层的图形API。实操心得在大多数项目规范中为了简化资源管理和避免运行时性能损耗会禁止图集打包旋转。如果你的项目有这个限制那么就可以安全地忽略旋转情况。在工具中可以添加一个检查如果发现旋转的精灵则输出警告并跳过或按未旋转处理结果会是错的但至少让用户知道。5.2 纹理边缘与滤波造成的颜色污染当精灵紧挨着图集边缘或者两个精灵在图集中紧密排列时由于纹理滤波Filter Mode不是Point的存在GetPixel在读取边缘像素时可能会采样到相邻精灵或透明区域的像素颜色导致切割出的精灵边缘有一圈淡淡的“颜色污染”。解决方案使用Point滤波在切割前将图集纹理的filterMode临时设置为FilterMode.Point切割完成后再改回去。这能确保GetPixel读取的是精确的像素不进行混合。FilterMode originalFilter atlasTexture.filterMode; atlasTexture.filterMode FilterMode.Point; // ... 执行切割操作 ... atlasTexture.filterMode originalFilter; // 注意直接修改导入纹理的filterMode可能不会立即生效或影响原始资产最好在内存中的纹理副本上操作。预留“透明边距”这是更根本的解决方案。在图集打包设置SpriteAtlas的Padding属性中增加足够的像素边距例如2或4像素。这样精灵之间就有间隙滤波也不会采样到邻居的颜色。我们切割时rect定义的区域已经包含了这个边距内的透明像素所以切割结果是干净的。精确的UV边界采样如果边距为0又必须用滤波那么计算采样坐标时需要向内收缩半个像素。例如对于一个从uMin到uMax的UV范围实际采样时使用uMin 0.5/texWidth到uMax - 0.5/texWidth。这在使用GetPixelBilinear时尤为重要但用GetPixel整数坐标采样时无法实现。5.3 内存管理与大纹理处理GetPixels32()会返回纹理所有像素的一个副本。一个2048x2048的RGBA32纹理占用内存约为2048 * 2048 * 4 bytes ≈ 16.7 MB。如果你的图集很大或者同时处理多个图集内存压力会剧增。优化策略分块处理不要一次性读取整个图集。可以按需读取每个精灵对应的矩形区域使用Texture2D.GetPixels32(int x, int y, int blockWidth, int blockHeight)。这样每次只读取一小块内存。及时销毁使用new Texture2D创建的新纹理以及GetPixels32返回的数组都是托管内存。在处理完一个精灵并保存为文件后应立即将引用置为null并可以调用Resources.UnloadAsset或DestroyImmediate来释放纹理对象在编辑器脚本中需谨慎。使用using或封装作用域将纹理处理代码封装在局部作用域内让临时变量尽快被垃圾回收。进度条与异步对于非常大的图集使用EditorUtility.DisplayProgressBar显示进度条并考虑将处理逻辑放在协程或异步任务中避免编辑器卡死无响应。// 示例分块读取与进度显示 int totalSprites spritesInAtlas.Count; for (int i 0; i totalSprites; i) { Sprite sprite spritesInAtlas[i]; // 更新进度条 if (EditorUtility.DisplayCancelableProgressBar(切割图集, $正在处理 {sprite.name} ({i1}/{totalSprites}), (float)i / totalSprites)) { // 用户取消了 break; } Rect rect sprite.rect; // 只读取这个精灵区域的像素 Color32[] blockPixels atlasTexture.GetPixels32((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height); // ... 处理并保存 ... // 及时清理 blockPixels null; Resources.UnloadUnusedAssets(); } EditorUtility.ClearProgressBar();6. 完整工具实现与扩展思路将上述所有步骤整合一个基础的图集切割工具就完成了。你可以为其添加更多功能批量选择与处理允许用户选择一个文件夹自动处理其中所有的Sprite Atlas。自定义命名规则输出文件名的规则例如添加前缀、后缀。多种输出格式支持PNG、JPG、TGA等。保留Sprite元数据不仅切割纹理还尝试将原始Sprite的Pivot、Border、Pixels Per Unit等信息保存到新的Sprite设置中。这需要更精细地操作TextureImporter和SpriteMetaData。生成Prefab或Animation Clip对于序列帧动画图集可以按命名规则排序后自动生成Animation Clip甚至包含Animator的Prefab。与AssetPostprocessor结合创建一个后处理器当检测到图集纹理被修改时自动执行切割用于某些自动化流水线。一个重要的扩展处理Multiple模式Sprite如果原始精灵是SpriteMode.Multiple即一张纹理包含多个精灵被打包进图集后每个子精灵都是一个独立的Sprite对象。我们的工具已经能处理这种情况因为spritesInAtlas列表里包含了每一个子精灵。切割后会为每一个子精灵生成独立的纹理文件。如果你希望保持“一张纹理包含多个精灵”的结构就需要更复杂的逻辑将所有子精灵切割后再根据它们的相对位置拼接到一张新的纹理上并生成对应的sprite sheet数据通过TextureImporter.spritesheet。这相当于一个反向的图集打包过程实现起来复杂得多需要仔细计算布局和UV。7. 避坑指南总结与常见问题排查Q1: 运行工具后输出的图片全是空白透明或颜色不对。A1:首先检查图集纹理的Read/Write Enabled是否打开。这是最常见的原因。在Project窗口选中图集纹理在Inspector中查看导入设置。A2:检查像素坐标计算。在循环中打印startX, startY, width, height以及几个样本点的sourceX, sourceY确认它们在纹理范围内并且GetPixel读取的颜色不是Color.clear。A3:确认你操作的是正确的纹理。通过Debug.Log(atlasTexture.name , atlasTexture.width x atlasTexture.height)输出信息核对。Q2: 切割出来的图片边缘有杂色或模糊。A2:如图集纹理的Filter Mode不是Point且精灵之间没有Padding。按照上文“纹理边缘与滤波”章节的解决方案处理。最推荐的方法是确保图集打包时有足够的Padding2-4像素。Q3: 处理速度非常慢编辑器卡死。A3:你很可能在使用GetPixel/SetPixel循环。立即切换到GetPixels32/SetPixels32方案。如果已经用了批量接口还慢检查是否一次性读取了超大图集。考虑使用分块读取(GetPixels32带区域参数)并添加进度条。Q4: 有些精灵切割后方向不对旋转了90度。A4:该精灵在图集中被旋转打包了。检查sprite.packingRotation。如果项目允许旋转你需要实现旋转逻辑。如果项目不允许应该在图集打包设置中禁用旋转并重新打包。Q5: 生成的Sprite的Pivot中心点不对。A5:我们的基础工具只复制了像素没有复制Sprite的元数据。在保存后设置TextureImporter时你可以从原Sprite读取pivot和spriteBorder等信息进行赋值。importer.spritePivot sprite.pivot; Vector4 border sprite.border; importer.spriteBorder new Vector4(border.x, border.y, border.z, border.w);Q6: 如何切割Mesh Type为Tightly Packed的精灵A6:Tightly Packed的精灵其网格不是矩形而是由许多三角形组成的自定义形状以节省透明区域。sprite.rect是其包围盒。如果你按矩形切割会包含很多透明像素。要精确切割需要遍历sprite.vertices和sprite.triangles为每个顶点根据sprite.uv从图集采样颜色然后在新纹理上对应位置需要重新计算UV绘制。这是一个高级主题涉及网格和纹理的重新映射通常需要借助Texture2D.ReadPixels配合临时RenderTexture或使用Graphics.DrawMesh来实现复杂度远超本文范围。对于大多数情况按矩形切割并保留透明边是可以接受的。最后分享一个我个人的小技巧在编写这类资源处理工具时总是先在一个备份项目或单独目录中测试。因为文件操作和AssetDatabase的刷新是不可逆的批量处理一旦出错可能会覆盖大量原始数据。先小范围测试确认输出无误后再进行全量操作。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻