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 安装与第一印象
sudo apt install jq -yjq --version# jq-1.7.1生信里 JSON 无处不在:NCBI E-utilities 返回 JSON,Ensembl REST API 返回 JSON,各种工具的配置文件(Nextflow、Snakemake 的参数)也是 JSON。直接 cat 看是灾难,jq 是解药。
# 美化输出curl -s "https://rest.ensembl.org/lookup/id/ENSG00000139618?expand=1" \ -H "Content-Type: application/json" | jq '.'# 彩色、缩进、一目了然1.2 基础过滤——点号路径访问
jq 的过滤表达式就是它的核心语法:
# 假设有一个 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.json1.3 数据转换——从 JSON 到 TSV
# 提取多个字段并输出为 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.json1.4 实战:处理 Ensembl REST API 返回
# 查询基因 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 数组和对象操作
# 统计数组长度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.json1.6 处理嵌套 JSON——生信最常见难题
NCBI E-utilities 返回的 JSON 往往嵌套很深:
# 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 变量和函数
# 使用变量jq --arg cond "treated" '.[] | select(.condition == $cond)' samples.json
# 自定义函数jq 'def mb: . / 1000000; .[] | {sample: .sample, reads_m: (.reads | mb)}' samples.json2. csvkit——命令行里的 Excel
2.1 安装
# 通过 pip 安装(推荐在 conda 环境里)pip install csvkit# 或用 condaconda install -c conda-forge csvkitcsvkit 是一套工具集,核心命令:
| 命令 | 功能 | 类比 Excel |
|---|---|---|
csvlook | 格式化查看 CSV | 打开文件看 |
csvcut | 选择列 | 删除/隐藏列 |
csvgrep | 按模式过滤行 | 筛选器 |
csvsort | 排序 | 排序功能 |
csvstat | 统计摘要 | 描述性统计 |
csvjoin | 关联两个表 | VLOOKUP |
csvstack | 纵向合并多个表 | 追加行 |
in2csv | 转换 Excel 到 CSV | 另存为 |
2.2 快速查看——csvlook
# 用 csvlook 美美地看 TSV/CSVcat clinical_data.tsv | csvlook -t# -t 指定 tab 分隔
# 只显示前10行(和 | head 类似但保留表头)csvlook -t clinical_data.tsv | head -11csvlook 的输出带 Markdown 风格的表格线,很适合粘贴到实验记录里。
2.3 列操作——csvcut
# 查看所有列名(生信里元数据表经常几十列)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
# 筛选 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' | csvlook2.5 排序——csvsort
# 按 reads 数量降序排列csvsort -t -c reads_count -r sample_stats.tsv | csvlook# -r = reverse(降序)
# 多列排序:先按condition,再按agecsvsort -t -c condition,age sample_stats.tsv | csvlook2.6 统计分析——csvstat
# 一键生成各列统计摘要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
# 类似 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_id2.8 合并多个文件——csvstack
# 纵向拼接(行追加)# batch1_samples.tsv + batch2_samples.tsv → all_samples.tsvcsvstack -t batch1_samples.tsv batch2_samples.tsv > all_samples.tsv
# 如果列不完全一致,用 --filenames 标记来源csvstack --filenames -t data/part_*.tsv > merged.tsv3. jq + csvkit 组合拳——一条管道搞定数据清洗
这是生信中最常见的模式:从 API 拉 JSON → 用 jq 提取 → 转 TSV → csvkit 进一步处理:
# 完整流程:从 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 -t4. 理论解释——为什么 jq 的流式处理这么快
jq 和 csvkit 都基于流式处理,内存占用与输入大小无关:
而 Python pandas 加载整个 DataFrame 的内存复杂度为:
对于几十万行的元数据表,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" 会按字符串匹配。数值过滤需要用 awk 或 csvsql(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 上实测。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!