A-股 一家 私募 的 quant,下午 三点半 收盘 之后 收到 数据团队 的 一条 消息:「今天 沪深300 ETF 的 tick 文件 落到 /data/market-data/cn/equity/tick/20250424/ 了,你 看看 行数 对不对、品种 有没有 缺、总成交额 大概 多少。」她 不打算 写一个 Python 脚本——这种 「看一眼」 的事 用 shell 一句话 更快。这一课 教 的 就是 这种 「一句话」 的 工艺:用 stdin / stdout / stderr 三个 流、用 | 拼小程序、用 grep / sed / awk / jq 做 行 与 字段 上的 工作。
Unix 哲学:流 与 管道
每个 进程 启动 时 都 拿到 三个 流:标准输入(stdin,文件描述符 0)、标准输出(stdout,文件描述符 1)、标准错误(stderr,文件描述符 2)。默认 三者 都连到 终端,但 它们 是 三股 独立 的字节流——这一点 之所以 重要,是 因为 当你 把 cmd1 | cmd2 这样 拼起来 时,cmd1 的 stdout 接到 cmd2 的 stdin;而 cmd1 写到 stderr 的 错误信息 不会 污染 数据流,仍然 落到 终端。
重定向 操作 都 是 shell 在 启动 子进程 之前 做的。基本 形式:cmd > out.txt 覆盖 输出 文件、cmd >> out.txt 追加、cmd < in.txt 把 文件 喂给 stdin、cmd 2> err.log 只 抓 stderr。合并 stderr 进 stdout 然后 一起 重定向 有 三种 形式:
cmd > out.log 2>&1 # correct: stdout 先 重定向,再让 stderr 跟上去
cmd &> out.log # modern shorthand: 等价于上一行,bash 4+ 支持
cmd 2>&1 > out.log # broken: stderr 绑到 原 stdout (终端),stdout 才被重定向到文件
第三行 是 经典 的 「foot-gun」:重定向 是从左到右处理 的,2>&1 此时 stdout 还指向 终端,于是 stderr 绑到 终端;下一步 > out.log 只改 stdout。结果 是 stderr 仍然 打在屏幕上,文件 里 只有 stdout。tee out.log 把 一个 流 同时 写到 文件 和 stdout,调试 时 很 顺手。
管道 cmd1 | cmd2 把 cmd1 的 stdout 接到 cmd2 的 stdin。这是 Unix 写出 「小程序、可组合」 的 关键 设计。
核心 工具箱
读 与 显示:cat file 打印 文件(但 cmd < file 或 cmd file 通常 更好——「useless cat」是 真正 的 反模式);head -n 5、tail -n 5;tail -f file.log 持续 跟 一个 在 写 的 日志;less file 分页 看。
计数:wc -l(行)、wc -c(字节)、wc -w(词)。
排序 与 去重:sort(默认 词典 序)、sort -n(数值)、sort -r(倒序)、sort -k 2,2(按 第二 字段 排)、sort -u(顺手 去 重)。uniq -c 数 相邻 重复——sort | uniq -c 是 经典 直方图 模式(先 排好 才能 用 uniq)。
切 与 拼:cut -d, -f1,3 从 逗号 分隔 文件 里 取 第 1、第 3 列;tr 'A-Z' 'a-z' 翻 字符;paste -d, a.txt b.txt 按列 拼 两个 文件;xargs -n 1 -I {} cmd {} 把 流 里 的 每个 token 当 参数 调一次命令。
直方图 模板——拿 一个 tick 文件 算 每个 ticker 出现 多少 次,前 十 名:
cut -d, -f1 tick_510300_20250424.csv | sort | uniq -c | sort -rn | head -n 10
五段 流水:取 ticker 列、排好、数邻接 重复、按 计数 倒序、取前十。这是 你 接下来 几年 会 写 几百遍 的 模式。
grep:三种 正则
grep 默认 是 基础 正则(BRE),但 你 应该 默认 使用 grep -E 的 扩展 正则(ERE)——+、?、()、{} 在 ERE 里 不用 转义。grep -F 'literal' 是 「字面字符串」,对 形如 [bracket] 的 关键字 既 安全 又 最快。常见 flag:-v 反 取(排除)、-c 数 行 数、-n 给 出 行号、-r 递归 搜目录、-l 只 列 文件 名、-i 大小写 不敏感、-o 只 输出 匹配 部分。
sed 's/old/new/g' 是 80% 的 替换 工作——g 表示 每行 全部 出现。sed -i.bak 's/old/new/g' file 原地 改 文件 并 留 一份 .bak 备份;裸 -i 在 GNU 和 BSD 之间 行为 不同,写脚本 时 显式 给 一个 后缀。更深的 sed 用法(hold space、多行 模式、跳转) 一句话 总结:当 sed 不够用 时,你 实际上 在 写 awk 或 python。
awk:一屏 范围
awk 默认 用 空白 分字段,awk -F, 改成 逗号。$1 是 第一字段,$0 是 整行,NR 是 当前 行号(1-based),NF 是 本行 字段数。BEGIN { ... } 在 第一行 之前 跑,END { ... } 在 最后一行 之后 跑——这两个 块 加上 在 主循环 里 累加 一个 变量,就 能 做 「跨行 状态」。
awk -F, 'NR > 1 {rows++; syms[$1]=1; notional += $4 * $5} END {n=0; for (s in syms) n++; print rows, n, notional}' tick_510300_20250424.csv
NR > 1 跳过 表头;主循环 累加 rows,把 ticker 当 关联数组 的 键 用来 算 distinct count,把 price * volume 加到 notional;END 里 遍历 syms 数 出 不同 品种 数,最后 一行 输出 三个 数。这一行 你 应该 默写。
jq:JSON 行 流
JSONL(JSON Lines)是 量化 数据 落地 的 主流 格式 之一:一行 一个 JSON 对象,每行 同 schema。jq '.field' 取 字段;jq '.[]' 遍历 数组;jq -c 输出 紧凑 JSONL;jq 'select(.price > 100)' 过滤;jq -r 输出 raw 字符串(不带引号);jq -r '[.a, .b] | @csv' 把 字段 投影 成 CSV,跟 后面 的 awk 拼。
book_510300_20250424.jsonl 的 一行 长这样:{"ts":1745467800000,"ticker":"510300","bid":3.84,"ask":3.86,"mid":3.85}。用 jq 走 同样 三个数 的 路:
jq -r '[.ticker, .mid] | @csv' book_510300_20250424.jsonl | awk -F, '{rows++; syms[$1]=1; notional += $2} END {n=0; for (s in syms) n++; print rows, n, notional}'
jq -r 把 每条 snapshot 投影 成 "510300",3.85 一行,awk 处理 跟 之前 同构——把 jq 当成 「JSON → CSV 适配器」,下游 工具 不变。
Python 一行 等价
python -c "import pandas as pd; df = pd.read_csv('tick_510300_20250424.csv'); print(len(df), df['ticker'].nunique(), (df['price'] * df['volume']).sum())"
Python 读 一遍 CSV、求 行数、distinct ticker、price * volume 求和。三种 写法 各 有 用武 之 地:
- 临时 探索 / 半夜 在 终端 排查 — 用 awk 或 jq;启动 快,不离开 shell。
- 定时 落地 任务 — 用 Python;可 测试、可 加 日志、可 加 异常 路径。
- 可 复跑 的 研究 脚本 — 用 Python;要 留 给 下周 的 自己 用。
口诀:三段以内、无状态、临时用:shell;超过 -> Python。一旦 你的 pipeline 长到 四 段、需要 跨行 状态 / 需要 在 cron 里 跑、需要 留给 同事 读,就 把 它 重写 成 一个 Python 脚本(这 正是 第 4 课 的 主题)。
一段 关于 压缩 与 数据库
研究机 上 的 tick 文件 大多 是 .csv.gz。zcat file.csv.gz | awk ... 等价于 把 file 先 解压 再 走 awk;zgrep pattern file.csv.gz 直接 在 压缩 文件 上 搜。数据库 客户端 也 能 喂 进 同一条 pipeline——psql -A -F, -t -c 'SELECT ...' 输出 CSV、流式 进 awk——但 psql 本身 是 第 3 个 模块 的 主题,这里 仅 一句 带过。LC_ALL=C sort 比 locale-aware sort 更快 更 可 预 测,量化 数据 通常 都 这样 跑。
练习
Exercise
给定 文件 tick_file.csv,表头 是 ticker,trade_date,trade_time,price,volume,写出 三 条 命令。(a) 用 grep 和 wc -l 数 出 trade_time 字段 以 0930 开头(开盘 第一分钟)的 行 数。(b) 用 awk -F, 计算 这一分钟 切片 的 总 成交额 sum(price * volume),跳过 表头 行。(c) 用 jq 处理 同一天 的 book_file.jsonl,把 distinct 的 ticker 字段 按 字典 序 排好 输出。最后 验证 (a) 的 行 数 和 jq '.' book_file.jsonl | wc -l 减去 非 开盘 分钟 行后 的 数字 一致。
提示
...,20250424,093015,3.85,1000——grep -E ',0930[0-9]{2},' 卡 trade_time 在第三列;wc -l 数 输出 行数。awk 里 $3 ~ /^0930/ 或者 在 主循环 起手 加 if ($3 ~ /^0930/) 卡同一片 区间。提示
jq -r '.ticker' 一行 一个 ticker;用 sort -u 去重 排序。(price * volume) 在 awk 里 写 $4 * $5,跨行 用 notional += $4 * $5,最后 END { print notional }。下一课预告
到 这一课 为止,你 已经 能 在 终端 里 把 一个 tick 文件 拆开 看。但 一旦 你 想 让 一个 backtest 跑 两天、跑 通宵、能 在 ssh 断线 后 继续 跑——你 就 需要 离开 「一句话」 的 世界,进入 进程 与 作业 控制。下一课 教 你 怎么 用 ps / top / htop 看 进程,用 kill 与 信号 关 进程,用 tmux 让 长 任务 在 ssh 断线 之后 仍然 活着,用 rsync 把 数据 在 机器 之间 拷 来 拷 去。
阅读清单
- 《鸟哥的 Linux 私房菜》第四版,第 11–12 章(正则表达式 与 文件查找、awk 与 sed)。
- GNU
grep/sed/awk中文 手册(社区翻译)。 jq用户手册中文翻译(jqlang.github.io 镜像)。- 一篇 被 A-股 量化社区 广泛 转载 的 「awk 处理 Tushare 导出 CSV」 经典 短文(不指名 来源)。
把 这一课 的 「直方图」 与 「三数 summary」 两个 模板 抄 进 你自己 的 笔记 本——你 会 用 一辈子。
参考卡
本课 出现 的 Fenced ```bash 块:三 段 重定向 形式(correct / modern shorthand / broken)、直方图 五段 流水、纯 awk 三 数 summary、jq + awk 联合 形式、python -c 等价。