jq与csvkit:JSON/CSV数据处理

1816 字
9 分钟
jq与csvkit:JSON/CSV数据处理

做生信分析之前有个很容易被忽略的环节:处理元数据。样本表、分组信息、临床数据、API 返回的 JSON……这些”边角料”往往占据分析 30% 的时间。很多人习惯打开 Python 写 pandas.read_csv(),但对于快速查看、过滤、转换——命令行工具快得多。

本文介绍两个命令行工具:jq 处理 JSON,csvkit 处理 CSV/TSV。无需启动 Python/R 即可完成大部分数据清洗工作——过滤、转换、统计、合并。

实测环境:Debian 12,jq 1.7.1,csvkit 2.0.1。

1. jq——JSON 处理的瑞士军刀#

1.1 安装与第一印象#

Terminal window
sudo apt install jq -y
jq --version
# jq-1.7.1

生信里 JSON 无处不在:NCBI E-utilities 返回 JSON,Ensembl REST API 返回 JSON,各种工具的配置文件(Nextflow、Snakemake 的参数)也是 JSON。直接 cat 看是灾难,jq 是解药。

Terminal window
# 美化输出
curl -s "https://rest.ensembl.org/lookup/id/ENSG00000139618?expand=1" \
-H "Content-Type: application/json" | jq '.'
# 彩色、缩进、一目了然

1.2 基础过滤——点号路径访问#

jq 的过滤表达式就是它的核心语法:

Terminal window
# 假设有一个 JSON 文件 samples.json
# [
# {"sample": "S1", "condition": "treated", "reads": 35000000, "qc": "PASS"},
# {"sample": "S2", "condition": "control", "reads": 28000000, "qc": "PASS"},
# {"sample": "S3", "condition": "treated", "reads": 15000000, "qc": "FAIL"}
# ]
# 提取所有样本名
jq '.[].sample' samples.json
# 提取 treated 组的样本
jq '.[] | select(.condition == "treated")' samples.json
# 提取 reads > 20000000 的样本
jq '.[] | select(.reads > 20000000)' samples.json
# 同时过滤多个条件
jq '.[] | select(.condition == "treated" and .qc == "PASS")' samples.json

1.3 数据转换——从 JSON 到 TSV#

Terminal window
# 提取多个字段并输出为 TSV(生信最常用操作)
jq -r '.[] | [.sample, .condition, .reads] | @tsv' samples.json
# -r = raw output(不加引号)
# @tsv 把数组转为 tab 分隔
# 加表头
echo -e "sample\tcondition\treads"
jq -r '.[] | [.sample, .condition, .reads] | @tsv' samples.json

1.4 实战:处理 Ensembl REST API 返回#

Terminal window
# 查询基因 BRCA2 的信息
curl -s "https://rest.ensembl.org/lookup/symbol/homo_sapiens/BRCA2?expand=1" \
-H "Content-Type: application/json" \
| jq '{id: .id, name: .display_name, chr: .seq_region_name, start: .start, end: .end, strand: .strand}'

1.5 数组和对象操作#

Terminal window
# 统计数组长度
jq 'length' samples.json
# 按条件分组
jq 'group_by(.condition)' samples.json
# 对某字段求和
jq '[.[].reads] | add' samples.json
# 计算平均值
jq '[.[].reads] | add / length' samples.json
# 去重
jq '[.[].condition] | unique' samples.json
# 排序
jq 'sort_by(.reads) | reverse' samples.json

1.6 处理嵌套 JSON——生信最常见难题#

NCBI E-utilities 返回的 JSON 往往嵌套很深:

Terminal window
# NCBI esummary 返回的 JSON 结构复杂
# 直接用 jq 层层深入
curl -s "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=nuccore&id=NM_000059&retmode=json" \
| jq '.result.uids[] as $uid | .result[$uid] | { accession: .accessionversion, title: .title, length: .slen }'

1.7 变量和函数#

Terminal window
# 使用变量
jq --arg cond "treated" '.[] | select(.condition == $cond)' samples.json
# 自定义函数
jq 'def mb: . / 1000000; .[] | {sample: .sample, reads_m: (.reads | mb)}' samples.json

2. csvkit——命令行里的 Excel#

2.1 安装#

Terminal window
# 通过 pip 安装(推荐在 conda 环境里)
pip install csvkit
# 或用 conda
conda install -c conda-forge csvkit

csvkit 是一套工具集,核心命令:

命令功能类比 Excel
csvlook格式化查看 CSV打开文件看
csvcut选择列删除/隐藏列
csvgrep按模式过滤行筛选器
csvsort排序排序功能
csvstat统计摘要描述性统计
csvjoin关联两个表VLOOKUP
csvstack纵向合并多个表追加行
in2csv转换 Excel 到 CSV另存为

2.2 快速查看——csvlook#

Terminal window
# 用 csvlook 美美地看 TSV/CSV
cat clinical_data.tsv | csvlook -t
# -t 指定 tab 分隔
# 只显示前10行(和 | head 类似但保留表头)
csvlook -t clinical_data.tsv | head -11

csvlook 的输出带 Markdown 风格的表格线,很适合粘贴到实验记录里。

2.3 列操作——csvcut#

Terminal window
# 查看所有列名(生信里元数据表经常几十列)
csvcut -n clinical_data.tsv
# 只保留指定列
csvcut -c sample_id,condition,age,gender clinical_data.tsv | csvlook
# 按列号选择(第1、3、5列)
csvcut -c 1,3,5 clinical_data.tsv
# 排除某些列
csvcut -C patient_notes,raw_data_path clinical_data.tsv
# -C = 排除这些列

2.4 过滤行——csvgrep#

Terminal window
# 筛选 condition 列等于 "tumor" 的行
csvgrep -t -c condition -m "tumor" clinical_data.tsv | csvlook
# -c 指定列名
# -m 匹配模式(支持正则)
# -i 反转(排除匹配的行)
# 正则筛选:样本名以 S 开头后跟数字
csvgrep -t -c sample_id -r "^S[0-9]+$" clinical_data.tsv
# 筛选年龄大于 50 的(需要结合其他工具)
# csvgrep 本身不支持数值比较,可以:
cat clinical_data.tsv | awk -F'\t' 'NR==1 || $4>50' | csvlook

2.5 排序——csvsort#

Terminal window
# 按 reads 数量降序排列
csvsort -t -c reads_count -r sample_stats.tsv | csvlook
# -r = reverse(降序)
# 多列排序:先按condition,再按age
csvsort -t -c condition,age sample_stats.tsv | csvlook

2.6 统计分析——csvstat#

Terminal window
# 一键生成各列统计摘要
csvstat -t sample_stats.tsv
# 输出示例:
# 1. "sample_id"
# Type: Text
# Unique values: 48
# 2. "reads_count"
# Type: Number
# Min: 12,345,678
# Max: 89,012,345
# Mean: 45,234,567
# Median: 44,100,000
# Std Dev: 15,234,567

生信场景:新拿到一批数据后,先 csvstat 看一眼每个样本的 reads 数是否合理、有无异常值。

2.7 表关联——csvjoin#

Terminal window
# 类似 SQL 的 LEFT JOIN
# sample_meta.tsv: sample_id, condition, batch
# sample_qc.tsv: sample_id, total_reads, q30_rate
csvjoin -t -c sample_id sample_meta.tsv sample_qc.tsv | csvlook -t
# 等价于:
# SELECT * FROM sample_meta LEFT JOIN sample_qc ON sample_meta.sample_id = sample_qc.sample_id

2.8 合并多个文件——csvstack#

Terminal window
# 纵向拼接(行追加)
# batch1_samples.tsv + batch2_samples.tsv → all_samples.tsv
csvstack -t batch1_samples.tsv batch2_samples.tsv > all_samples.tsv
# 如果列不完全一致,用 --filenames 标记来源
csvstack --filenames -t data/part_*.tsv > merged.tsv

3. jq + csvkit 组合拳——一条管道搞定数据清洗#

这是生信中最常见的模式:从 API 拉 JSON → 用 jq 提取 → 转 TSV → csvkit 进一步处理:

Terminal window
# 完整流程:从 Ensembl API 获取基因列表信息,输出为排序好的表格
curl -s "https://rest.ensembl.org/lookup/symbol/homo_sapiens/BRCA1?expand=1" \
-H "Content-Type: application/json" \
| jq -r '{id: .id, name: .display_name, chr: .seq_region_name, start: .start, end: .end} | [.id, .name, .chr, .start, .end] | @tsv' \
| csvsort -t -c 4 | csvlook -t

更复杂的例子——批量查询多个基因:

#!/bin/bash
# 批量查询基因坐标
genes=("BRCA1" "BRCA2" "TP53" "EGFR" "KRAS")
echo -e "gene\tid\tchr\tstart\tend\tstrand"
for gene in "${genes[@]}"; do
curl -s "https://rest.ensembl.org/lookup/symbol/homo_sapiens/${gene}?expand=1" \
-H "Content-Type: application/json" \
| jq -r "[.display_name, .id, .seq_region_name, .start, .end, .strand] | @tsv"
sleep 0.5 # API 限速,每秒不超过 15 次请求
done | csvsort -t -c 3,4 | csvlook -t

4. 理论解释——为什么 jq 的流式处理这么快#

jq 和 csvkit 都基于流式处理,内存占用与输入大小无关:

MmemoryO(1)(流式处理)M_{memory} \approx O(1) \quad \text{(流式处理)}

而 Python pandas 加载整个 DataFrame 的内存复杂度为:

MpandasO(n×m)(全量加载)M_{pandas} \approx O(n \times m) \quad \text{(全量加载)}

对于几十万行的元数据表,pandas 可能吃几个 GB 内存,csvkit 几乎不占内存。缺点是 csvkit 不方便做复杂的数据变换(如标准化),这时候才该切到 Python。

5. 踩坑记录#

坑1:jq 不认带空格的 key。 JSON 的 key 如果包含空格或特殊字符,必须用双引号括起来:.["sample name"] 而不是 .sample name

坑2:csvkit 默认逗号分隔而非 Tab。 生信数据几乎都是 TSV(tab 分隔),每次都要加 -t。可以在 ~/.bashrc 里设置别名:alias csvlook='csvlook -t'

坑3:csvgrep 不支持数值比较。 csvgrep -c age -m ">50" 会按字符串匹配。数值过滤需要用 awkcsvsql(csvkit 的 SQL 查询工具)。

坑4:jq 的 @csv@tsv 输出格式。 @csv 会用引号包裹含逗号的字段,@tsv 不会。如果字段本身含 tab 或换行符,@tsv 可能出问题。用 @csv 更安全但要注意生信工具通常吃 TSV。

坑5:API 限速被忽略。 Ensembl REST API 限制每秒最多 15 次请求。在循环里狂发 curl 会被封 IP。务必加 sleep 0.5 或用 --limit-rate

坑6:csvjoin 默认是 INNER JOIN。 如果你想要 LEFT JOIN(保留左表所有行),需要加 --left。我踩过这个坑,丢了没 QC 数据的样本行。


本文于 2025-09-18 在 Debian 12 上实测。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

jq与csvkit:JSON/CSV数据处理
https://fg.ink/posts/json-csv-processing/
作者
风观
发布于
2025-04-01
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
风观
风有来路,观有所思
分类
标签
站点统计
文章
50
分类
1
标签
29
总字数
61,837
运行时长
0
最后活动
0 天前

文章目录