JSON Toolkit

Jujukan Teks JSON (RFC 7464): JSON Penstriman Berdelimit RS

Inti pati: RFC 7464 menggunakan pemisah 0x1E untuk menstrim teks JSON yang boleh anda baca dan tulis sekali gus. Meliputi peraturan, cara ia berbeza daripada NDJSON, dan pelaksanaan pisah-kemudian-huraikan.

Pertengahan~9 min bacaanDiterbitkan 2026-10-31Dikemas kini 2026-10-31
Kulit Jujukan Teks JSON dan pemisah rekod RFC 7464

Kesimpulan utama: gunakan JSON-seq apabila anda mengawal kedua-dua hujung, memerlukan jenis media berdaftar, dan mahu pembaca menyegerak semula selepas pemotongan; gunakan NDJSON apabila data menyuap sistem log, mesti boleh dibaca manusia, atau perlu berfungsi dengan alat sedia ada — jangan sekali-kali memaksa JSON-seq pada rakan yang hanya memahami input berdelimit baris.

NDJSON memisahkan rekod pada baris baru, tetapi baris baru ialah konvensyen manusia. RFC 7464 menulis sempadan ke dalam strim bait itu sendiri: 0x1E yang tidak kelihatan sebelum setiap rekod.

1. Masalah yang diselesaikan oleh satu bait

RFC 7464 diterbitkan pada Februari 2015 dengan tajuk JSON Text Sequences, dan ia mendaftarkan jenis media application/json-seq. Ia tidak menambah jenis data baharu. Ia hanya mentakrifkan bagaimana jujukan teks JSON disusun dalam satu strim bait. Keseluruhan peraturan muat dalam satu ayat: letakkan satu RS sebelum setiap teks JSON dan satu LF selepasnya.

  • RS ialah pemisah rekod ASCII, titik kod 0x1E, dikenali dalam Unicode sebagai INFORMATION SEPARATOR TWO (U+001E);
  • LF ialah suapan baris, 0x0A.

Dua rekod kelihatan seperti ini pada peringkat bait:

1E 7B 22 69 64 22 3A 31 7D 0A      ← 0x1E {"id":1} + LF
1E 7B 22 69 64 22 3A 32 7D 0A      ← 0x1E {"id":2} + LF

RS terletak sebelum rekod, yang bertentangan dengan susun atur NDJSON "tulis kandungan, kemudian tulis pemisah". Arah itu mengubah tingkah laku pemulihan: pembaca yang melihat 0x1E tahu rekod sebelumnya telah berakhir, jadi walaupun rekod yang dipotong di tengah-tengah penulisan tidak boleh mengaburkan sempadan rekod seterusnya.

Ini bukan reka bentuk teori. RFC secara eksplisit menyebut rekod yang ditambah ke fail log yang dipotong oleh sistem fail selepas ranap atau pembunuhan pentadbiran, dan ia menyatakan penghurai tidak sepatutnya membatalkan keseluruhan bacaan kerana itu.

2. Dua set peraturan: pengekod dan penghurai

RFC 7464 luar biasa kerana menyediakan dua tatabahasa ABNF, satu yang mengekang penulis dan satu yang melonggarkan pembaca.

Peranan Tatabahasa Maksud
Pengekod *(RS JSON-text LF) Satu RS sebelum setiap teks JSON, satu LF selepasnya
Penghurai *(1*RS possible-JSON) Bait RS berturutan dibenarkan, dan LF antara rekod adalah pilihan

Dalam istilah kejuruteraan, terdapat lima peraturan keras:

  1. UTF-8 adalah wajib; UTF-16 dan UTF-32 tidak boleh digunakan;
  2. Penghurai menganggap "apa-apa bait selain RS" sebagai teks JSON yang mungkin dan cuba menghuraikannya; pada kegagalan ia melangkau ke RS seterusnya dan teruskan;
  3. Bait RS berturutan tidak mewakili elemen kosong dan diabaikan;
  4. Tiada penanda penghujung jujukan, jadi strim hanya berakhir;
  5. Apabila nilai peringkat atas ialah nombor, true, false atau null, rekod mesti berakhir dengan bait ruang putih, jika tidak ia digugurkan sebagai mungkin dipotong.

Peraturan kelima ialah yang orang lupa. Objek, tatasusunan dan rentetan adalah berpemisah sendiri dalam JSON; nombor tidak. <RS>123<RS> tidak boleh memberitahu anda sama ada ia mengandungi 123 yang lengkap atau 1234 yang dipotong. Oleh itu RFC mewajibkan penulis menambah LF, menggunakannya sebagai bait kenari untuk pengesanan pemotongan. Begitu juga <RS>true<RS> mungkin pada asalnya trueish.

3. Memilih antara JSON-seq dan NDJSON

Kedua-dua format menstrim, tetapi mereka menyasarkan masalah berbeza:

Dimensi Jujukan Teks JSON NDJSON / JSON Lines
Kedudukan pemisah RS (0x1E) sebelum rekod LF (0x0A) selepas rekod
Jenis media application/json-seq, berdaftar IANA Tiada pendaftaran rasmi, konvensyen komuniti
Kebolehan dibaca bait Aksara kawalan tidak kelihatan, cat tidak menunjukkan apa-apa Teks biasa, terus boleh dibaca
Sisipan selari Pembaca boleh menyegerak semula dan menggugurkan hujung yang dipotong Sisipan juga berfungsi, baris rosak dilangkau
Nombor peringkat atas Memerlukan bait ruang putih di hujung untuk dipercayai Suapan baris ialah sempadan
Pemisah di dalam rentetan RS hanya boleh muncul sebagai \u001e LF mesti ditulis sebagai \n
Perkakasan jq --seq dan beberapa pustaka Hampir setiap alat log dan data besar
Sambungan fail Tidak ditakrifkan oleh RFC .jsonl / .ndjson

Keputusannya ringkas: gunakan JSON-seq apabila anda mengawal kedua-dua hujung, mahukan jenis media formal, dan perlu pembaca menyegerak semula selepas pemotongan; gunakan JSON Lines dan NDJSON apabila data menyuap sistem log, perlu dibaca manusia, atau mesti berfungsi dengan alat sedia ada.

4. Penghurai: pisah pada 0x1E, kemudian JSON.parse

Manfaat paling nyata RFC 7464 ialah anda tidak memerlukan penghurai JSON penstriman. Pisahkan mengikut bait, kemudian panggil JSON.parse pada setiap segmen:

/** Pemisah rekod ASCII, ditakrifkan oleh RFC 7464 sebagai 0x1E */
const RECORD_SEPARATOR = 0x1e;

/**
 * Menghuraikan satu ketulan bait Jujukan Teks JSON.
 * @param buffer Bait yang telah dibaca setakat ini
 * @returns Rekod yang berjaya dihuraikan dan bilangan rekod yang gagal
 */
function parseJsonSequence(buffer: Buffer): { records: unknown[]; badCount: number } {
  const records: unknown[] = [];
  let badCount = 0;
  let recordStart = buffer.indexOf(RECORD_SEPARATOR);

  while (recordStart !== -1) {
    const nextRecordStart = buffer.indexOf(RECORD_SEPARATOR, recordStart + 1);
    const recordEnd = nextRecordStart === -1 ? buffer.length : nextRecordStart;
    const rawText = buffer.subarray(recordStart + 1, recordEnd).toString('utf8').trim();

    if (rawText !== '') {
      try {
        records.push(JSON.parse(rawText));
      } catch {
        badCount += 1;
      }
    }

    recordStart = nextRecordStart;
  }

  return { records, badCount };
}

Tiga nota pelaksanaan:

  • Pemisahan pada peringkat bait adalah selamat. Bait sambungan UTF-8 berada dalam julat 0x80–0xBF, jadi ia tidak pernah boleh sama dengan 0x1E. Memisahkan pada 0x1E tidak boleh memotong aksara berbilang bait separuh.
  • Penampan hujung semasa menstrim. Semasa membaca dalam ketulan, segmen akhir mungkin belum sampai ke RS seterusnya. Kekalkannya dalam penampan sehingga ketulan seterusnya tiba; jika tidak anda akan menghuraikan separuh rekod yang sama dua kali.
  • Kira kegagalan dan bukannya membatalkan. RFC memberitahu penghurai untuk melangkau ke RS seterusnya dan teruskan, jadi badCount ialah metrik amaran, bukan pengecualian. Rekod akhir yang dipotong juga tergolong dalam baldi itu.

Penggunaan memori bergantung pada rekod tunggal terbesar tambah satu penampan, bukan saiz keseluruhan fail. Itulah idea yang sama yang dibincangkan dalam penghuraian dan penstriman JSON besar.

5. Aksara kawalan di dalam rentetan

JSON mewajibkan setiap aksara kawalan dari U+0000 hingga U+001F dilepaskan di dalam rentetan, dan RS tergolong dalam julat itu. Oleh itu rentetan JSON yang sah hanya boleh mengandungi lima aksara \u001e, tidak pernah bait 0x1E mentah. Itulah sebabnya memisahkan pada 0x1E sentiasa selamat untuk data yang sah.

Bahaya terletak di pihak penulis:

  1. Membina teks JSON dengan tangan menggunakan '\x1e', memasukkan aksara kawalan mentah ke dalam rentetan;
  2. Membina JSON dengan literal templat dan bukannya JSON.stringify, meninggalkan baris baru, tanda petik dan aksara kawalan tanpa dilepaskan;
  3. Menyalin teks yang mengandungi RS daripada terminal atau editor, di mana aksara kawalan ditelan atau diganti.

Corak yang betul sentiasa menjana teks dahulu, kemudian melampirkan pemisah:

const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;

JSON.stringify membebaskan aksara kawalan sebagai \u00XX, jadi RS mentah tidak pernah boleh muncul di dalam rekod. Sebaik sahaja langkah ini betul, logik pemisahan hanya perlu mengendalikan bait dan tidak pernah perlu memahami sintaks JSON.

Kebanyakan kegagalan dalam kategori ini muncul sebagai JSON.parse melontar Unexpected token, terutamanya kes mengelirukan di mana "aksara pertama ialah bait kawalan yang tidak kelihatan". Jadual rujukan mesej ralat JSON membincangkan kelas ralat itu baris demi baris.

6. Pengangkutan HTTP dan jenis media

application/json-seq ialah jenis media berdaftar tanpa parameter wajib atau pilihan, dan set aksara ditetapkan kepada UTF-8. Pada HTTP, tiga amalan penting:

  • Hantar Accept: application/json-seq dan balas dengan Content-Type: application/json-seq;
  • Untuk menstrim secara sebenar, pelayan mesti menggunakan pemindahan berketul dan melumpuhkan penampan respons dalam perantara seperti proksi songsang, CDN dan alat ujian beban;
  • Pelanggan yang membaca ketulan demi ketulan mungkin menerima ketulan yang berakhir di tengah-tengah rekod, jadi penghurai mesti menampan sehingga RS seterusnya sebelum mengeluarkan rekod.

Jika pihak lain hanya memahami input berdelimit baris, jangan paksa JSON-seq padanya. Ia standard, tetapi penembusan ekosistemnya jauh di bawah NDJSON, jadi keserasian harus diputuskan semasa rundingan protokol dan bukannya selepas kegagalan penghuraian.

7. Baris arahan, log dan sokongan alat

jq ialah salah satu daripada beberapa alat dengan sokongan asli; bendera --seq-nya wujud tepat untuk RFC 7464:

printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'

Itu mencetak 1 dan 2 pada baris berasingan. Untuk titik akhir penstriman, ingat untuk melumpuhkan penampan curl:

curl -sN https://api.example.com/events | jq --seq -c '.type'

jq --seq membaca input RFC 7464, mengabaikan rekod rosak, dan terus menghuraikan. Itu sangat berguna semasa mengekori fail log yang masih ditulis: rekod terakhir mungkin separuh ditulis, dan output tidak akan menunjukkan objek akhir palsu.

7.1 Sokongan asli mengikut persekitaran

Persekitaran Sokongan terbina dalam Nota
jq --seq Pilihan baris arahan paling mudah
Node.js Tiada Buffer.indexOf ditambah JSON.parse
Python Tiada Pisah pada b'\x1e'
Go Tiada SplitFunc tersuai untuk bufio.Scanner
Pelayar Tiada fetch ditambah ReadableStream, pisahkan bait secara manual

7.2 Perangkap biasa

Kesilapan berkumpul di pihak pembacaan:

  1. Membaca dengan readline, yang meninggalkan awalan RS tidak kelihatan pada baris pertama sehingga JSON.parse sentiasa gagal;
  2. Membuka fail .json-seq dalam editor di mana aksara kawalan tidak kelihatan, jadi ia kelihatan seperti satu dokumen JSON pelik;
  3. Memisahkan dengan split(/\n/), yang mengelirukan baris baru yang dilepaskan di dalam rekod dengan LF antara rekod;
  4. Menganggap RS sebagai aksara biasa dalam dokumentasi, jadi ia hilang selepas salin dan tampal.

8. Bila ia berbaloi digunakan

Sesuai:

  • Anda memiliki kedua-dua pengeluar dan pengguna, tanpa alat pihak ketiga di antaranya;
  • Data ialah strim peristiwa yang ditambah berterusan dan proses mungkin ranap, jadi anda memerlukan pemulihan daripada pemotongan;
  • Protokol itu layak mendapat jenis media formal dan bukannya konvensyen seperti .jsonl;
  • Rekod individu mungkin nombor atau boolean peringkat atas, di mana peraturan pemotongan eksplisit penting.

Kurang sesuai:

  • Log mesti masuk ke ELK, Loki atau sistem lain yang hanya memahami input berdelimit baris;
  • Orang ramai perlu cat, grep dan awk data secara terus;
  • Pihak lain hanya boleh menghuraikan application/json.

9. Senarai semak

  • [ ] Penulis menjana rekod dengan JSON.stringify, kemudian menambah 0x1E di hadapan dan \n di hujung;
  • [ ] Rekod yang mengandungi nombor, boolean atau null peringkat atas berakhir dengan LF;
  • [ ] Pembaca memisahkan pada bait dan bukannya membaca baris demi baris;
  • [ ] Bacaan penstriman menyimpan hujung yang belum tertutup dalam penampan sehingga RS seterusnya tiba;
  • [ ] Kegagalan penghuraian dikira dan dilangkau, tidak pernah mematikan jujukan;
  • [ ] Antara muka awam mengisytiharkan application/json-seq secara eksplisit dan mengekalkan sandaran NDJSON.

Soalan Lazim

Patutkah saya menggunakan JSON-seq untuk titik akhir API awam?

Hanya apabila kedua-dua hujung boleh diubah atau format dirundingkan lebih awal. JSON-seq ialah standard berdaftar dengan jenis media application/json-seq, tetapi penembusan ekosistemnya jauh di bawah NDJSON, jadi memaksanya pada pelanggan pihak ketiga biasanya gagal. Putuskan keserasian semasa rundingan protokol dan kekalkan sandaran NDJSON.

Patutkah saya menukar saluran NDJSON sedia ada kepada JSON-seq?

Biasanya tidak. NDJSON sudah menstrim, menambah dan melangkau baris rosak, dan hampir setiap alat log dan data besar menerimanya. Tukar hanya apabila anda memerlukan jenis media berdaftar IANA atau penyegerakan semula yang diberikan oleh pemisah di hadapan, dan hanya apabila pengeluar serta pengguna boleh diubah bersama-sama.

Mana lebih biasa, Jujukan Teks JSON atau NDJSON?

NDJSON. Peraturannya lebih ringkas, liputan alatnya jauh lebih luas, dan sistem log serta saluran data besar umumnya menerimanya. JSON-seq menang pada jenis media berdaftar IANA dan pada penyegerakan semula yang datang daripada meletakkan pemisah di hadapan, yang sesuai untuk protokol penstriman terkawal dan bukannya pertukaran data umum.

Mengapa pemisah diletakkan sebelum rekod dan bukannya selepasnya?

Kerana rekod boleh dipotong. Dengan pemisah di hadapan, pembaca yang melihat RS tahu rekod baharu telah bermula, jadi rekod sebelumnya tidak boleh merosakkan yang seterusnya sama ada ia lengkap atau tidak. Dengan pemisah di hujung, penulisan yang terganggu meninggalkan segmen akhir yang tiada penanda penghujung dan tiada penanda permulaan berikut, jadi sempadan hanya boleh diteka.

Apa yang berlaku jika 0x1E muncul di dalam rekod?

Rentetan JSON yang sah mesti melepaskan RS sebagai \u001e, jadi 0x1E mentah tidak sepatutnya muncul. Jika ia muncul, teks itu tidak lagi JSON yang sah: memisahkan pada 0x1E akan terputus di tempat yang salah dan JSON.parse akan gagal. Pembaikan tergolong di pihak penulis: gunakan JSON.stringify dan bukannya teks yang dibina dengan tangan.

Bolehkah pelayar menghuraikan application/json-seq secara terus?

Tidak. Pelayar tidak menyertakan penghurai JSON-seq; fetch hanya menyampaikan bait. Anda perlu menggunakan response.body.getReader(), mengumpul ketulan sehingga 0x1E seterusnya, dan memanggil JSON.parse pada setiap segmen. Untuk muatan kecil, membaca keseluruhan badan sebagai rentetan dahulu dan memisahkannya selepas itu juga berfungsi.

Mengapa nombor peringkat atas memerlukan LF di hujung?

Objek, tatasusunan dan rentetan berpemisah sendiri dalam JSON, manakala nombor tidak: <RS>123<RS> mungkin 123 yang lengkap atau 1234 yang dipotong. RFC 7464 oleh itu mewajibkan satu bait ruang putih selepas setiap rekod, dan LF bertindak sebagai kenari untuk pengesanan pemotongan. Peraturan yang sama terpakai kepada true, false dan null peringkat atas.