当前位置: 首页 > news >正文

Linux小课堂: 文件归档与压缩技术之从 tar 到 gzip、bzip2 与 zip/rar 详解

核心概念梳理:打包(归档)与压缩的本质区别

在 Linux 系统中,文件的归档(打包)与压缩是两个独立但常被联合使用的操作。理解二者之间的区别,是掌握高效文件管理的第一步。

  • 打包 / 归档(Archiving)
    指将多个文件或目录合并为一个单一的“总文件”,即所谓的 archive(归档文件),不涉及数据体积的减少。这个过程类似于把一堆散落的纸张用订书机装订成册,便于统一管理和传输。
    常用的归档工具是 tar 命令,生成的文件通常以 .tar 为扩展名,称为 tarball(tar 包)。

  • 压缩(Compression)
    是指通过特定算法(如 Lempel-Ziv、Burrows-Wheeler 等)对单个大文件进行编码,使其占用更少磁盘空间的过程。压缩不会改变文件结构逻辑,仅优化存储效率。
    常见的压缩工具有 gzipbzip2,分别对应 .gz.bz2 扩展名。

关键点强调:tar 是归档工具,本身不具备压缩能力;而 gzipbzip2 是压缩工具,不能直接处理多文件集合。因此,典型的流程是:先用 tar 归档 → 再用 gzipbzip2 压缩归档文件,或使用 tar 的内置选项一步完成

tar:Linux 下最强大的归档工具详解

tar(Tape Archive)最初用于磁带备份,如今已成为 Linux 中通用的归档命令
它支持多种格式和扩展参数,能够灵活地创建、查看、提取和更新归档文件

1 ) 基本语法结构

tar [选项] [归档文件名] [目标文件/目录]

2 ) 常用参数解析

核心参数解析

参数含义
-ccreate:创建新的归档文件
-xextract:解压归档内容
-tlist:列出归档内包含的文件
-vverbose:显示处理过程中的详细信息
-ffile:指定归档文件名(必须紧跟其后)
-rappend:向已有归档追加文件
-z调用 gzip 进行压缩/解压(生成 .tar.gz.tgz
-j调用 bzip2 进行压缩/解压(生成 .tar.bz2
-J调用 xz 进行压缩/解压(生成 .tar.xz

示例命令结构:

tar -cvf archive.tar file1.txt file2.txt   # 创建2个文件的归档
# 最佳实践建议:在归档前应确保所有目标文件位于同一父目录内。
# 若归档不含目录结构而直接包含数百个文件,解压时极易污染当前工作目录,造成混乱。
tar -cvf sorting.tar sorting/              # 将 `sorting/` 目录下所有内容打包为 `sorting.tar`,包含原始路径结构# 该操作无需解压即可查看包内结构,适用于验证归档完整性
tar -tvf sorting.tar                       # 查看内容, 如下输出
# drwxr-xr-x user/user       0 2025-04-05 10:00 sorting/
# -rw-r--r-- user/user    1234 2025-04-05 10:00 sorting/quickSort.java
# -rw-r--r-- user/user    1567 2025-04-05 10:00 sorting/mergeSort.java
# -rw-r--r-- user/user    1098 2025-04-05 10:00 sorting/selectionSort.javatar -xvf archive.tar                      # 解压归档

3 ) 实战操作流程

3.1 准备工作:构建测试环境
为避免干扰主目录,建议建立专用目录用于实验:

mkdir compression && cd compression
cp -r /path/to/share/sorting ./
chown -R $USER:$USER sorting 

此时 sorting/ 目录包含三个 Java 排序实现文件:

  • quicksort.java
  • mergesort.java
  • selectionsort.java

3.2 使用 tar 进行归档

tar -cvf sorting.tar sorting/

执行后生成 sorting.tar,可通过以下命令验证内容:

tar -tf sorting.tar

输出示例:

sorting/
sorting/quicksort.java 
sorting/mergesort.java
sorting/selectionsort.java

最佳实践提示:归档前应将所有目标文件置于同一目录下。若归档不带父目录且含大量文件,解压时将导致当前路径混乱

3.3 动态追加文件至已有归档

echo "Extra content" > file_extra.txt# 追加文件到已有归档:`-r` 参数
tar -rvf sorting.tar file_extra.txt
  • 使用 -r 参数可向已存在的 .tar 文件追加新成员,追加文件至归档末尾
  • 注意:不能追加到已压缩的 .tar.gz.tar.bz2 文件,仅支持未压缩 .tar

2.4 提取归档内容

rm -rf sorting/              # 删除原目录模拟还原场景# 解开归档:`-x` 参数
tar -xvf sorting.tar         # 解压回原始结构ls sorting/                  # 验证恢复结果 
  • -x:extract,从归档中提取文件
  • 提取时会重建原始目录结构,除非使用 --strip-components=N 控制层级

压缩技术对比:gzip vs bzip2

归档完成后,下一步是对 .tar 文件进行压缩以节省空间。Linux 提供多种压缩工具,其中最常用的是 gzipbzip2

特性gzipbzip2
压缩率中等更高
压缩速度
CPU 资源消耗较低较高
默认后缀.gz.bz2
内存占用
适用场景日常快速压缩、Web 内容大型归档长期存储

1 ) 使用 gzip 压缩与解压

GZIP(.gz)—— 快速通用型压缩

  • 压缩率适中,速度快,适合日常使用
  • 使用 DEFLATE 算法(LZ77 + Huffman 编码)
  • 默认扩展名:.gz(单独文件)、.tar.gz.tgz(归档+压缩)
gzip sorting.tar                        # 压缩为 sorting.tar.gz,原文件自动删除
gzip -k sorting.tar                     # 压缩为 sorting.tar.gz,保留原文件
gzip -c sorting.tar > sorting.tar.gz    # 压缩为 sorting.tar.gz,保留原文件,可修改为其他名称,灵活度高gunzip sorting.tar.gz          # 解压恢复 sorting.tar
# 或等价写法
gzip -d sorting.tar.gz         # 解压后恢复为 `sorting.tar` 

两种实现方式的对比

参数命令示例原始文件压缩文件输出灵活性
-cgzip -c file.tar > custom_name.gz✅ 保留✅ 可自定义文件名高(需重定向)
-kgzip -k file.tar✅ 保留❌ 固定为 file.tar.gz低(自动命名)

操作建议

  1. 保留原始 .tar 且需自定义压缩文件名 → 用 -c + 重定向:
    gzip -c sorting.tar > sorting_backup.tar.gz 
    
  2. 保留原始 .tar 且接受默认压缩文件名 → 用 -k
    gzip -k sorting.tar  # 生成 sorting.tar.gz 
    

注意:

  • 部分 Unix 系统(如 macOS 的 BSD 版本)默认 gzip 可能不支持 -k
  • 若需跨平台兼容,优先选择 -c 方案

gzip 使用 DEFLATE 算法(LZ77 + Huffman 编码),压缩速度较快,压缩率适中,广泛用于网络传输和日志归档

2 )使用 bzip2 压缩与解压

BZIP2(.bz2)—— 高压缩率慢速选择

  • 压缩率显著高于 GZIP(尤其文本类数据)
  • 使用 Burrows-Wheeler 变换 + Move-to-Front + Huffman 编码
  • 更耗 CPU 与时间,适用于长期存储或网络分发场景
  • 扩展名:.bz2(单文件)、.tar.bz2
bzip2 sorting.tar              # 生成 sorting.tar.bz2
bunzip2 sorting.tar.bz2        # 解压为 sorting.tar

同样,默认行为是删除原始文件,可用 -k 保留。

一体化操作:tar 的高级用法(归档+压缩一步到位)

现代 tar 支持调用外部压缩程序,实现“一键归档并压缩”

1 ) 使用 gzip 一体化压缩

# 先清理旧文件
rm -f sorting.tar sorting.tar.gz# 一步归档并压缩为 .tar.gz 
tar -zcvf sorting.tar.gz sorting/

此处 -z 表示启用 gzip 压缩
实际等价于:tar -cvf - sorting/ | gzip > sorting.tar.gz

解压也只需一条命令:

tar -zxvf sorting.tar.gz

自动识别 .gz 后缀并通过 gunzip 流式解压

2 ) 使用 bzip2 一体化压缩

tar -jcvf sorting.tar.bz2 sorting/    # 归档并压缩
tar -jxvf sorting.tar.bz2             # 解压并展开

-j:表示使用 bzip2 压缩

注意:部分系统中 -j 依赖 bzip2 工具包安装。未安装时将报错 bzip2: command not found

反向验证流程示例:

rm -rf sorting/                          # 清理环境
tar -jxvf sorting.tar.bz2                # 一键解压
find sorting/ -name "*.java" -exec cat {} \;  # 查看源码内容

查看压缩文件内容而不解压

有时我们只想预览压缩包内的文本内容,无需解压整个文件
Linux 提供了专用工具链:

1 )针对 gzip 压缩文件

zcat    sorting.tar.gz      # 输出全部内容到终端     (相当于 `tar -O -xzf file.tar.gz`)
zmore   sorting.tar.gz      # 分页查看(类似 more)
zless   sorting.tar.gz      # 更强分页功能(推荐)

示例:查看归档中某 Java 文件内容

zless sorting.tar.gz
在内部可搜索:/QuickSort

2 )针对 bzip2 压缩文件

bzcat   sorting.tar.bz2        # 输出解压内容
bzmore  sorting.tar.bz2        # 分页查看
bzless  sorting.tar.bz2        # 推荐使用的交互式查看器

应用场景:分析远程日志压缩包(如 access.log.tar.gz)是否包含错误信息

这些命令底层调用了对应的解压流处理器,直接解析压缩流并输出明文,极大提升效率。
这些命令仅适用于由 gzip/bzip2 单独压缩的 .tar 文件,不适用于嵌套压缩或多段压缩包

跨平台兼容:处理 ZIP 与 RAR 格式

尽管 .tar.gz 是 Linux 主流格式,但在与 Windows 用户交互时,常遇到 .zip.rar 文件。

1 )安装与使用 unzip/zip 工具处理 ZIP

大多数发行版默认未安装 unzip,需手动安装:

CentOS/RHEL
sudo yum install -y unzip zip Ubuntu/Debian 
sudo apt-get install -y unzip zipSUSE/openSUSE
sudo zypper install unzip zip

压缩操作(生成 .zip)(递归打包)

zip -r sorting.zip sorting/

必须加 -r:否则只会打包空目录结构
支持加密:zip -er secured.zip folder/

解压操作

unzip sorting.zip
unzip -l sorting.zip    # 仅列出内容,不解压

优势:.zip 格式天然支持多文件压缩且自包含目录结构,适合跨平台交换

2 )处理 RAR 文件(rar/unrar)

RAR 工具非开源,需额外安装:

下载并安装 rar/unrar(需确认架构 以 x86_64 为例)
wget https://www.rarlab.com/rar/rarlinux-x64-6.0.2.tar.gz 
tar -xf rarlinux-x64-6.0.2.tar.gz
sudo cp rar/{rar,unrar} /usr/local/bin/

使用方法:

rar a archive.rar file1.txt file2.txt     # 创建 RAR
unrar x archive.rar                       # 解压 RAR
unrar l archive.rar                       # 查看内容
unrar e sorting.rar                       # 扁平化提取到当前目录
  • a:add files to archive

注意:生产环境中应谨慎使用非自由软件,考虑替代方案如 7z(p7zip-utils)
版权说明:RAR 工具属于商业软件,虽提供免费版,但企业部署建议购买授权

NestJS + TypeScript 示例:自动化归档与压缩服务模块


1 ) 方案1

以下是一个基于 NestJS 的微服务模块,封装了常见的归档与压缩逻辑,适用于需要在后端执行系统级文件操作的场景(如日志归档、用户上传打包等)。

技术栈:NestJS + TypeScript + node-tar + zlib + child_process

安装依赖

npm install tar zlib @types/node
npm install -D @types/compressing  # 如需高级压缩库

服务 CompressionService.ts

import { Injectable } from '@nestjs/common';
import * as tar from 'tar';
import * as fs from 'fs';
import * as path from 'path';
import { spawnSync } from 'child_process';@Injectable()
export class CompressionService {private readonly WORK_DIR = '/tmp/compression';constructor() {if (!fs.existsSync(this.WORK_DIR)) {fs.mkdirSync(this.WORK_DIR, { recursive: true });}}/* 使用 tar + gzip 一步创建 .tar.gz 归档*/createTarGz(sourceDir: string, outputFilename: string): boolean {const outputPath = path.join(this.WORK_DIR, outputFilename);try {tar.create({gzip: true,cwd: path.dirname(sourceDir),file: outputPath,recursive: true,},[path.basename(sourceDir)]);console.log(`✅ 归档成功: ${outputPath}`);return true;} catch (err) {console.error('❌ 归档失败:', err);return false;}}/* 解压 .tar.gz 文件*/extractTarGz(archivePath: string, targetDir: string): boolean {try {tar.extract({file: archivePath,cwd: targetDir,preservePaths: true,});console.log(`✅ 解压成功: ${targetDir}`);return true;} catch (err) {console.error('❌ 解压失败:', err);return false;}}/* 使用系统 zip 命令压缩目录 */zipDirectory(sourceDir: string, zipPath: string): boolean {const result = spawnSync('zip', ['-r', zipPath, '.'], {cwd: sourceDir,encoding: 'utf-8',});if (result.error) {console.error('Zip error:', result.error);return false;}if (result.status !== 0) {console.error('Zip failed:', result.stderr);return false;}console.log(`✅ ZIP 打包成功: ${zipPath}`);return true;}/* 使用系统 unzip 命令解压 ZIP 文件*/unzipFile(zipPath: string, targetDir: string): boolean {const result = spawnSync('unzip', ['-o', zipPath, '-d', targetDir], {encoding: 'utf-8',});if (result.status !== 0) {console.error('Unzip failed:', result.stderr);return false;}console.log(`✅ ZIP 解压成功: ${targetDir}`);return true;}/* 获取归档内容列表(模拟 tar -t)*/listTarContents(tarPath: string): string[] {const isGzipped = tarPath.endsWith('.gz');const args = isGzipped ? ['--gzip', '-tf'] : ['-tf'];args.push(tarPath);const result = spawnSync('tar', args, { encoding: 'utf-8' });if (result.status !== 0) {throw new Error(`Failed to list archive: ${result.stderr}`);}return result.stdout.trim().split('\n');}
}

控制器 Controller 示例调用

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CompressionService } from './compression.service';@Controller('archive')
export class ArchiveController {constructor(private readonly compService: CompressionService) {}@Post('tar-gz')archive(@Body() body: { src: string; name: string }) {const success = this.compService.createTarGz(body.src, body.name);return { success };}@Get('list')list(@Body() body: { path: string }) {try {const files = this.compService.listTarContents(body.path);return { files };} catch (e) {return { error: e.message };}}
}

2 )方案2

模拟 Linux 压缩行为的代码实现
虽然 Linux 命令行工具高效稳定,但在现代 DevOps 场景中,常需通过 API 自动化归档任务
以下是一个基于 NestJS 的服务模块,模拟 tar + gzip 行为:

// compression.service.ts
import { Injectable } from '@nestjs/common';
import * as tar from 'tar';
import * as zlib from 'zlib';
import * as fs from 'fs';
import * as path from 'path';@Injectable()
export class CompressionService {/* 创建 .tar.gz 归档文件 * @param sourceDir 源目录路径 * @param destTarGz 输出的 .tar.gz 文件路径*/async createTarGz(sourceDir: string, destTarGz: string): Promise<void> {const output = fs.createWriteStream(destTarGz);const archive = tar.pack(sourceDir); // 打包目录const compress = zlib.createGzip();   // GZIP 压缩流 return new Promise((resolve, reject) => {archive .pipe(compress).pipe(output).on('finish', () => resolve()).on('error', (err) => reject(err));});}/* 解压 .tar.gz 文件 * @param tarGzPath 输入的 .tar.gz 文件路径* @param extractPath 解压目标目录*/async extractTarGz(tarGzPath: string, extractPath: string): Promise<void> {const readStream = fs.createReadStream(tarGzPath);const extract = tar.extract({ cwd: extractPath, strip: 1 });return new Promise((resolve, reject) => {readStream.pipe(zlib.createGunzip()) // 先解压 GZIP.pipe(extract)             // 再解包 TAR.on('close', () => resolve()).on('error', (err) => reject(err));});}/* 获取归档内文件列表(模拟 tar -t)*/async listTarGzContents(tarGzPath: string): Promise<string[]> {const files: string[] = [];const readStream = fs.createReadStream(tarGzPath);return new Promise((resolve, reject) => {readStream.pipe(zlib.createGunzip()).pipe(tar.t({onentry: (entry) => {files.push(entry.path);}})).on('end', () => resolve(files)).on('error', (err) => reject(err));});}
}

注册控制器调用示例:

// compression.controller.ts
import { Controller, Post, Get, Body } from '@nestjs/common';
import { CompressionService } from './compression.service';@Controller('archive')
export class CompressionController {constructor(private readonly compService: CompressionService) {}@Post('create')async createArchive(@Body() body: { src: string; dest: string }) {await this.compService.createTarGz(body.src, body.dest);return { message: `Archive created: ${body.dest}` };}@Post('extract')async extractArchive(@Body() body: { file: string; target: string }) {await this.compService.extractTarGz(body.file, body.target);return { message: `Extracted to: ${body.target}` };}@Get('list')async listContents(@Body('file') file: string) {const contents = await this.compService.listTarGzContents(file);return { files: contents };}
}

应用场景:CI/CD 构建归档、日志打包上传、微服务间文件传输接口等。

3 ) 方案3

模拟了文件归档与压缩的核心逻辑(依赖 shell 执行系统命令):

// src/compression/compression.service.ts
import { Injectable } from '@nestjs/common';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';@Injectable()
export class CompressionService {private readonly baseDir = '/home/user/compression';/* 创建 TAR 归档*/createTar(archiveName: string, sourcePath: string): string {const tarFile = path.join(this.baseDir, `${archiveName}.tar`);const cmd = `tar -cvf ${tarFile} -C ${this.baseDir} ${sourcePath}`;try {const output = execSync(cmd, { encoding: 'utf-8' });console.log('TAR Output:', output);return tarFile;} catch (error) {throw new Error(`Failed to create TAR: ${error.message}`);}}/* 使用 GZIP 压缩 TAR 文件*/compressWithGzip(tarFile: string): string {const gzFile = `${tarFile}.gz`;const cmd = `gzip -c ${tarFile} > ${gzFile}`;try {execSync(cmd);return gzFile;} catch (error) {throw new Error(`GZIP compression failed: ${error.message}`);}}/* 使用 BZIP2 压缩 TAR 文件 */compressWithBzip2(tarFile: string): string {const bz2File = `${tarFile}.bz2`;const cmd = `bzip2 -c ${tarFile} > ${bz2File}`;try {execSync(cmd);return bz2File;} catch (error) {throw new Error(`BZIP2 compression failed: ${error.message}`);}}/* 一体式压缩:直接生成 .tar.gz*/createTarGz(archiveName: string, sourcePath: string): string {const filePath = path.join(this.baseDir, `${archiveName}.tar.gz`);const cmd = `tar -zcvf ${filePath} -C ${this.baseDir} ${sourcePath}`;try {execSync(cmd, { stdio: 'pipe' });return filePath;} catch (error) {throw new Error(`Failed to create tar.gz: ${error.message}`);}}/* 解压 .tar.gz 文件*/extractTarGz(tarGzFile: string, destination: string): void {const dest = path.join(this.baseDir, destination);if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });const cmd = `tar -zxvf ${tarGzFile} -C ${dest}`;try {execSync(cmd);} catch (error) {throw new Error(`Extraction failed: ${error.message}`);}}/* 列出 .tar.gz 内容*/listTarGzContents(tarGzFile: string): string[] {const result = execSync(`tar -tzf ${tarGzFile}`, { encoding: 'utf-8' });return result.trim().split('\n');}
}
// src/compression/compression.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CompressionService } from './compression.service';@Controller('compression')
export class CompressionController {constructor(private readonly compressionService: CompressionService) {}@Post('tar-gz')createTarGz(@Body() body: { name: string; path: string }) {const file = this.compressionService.createTarGz(body.name, body.path);return { message: 'Created .tar.gz', file };}@Get('list/:filename')listContents(@Body('filename') filename: string) {const fullPath = `/home/user/compression/${filename}`;const contents = this.compressionService.listTarGzContents(fullPath);return { files: contents };}@Post('extract')extract(@Body() body: { file: string; dest: string }) {this.compressionService.extractTarGz(body.file, body.dest);return { message: 'Extracted successfully' };}
}

说明:此模块封装了 tar, gzip, bzip2 的常见操作,可用于构建自动化部署脚本或文件管理系统后台

总结:Linux 文件压缩体系全图景

操作类型工具命令示例输出格式适用场景
归档tartar -cvf archive.tar dir/.tar备份、迁移
归档+gzip
压缩
tar + gziptar -zcvf archive.tar.gz dir/.tar.gz.tgz通用发布
归档+bzip2
高压缩
tar + bzip2tar -jcvf archive.tar.bz2 dir/.tar.bz2存档存储
ZIP 压缩
跨平台共享
zipzip -r archive.zip dir/.zipWindows/Linux 互通
ZIP 解压unzipunzip archive.zip解出原结构
RAR 支持rar/unrarunrar x archive.rar.rar
查看压缩内容zless, bzlesszless file1.gz file2.gz
bzless error.log.bz2 -g “timeout”
# -g 高亮显示 “timeout” 关键词
快速审计

最终要点凝练:

  • tar 是归档核心,gzip/bzip2 是压缩引擎
  • .tar.gz 是 Linux 最佳实践格式
  • ZIP 适合跨平台共享,RAR 需额外安装支持
  • 可通过 zcat, bzless 等命令直接读取压缩文本内容
  • 在开发中可通过 Node.js 调用系统命令或使用 tar 库实现自动化归档服务

注意事项

项目推荐做法
归档前组织文件先将待打包文件放入统一目录,避免解压时污染当前路径
命名规范使用清晰命名,如 project-v1.0.tar.gz,便于版本控制
压缩选择日常通信选 .tar.gz;长期归档选 .tar.bz2;跨平台选 .zip
权限保持tar 默认保留权限与时间戳,重要数据请验证一致性
增量备份可结合 findtar 实现按修改时间归档

最终结论:

  • tar 是骨架,gzip/bzip2 是肌肉,zip/rar 是桥梁
  • 掌握这一体系,不仅能应对本地文件管理,更能无缝融入自动化运维与分布式系统构建之中
  • 务必牢记:先归档,后压缩;看清格式,选对工具;善用流式处理,避免临时文件泛滥
http://www.dtcms.com/a/511580.html

相关文章:

  • IT科技资讯新闻类织梦网站模板定制化网站开发
  • 编程 网站建设一站式快速网站排名多少钱
  • 工厂防护鞋穿戴检测预防足部伤害 防护鞋穿戴检测 未佩戴防护鞋实时报警 基于YOLOv8的防护鞋识别算法
  • 「日拱一码」126 机器学习路线
  • react学习笔记【一】
  • Drawnix - 开源白板工具
  • 网站制作是怎么学的WordPress博客右边设置
  • go build -tags的其他用法
  • 【Unity开发】try-finally 与 try-catch 的区别详解
  • PHP数据库操作全攻略
  • 标准解读——GB/T 46353—2025《信息技术 大数据 数据资产价值评估》国家标准
  • Herm详解
  • 重庆网站建设哪家公司那家好winserver2008上用iis发布网站
  • HTML-CSS项目练习
  • 如何编写自动化测试用例?
  • 【Vibe Coding】001-前端界面常用布局
  • webview 中 cursor:pointer无效是由于-webkit-app-region导致的
  • 【C++】哈希表的实现【开放定址法vs链地址法】
  • 【业务逻辑漏洞】认证漏洞
  • 做网站在哪深圳做网站 汉狮网络
  • 修改k8s的镜像源为国内镜像源
  • Arbess从入门到实战(15) - 使用Arbess+GitHub实现Docker项目自动化构建部署
  • 【MySQL】从零开始了解数据库开发 ---mysql事务机制(一)
  • 网站建设明细盐都建设局网站
  • 基于单片机的气象站labview上位机监测系统
  • Chainlit+LlamaIndex 多模态 RAG 开发实战7:从系统架构到功能落地,搞定 PDF/PPT/ 图片全类型文件处理
  • 在VScode中将一个分支的某一次提交合并到另一个分支中
  • MAC M芯片安装配置VMware+Ubuntu
  • 免费seo推广软件网站排名优化软件联系方式
  • Nebula全球私有云网络部署与配置综合指南