# Python 读取 jsonl 的最佳实践 (December 11, 2024)

发现一个有趣的结论。

在 Python 中读取 jsonl 文件，读取为一个 `list[dict]`，最佳实践是怎么样？

刚学 python 的人会这么写：

```python
def read_jsonl(jsonl_path: str):
    content = Path(jsonl_path).read_text()
    results = []
    for i in content.splitlines():
        results.append(json.loads(i))
    return results
```

这个人写的已经够好了，如果是学业不精的人还能写出更千奇百怪的写法，比如用 while 找`\n`，或者用 range 来迭代，然后用索引来访问一个个元素什么的。如果我是老师，我觉得上面这个已经可以打 60 分了。加上它用 Path 而不是 open 我觉得可以额外加 10 分。

但熟悉 micro optimization 的人一眼就能看出各种问题，比如存在不应该的数据复制、列表不如迭代器、append 和 json.loads 反复地对象取属性，优化之后大概长这样：

```python
def read_jsonl(jsonl_path: str):
    with open(jsonl_path) as f:
        return list(map(json.loads, f))
```

这样写感觉很 pythonic，比如用到了 python 中的上下文管理器，用到了 file 对象是可以按行迭代的迭代器，用到了 map 函数式编程等特性。可读性也 up up up

如果一个学生写到这种程度，我觉得已经可以打 100 分了，甚至还有点炫技的嫌疑，我可能会让 Ta 上台来给大家讲一讲 哈哈哈

### 但是！
这不是最快的办法。假设你只是读一个几 MB 大的 jsonl 数据文件，不关心内存（几 MB 也没啥好优化内存的），这样是最快的

```python
def read_jsonl(path: str):
    content = Path(jsonl_path).read_text().rstrip().replace("\n", ",")
    return json.loads(f"[{content}]")
```

看懂了吗？是不是很 hack，这其实是把整个 jsonl 用字符串替换的方式变成一个 json 列表，然后直接一次性 loads

### 快多少
在我的电脑上，一个 3MB 的 jsonl 文件，上面两种办法分别是 49ms 和 42ms，后面这种 hack 是 19ms

可以认为是从 3 帧的卡顿变成了 1 帧，这个优化效果还是非常惊艳的

### 为什么
Python 性能之禅，能避免用 python 就避免用 python（比如避免循环），即使这会让你的代码更绕弯路

---

本故事源于我前天开的一个 [PR](https://github.com/fake-useragent/fake-useragent/pull/413)：

<img src="https://opengraph.githubassets.com/1be1e131e658c5059fcc030df693d3c99a450e6d0632394a6fc09a6d3cde97d3/fake-useragent/fake-useragent/pull/413" width="1200" title="null" crop="0,0,1,1" id="NU12I" class="ne-image">

---

### 思考题
如果是大文件呢？比如 GB 级的，肯定不能一次性读到内存中的

#### 参考答案
我觉得得分类讨论，如果是每行都很大的，比如一行都是 MB 级的，那可能可以一行一行做：

```python
def iter_jsonl_data(jsonl_path: str):
    with open(jsonl_path) as f:
        yield from map(json.loads, f)
```

但如果是行数很多，但每行都不大的情况（比如日志流），那我觉得就麻烦了，可能得分 chunk，然后每个 chunk 内用上面第三种方法来 hack

当然，这些方法都是戴着镣铐跳舞，如果条件允许，最好的办法当然是拿低级语言写个 pyd import 进来，效率绝对比这些都快

这个故事还是教学意义比实践意义大