Net-Base Žurnalas

13.08.2026

JSON in Delphi: Greitas srautinio analizatorius su System.JSON + UTF-8 spąstai dėl specialiųjų simbolių

Jei JSON duomenų apkrovos (payloads) į Delphi ateina tiesiogiai iš srauto, iš „greito parsavimo“ netrunka tapti gamybos problema: didelis atminties poreikis, retkarčiais pasitaikančios parsavimo klaidos ir sugadinti diakritiniai ženklai. Šis praktinis straipsnis parodo, kaip su System.JSON sukurti greitą srautinį analizatorių...

13.08.2026

Nuo žurnalo temos iki projekto įgyvendinimo

Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui

„JSON in Delphi“ skamba kaip išspręsta problema: System.JSON yra prieinamas, REST-Calls grąžina tekstą, viskas. Praktikoje tikros klaidos atsiranda ten, kur JSON nepateikiamas kaip patogus String, o kaip Stream: HTTP-Response-Stream, Datei-Stream, Named Pipe, Message-Queue arba didelis BLOB iš duomenų bazės. Tada susijungia trys kasdieniame darbe dažnai nuvertinami dalykai: atminties elgsena, simbolių kodavimas (ypač UTF-8) ir kraštutiniai atvejai susiję su specialiais simboliais.

Šis straipsnis demonstruoja tvarkingą, greitą požiūrį, kaip parsinti JSON iš TStream nekurstant nereikalingų kopijų — ir svarbiausia be tipinių UTF-8 spąstų, kurių metu umlautai „sugenda“ arba parseriai kartais nutrūksta su kriptinėmis žinutėmis. Dėmesys skiriamas poveikiui eksploatacijai ir sąsajų stabilumui: pakartojamas debuggingas, aiškios metodo ribos ir kriterijai, kada pastangos iš tiesų atsiperka.

Kodėl srautai JSON analizėje Delphi elgiasi kitaip

Kol JSON dokumentas yra mažas, kelias „nuskaityti Stream į String, tada parsinti“ yra patogus. Tačiau nuo tam tikro payload dydžio (įprastai: didelės lentelės, ataskaitos, sinchronizacijos duomenys, žurnalų eksportai) tai tampa brangu:

  • Atminties dublikatai: Jūs skaitote baitus į buferį, konvertuojate juos į Unicode-String (Delphi-String = UTF-16), o parseris viduje sukuria papildomas struktūras. Tai trumpam gali reikšti kelias kopijas.
  • GC/Heap spaudimas: Daugybė laikinų String objektų ir JSON reikšmių didina fragmentaciją ir paskirstymo overhead’ą, ypač ilgai veikiančiuose procesuose (Services, Worker, Import-Jobs).
  • Klaidų vaizdas tampa neaiškus: Jei įskaitant jau padaromos neteisingos koduotės prielaidos, JSON parseris mato tik „keistus simbolius“ arba netikėtus valdymo baitus.

Svarbi aiški atskirtis: JSON formaliai yra Unicode, per tinklą jis beveik visada yra UTF-8. Delphi viduje dirba su UTF-16. Pereinamasis momentas nuo baitų (Stream) prie simbolių (String) yra ta vieta, kur atsiranda problemos su specialiais simboliais — ne pačiame JSON.

System.JSON: ką jis gerai atlieka – ir kur turi būti atsargus

System.JSON yra Delphi standartas DOM pagrįstam JSON: gaunate objektinį modelį (TJSONObject, TJSONArray), galite užklausti reikšmes, iteruoti, serializuoti. Tai tvirtas sprendimas tipinėms verslo integracijoms, bet tai turi dvi pasekmes:

  • Tai nėra tikras streaming-parser: Objektinis modelis yra sukuriamas pilnai. Nors kartais galima apsieiti be papildomo String nuskaitymo, pats DOM lieka atminties intensyvus.
  • Parserio įvestis paprastai yra tekstas: Priklausomai nuo Delphi versijos ir naudojamo API, greitai vėl atsiduriate su String, įskaitant koduotės konvertavimą.

Jei jūsų tikslas yra „greitas Stream-parser“, praktiškai dažniausiai tai reiškia vieną iš dviejų dalykų: (1) vengti nereikalingų kopijų ir (2) užtikrinti kuo ankstyvesnį fail-fast defektiniams payloadams. Abu tikslai yra pasiekiami su System.JSON, jei valdote baitų-į-teksto žingsnį.

UTF-8 spąstai su specialiais simboliais: tipinės priežastys

Abstrakti grafika rodo UTF-8 daugbaitinius simbolius per chunkų ribas ir teisingą dekoderio talpinimą prieš JSON analizę
Chunking nėra problema – jei UTF-8 dekoderis talpina daugbaitines sekas, kurios kertasi tarp dalių.

Jei rezultatiniame tekste umlautai (ä/ö/ü/ß) arba kiti specialieji simboliai rodomi neteisingai (ä, – ir pan.), tai beveik visada yra koduotės neatitikimas. Delphi aplinkoje tokios priežastys pasitaiko ypač dažnai:

1) ANSI-fallback dėl „patogių“ pagalbinių funkcijų

Kai kurie skaitymo keliai tyliai priima sistemos ANSI koduotę (sistema naudojamos kodo puslapio, Codepage des Windows-Systems), jei neperduodama aiški koduotė. Tai paaiškėja tik tada, kai payload’as turi ne tik ASCII. Testo duomenyse tai dažnai atsitiktinai „veikia“, bet produkcijoje griūva su realiais vardais, vietovėmis ir laisvo teksto įrašais.

2) BOM-sumaištis (Byte Order Mark)

UTF-8 gali prasidėti nuo BOM (baitai EF BB BF). Tinklo kontekste BOM gana reta, bet failuose pasitaiko. Kai kurie skaitytuvai atpažįsta BOM ir atitinkamai pritaiko koduotę, kiti — ne arba tik tam tikrais režimais. Jei BOM patenka į eilutę kaip įprastas simbolis, dažnai matysi nematomą „Zero Width No-Break Space“ pradžioje arba JSON-analizatorius užstringa prie pirmojo token’o.

3) Dviguba konvertacija (UTF-8 interpretuojamas „dar kartą“)

Klasikinis klaidos vaizdas „ä“ vietoje „ä“ atsiranda, kai UTF-8 baitai pirmiausia teisingai dekoduojami į Unicode, bet vėliau dar kartą klaidingai interpretuojami kaip ANSI/UTF-8 baitai (ar priešingai). Delphi tai dažnai nutinka, kai neaiškiai konvertuojama tarp TBytes, RawByteString ir string.

4) Apkarpymas vidury daugbaitinio simbolio

UTF-8 koduoja specialiuosius simbolius 2–4 baitais. Jei skaitai dalimis (pvz., 8 KB) ir chunk’o riba patenka vidury simbolio, dekoderis turi tinkamai talpinti likusius baitus. Naivus požiūris, kai kiekvieną chunk’ą atskirai verčiama į eilutę ir vėliau sujungiama, generuoja neteisingas sekas. Tai gali pasirodyti kaip „sporadinė“ klaida, priklausomai nuo paketų ribų, proxy elgsenos ar HTTP-Chunking.

5) Klaidingi spėjimai iš HTTP antraščių

REST atveju šaltinis dažnai deklaruojamas kaip Content-Type: application/json; charset=utf-8. Kai kurie serveriai tačiau negrąžina charset, kai kurie grąžina neteisingą reikšmę. Jei aklai pasiklioviate antrašte, tai gali keistis priklausomai nuo backend versijos. Eksploatacijai ir support’ui naudinga patikrinti faktinį baitų srautą ir gedimo atveju jį loguoti.

Švarus požiūris: Stream → UTF-8-Decoder → JSON-Parser

Atsparioji pipeline susideda iš trijų aiškių etapų:

  1. Baitus skaityti iš srauto (kontroliuotai, prireikus su ribojimu/laikmačiu HTTP-Client).
  2. Dekoduoti į Unicode su aiškiai nurodyta UTF-8 koduote (BOM toleravimas neprivalomas).
  3. Analizuoti (Parsing) su System.JSON į objektinį modelį arba į taikytą išgavimą.

Svarbiausias svertas yra 2 etapas: nenorite, kad kažkur spręstų „numatytoji koduotė“. Delphi kontekste tai reiškia: TEncoding.UTF8 explizit setzen ir nepasitikėti implicitinėmis konvertacijomis.

Was „schnell“ hier konkret bedeutet

Naudodamas System.JSON tu DOM’o „neišoptimizuosi“. Bet gali išvengti:

  • papildomos visos payload kopijos kaip tarpinio string’o, jei tau viduje vis tiek reikia tik kelių reikšmių (tuomet labiau apsimoka kitas parseris; apie tai vėliau),
  • daugkartinio perkodavimo,
  • ir tu gali valdomai nuskaityti labai didelius payload’us (su dydžio limitu ir aiškia klaidos žinute), vietoje to, kad procesas baigtųsi Out-of-Memory arba Access Violations.

Konkreti kraštutinė situacija: specialieji simboliai sugadinti, bet tik kartais

Vienas praktinis kraštutinis atvejis ypač klastingas: payload iš esmės yra galiojantis UTF-8-JSON, bet tu jį skaitai fragmentais (Chunks) ir kiekvieną fragmentą konvertuoji į String. Kol yra tik ASCII simboliai, nieko nepastebėsi. Kai tik umlautas pataiko tiksliai ant fragmento ribos, susidaro negaliojančios UTF-8 sekos. Rezultatas: arba sugadinti simboliai, arba Parse klaida vietoje, kuri neatitinka tikro turinio.

Kaip tai atpažinti:

  • Parse klaidos „atsitiktinai“ pasireiškia prie didelių atsakymų, ne prie mažų.
  • Tas pats request kartais pavyksta, kartais ne (priklausomai nuo Chunking/Transport).
  • Bytų hexdump rodo galiojantį UTF-8, bet tavo užfiksuota String eilutė turi pakaitinius simbolius (�) arba klasikinį Mojibake.

Sprendimas nėra naudoti „daugiau readln“ arba „didesnius Buffer“, o dekoderį, kuris teisingai kaupia daugabačius (Multibyte) sekas per Chunk-grąžas. Būtent čia TStreamReader kartu su UTF-8-encoding gali būti naudingas – jei jį teisingai inicializuoji.

Praktinis vadovas: UTF-8 reprodukuojamai tikrinti in Delphi

Debugging-Setup mit Byte-Dump-Ausdrucken und Arbeitsmaterial, um UTF-8-Bytes und BOM in JSON-Payloads zu prüfen
Norint tvarkingai derinti, pirmiausia svarbus baitų srautas: BOM, sutrumpinimas ir negaliojančios sekos taip greitai matomos.

Prieš pradėdamas kabintis prie parserio, tau reikia derinimo įrankio, kuris padarys matomą faktinę baitų seką. Tai palaikymui ir eksploatavimui yra labai vertinga, nes vėliau galėsi aiškiai pasakyti, ar priešinė pusė siunčia neteisingus duomenis, ar tavo pipeline juos neteisingai dekoduoja.

1) Die ersten Bytes prüfen (BOM, JSON-Start)

Jei JSON ateina su BOM, srauto pradžioje matysi EF BB BF. Tiesiai po to paprastai turėtų sekti „{“ arba „[“. Jei eilutėje jau stovi „“, BOM nebuvo traktuotas kaip BOM, o buvo dekoduotas kaip tekstas.

2) Rohbytes in Hex loggen – aber begrenzt

Nefiksuok visų payload’ų produkcijoje (duomenų apsauga, kaštai, log apimtis). Pasiteisino:

  • Prefix (pvz., pirmi 256 arba 1024 baitai),
  • Suffix (paskutiniai 256 baitai),
  • ir hash (SHA-256) koreliacijai, jei reikia palyginti payload’us.

Tai dažnai leidžia per kelias minutes suskirstyti specialiųjų simbolių problemas: ar baitų seka „ä“ teisinga (C3 A4)? Ar yra sutrumpinimas? Ar pasirodo netikėtas 0x00 (nulio baitas), pvz., dėl klaidingos UTF-16 interpretacijos?

3) Content-Type und Charset mitloggen

Bei HTTP/REST: Žurnaluok Content-Type ir deklaruotą charset. Jei baitai aiškiai yra UTF-8, bet charset teigia ką kitą, kliento neturėtum aklai sekti. JSON atveju UTF-8 yra de-facto standartas. Abiprasmybės atveju: baitų analizė laimi prieš antraštę.

Greitas Stream-Parser su System.JSON: dizainas be nereikalingų kopijų

Scheminis srauto, UTF-8 dekodavimo, dydžio limito ir JSON DOM analizės vamzdynas in Delphi
Aiški pipeline su apribojimu ir eksplicitu UTF-8 aiškiai atskiria transportą, dekodavimą ir parsinimą.

Praktiškas modelis yra: skaitai iš stream į baitų buferį, iš jo sukurti String tik vieną kartą naudojant UTF-8 ir perduodi tą String JSON-analizatoriui. Tai nėra „Streaming“ SAX prasme, bet tai kontroliuojama, efektyvi pipeline be netikėtų koduotės pokyčių.

Svarbu architektūrai: sukurk funkciją taip, kad ji vienoje vietoje centralizuotai nuspręstų:

  • Kokia encoding taisyklė galioja (dažniausiai UTF-8, BOM gali būti toleruojamas)?
  • Koks maksimalus gali būti turinio dydis (DoS apsauga, operacinė riba)?
  • Kokią formą turi klaidų pranešimai (su kontekstu, bet be duomenų nutekėjimo)?

Skaitymo strategija: ribotas buferiavimas vietoje „StreamToString“ be limito

Jei priimi JSON iš išorinių šaltinių (partneriai, mobilūs klientai, trečiųjų šalių tiekėjai), dydžio limitas yra privalomas. Be limito vienas nelaimingas užklausimas gali sukelti paslaugos atminties spaudimą. Praktika reiškia: skaitant skaičiuok baitų sumą ir viršijus ribą nutrauk – su aiškia išimtimi, kuri yra suprantama monitoringui.

Kodėl aš UTF-8 dažnai laukiu „be BOM“, bet BOM toleruočiau

REST-payloaduose BOM pasitaiko retai. Failuose (eksportai, rankinis redagavimas) jis pasitaiko dažniau. Robusčioms importo grandinėms verta BOM toleruoti, bet užfiksuoti žurnale, nes tai gali būti požymis, kad tai „failų pasaulis“, o ne „API pasaulis“.

Tyliosios grėsmės: TStreamReader numatytieji nustatymai ir teksto readerių mišrus naudojimas

TStreamReader yra patogu, bet turi aiškiai kontroliuoti du dalykus:

  • Encoding eksplicitiškai nustatyti: Nesitikėk, kad jis jį „atpažins“.
  • Suprasti buferizavimą: Readeris viduje buferizuoja. Jei tą patį stream vėliau skaitai kitur, pozicija yra reikšminga. Skamba trivialiai, bet didesnėse importo pipeline’ose tai greitai tampa klaidų šaltiniu.

Ypač nepatogu yra mišrus režimas: pirmiausia skaitomas dalis kaip baitai (pvz., žurnalui arba magic-bytes), o paskui tęsiama su TStreamReader. Jei negrąžini stream į teisingą poziciją arba neinicijuoji readerio tinkamoje vietoje, gali skaityti nuo Byte 257 vietoje nuo 0. JSON-analizatorius tada praneš „Invalid character at position …“, nors payloadas iš esmės yra korektiškas.

Kai specialieji ženklai, nors ir UTF-8, lieka „sugedę“: Escaping vs tikrasis Unicode

JSON gali turėti specialiuosius ženklus dviem būdais:

  • Kaip tikri UTF-8 simboliai (pvz., „München“ kaip baitai C3 BC …).
  • Kaip escape sekvencija (pvz. „Mu00fcnchen“).

Abu variantai galioja. Praktikoje svarbu: escape sekvencijos apeina daug transporto problemų, bet jos tik išoriškai užmaskuoja kodoavimo klaidas. Jei jūsų sistema kažkur neteisingai interpretuoja baitus, tai yra eksploatacijos rizika, ne tik kosmetinė klaida. Be to, escape sekvencijos gali klaidinti registravimą/monitoringą, jei komandos tikisi matyti „skaitomą tekstą“.

System.JSON abiem atvejais galiausiai grąžins įprastas Delphi eilutes (UTF-16), jei tik kelias iki jų buvo teisingas.

Realistiškai įvertinkite našumą: DOM-Kaštai, große Arrays ir selektyvumas

Pagrindinė našumo priemonė dažnai nėra „padaryti parserį greitesnį“, o parsuoti mažiau. Su System.JSON tai sudėtinga, nes gaunate DOM. Trys tipinės situacijos:

  • Große Arrays (10.000+ Elemente): DOM konstrukcija kainuoja laiką ir atmintį. Jei jums reikia tik 2 laukų kiekvienam elementui, dažnai prasmingiau naudoti srautinį analizatorių (SAX/Tokenizer), nes System.JSON tam nėra pritaikytas.
  • Pavieniai objektai su daug laukų: Jei reikia tik kelių laukų, vis tiek galite naudoti DOM, bet venkite daugkartinio naršymo. Paimkite reikšmes vieną kartą ir priskirkite jas savo struktūroms.
  • Kelios didelės žinutės (Payload) iš eilės: importo užduotyse arba sinchronizacijos workeriuose verta parsinimą kapsuliuoti į atskirą žingsnį ir po kiekvieno dokumento atlaisvinti visas nuorodas, kad atminties valdytojas galėtų išvalyti. Tai elementaru, bet servisų kontekste dažnai daroma „šalia“.

Todėl „greitas srautinio parserio“ panaudojimas kartu su System.JSON dažnai yra geras kompromisas: efektyvus ir teisingas nuskaitymas, DOM naudojimas sąmoningai, ribų apibrėžimas. Jei jums reikia tikros streaming semantikos (pvz., apdoroti masyvo elementus po vieną, nelaikant visko atmintyje), System.JSON nėra tinkama bazė.

Veikimo patikimumas: klaidų pavyzdžiai ir kaip jas padaryti iškart suprantamomis

JSON analizės klaidos žurnaluose dažnai nėra pakankamai informatyvios, nes nurodo tik poziciją. Eksploatacijai ir palaikymui jums reikia konteksto:

  • Baitų pozicija vs. simbolių pozicija: UTF-8 atveju tai nėra tas pats. Jei analizatorius praneša simbolių poziciją, baitų pozicija gali skirtis. Baitų pozicija yra lemiama, kai darote baitų išrašus (byte-dumps).
  • Ištrauka aplink klaidos vietą: Klaidos atveju žurnaluokite nedidelį langelį aplink poziciją (pvz., 40 simbolių prieš/po), tačiau tik jei nėra jautrios informacijos. Alternatyva: žurnaluokite tik šešioliktainėmis reikšmėmis.
  • Koreliacija: Request-ID, Endpoint, Partner-ID, Payload-Hash. Priešingu atveju tą vienintelę klaidą vėliau surasti bus praktiškai neįmanoma.

Tikslas — kad po gamybinio incidento per kelias minutes galėtumėte atsakyti: „Kodoavimas neteisingai interpretuotas“, „Payload nutrūkęs“, „Serveris grąžina invalid JSON“ arba „turime mapavimo problemą“.

Kaip tikslingai išvengti UTF-8 spąstų: kontrolinis sąrašas

  • Visada nurodykite koduotę: skaitant iš srauto ir rašant į žurnalus/ failus nepasikliaukite numatytaisiais nustatymais.
  • Nekonvertuokite chunk-ų į string: jei skaitote po gabalus (chunked), rinkite baitus arba naudokite dekoderį, kuris buferizuoja multibaitines sekas.
  • BOM tolerantiškumas, bet matomas: priimkite BOM, bet derinimo metu sugebėkite jį aptikti.
  • Nustatykite ribas: maks. Payload-dydis, maks. objekto/massyvo gylis (jei galite tai kontroliuoti), timeout’ai HTTP kliente.
  • Atskiri atsakomybės sluoksniai: „Transporto skaitymas“ ir „JSON analizė“ inkapsuliuoti atskirai. Taip klaidų diagnostika vyksta greičiau ir vėliau galima pakeisti parserį.
  • Kada tikrai apsimoka investuoti į srautinį parserį?

    Nereikia optimizuoti kiekvienos JSON vietos. Šis požiūris paprastai pasiteisina, kai bent vienas iš šių punktų yra teisingas:

    • Dideli payload’ai (keli MB ar daugiau) reguliariai pasitaiko arba gali pasitaikyti.
    • Ilgai trunkantys procesai (service, worker) apdoroja daug payload’ų ir pastebite atminties pikus arba fragmentaciją.
    • Sąveika su heterogeninėmis sistemomis: keli partneriai, skirtingos platformos, kartais neteisingos koduotės.
    • Incidentų istorija: jau buvo „sugadinti umlautai“, sporadinės analizės klaidos arba sunkiai reprodukuojami importo pertrūkiai.

    Jei jūsų payload’ai yra maži ir gaunami iš kontroliuojamo šaltinio, dažnai pakanka paprasto sprendimo – bet net ir tada: aiškiai nustatyti UTF-8 beveik nieko nekainuoja ir užkerta kelią vėlesnėms netikėtoms problemoms.

    Ribos: kada iš tiesų reikia srautinio apdorojimo

    System.JSON yra orientuotas į DOM. Jei duomenis reikia apdoroti „pakeliui“, pvz. didelį masyvą elementą po elemento, jo nelaikant pilnai atmintyje, reikalingas kitoks parserio požiūris (tokenizer/SAX). Tai nėra vertinimas, o architektūrinis sprendimas:

    • DOM (System.JSON): patogu, tinka tipiniams verslo objektams, bet reikalauja daug atminties.
    • Streaming/SAX: mažesnės atminties sąnaudos, tinkama labai dideliems duomenims, bet reikalauja daugiau įgyvendinimo darbo ir kruopštesnio klaidų valdymo.

    Dažnai prasmingas kompromisas yra: tvarkingai įdiegti srautų valdymą, koduotę ir limitus, ir tik po to spręsti, ar DOM vis dar tinka. Daugelyje projektų vien tai reikšmingai stabilizuoja eksploataciją.

    Išvada: JSON in Delphi tampa patikimas, jei koduotę ir srautus tvarkote kaip atskirą sluoksnį

    Dauguma problemų, susijusių su „JSON in Delphi“, nėra susijusios su pačiu JSON parseriu, o su nepastebima dalimi prieš jį: baitai iš srauto paverčiami tekstu. Jei ten aiškiai tvarkysite UTF-8, atpažinsite BOM atvejus, neišmesite chunk ribų ir nustatysite aiškius dydžio limitus, tipinės problemos su specialiaisiais simboliais išnyks – o sporadinės analizės klaidos taps reprodukuojamos.

    System.JSON išlieka pragmatišku standartu: ne pats greičiausias srautinio parseris, bet patikimas, jei kontroliuojate įvestį ir sąmoningai priimate DOM sąnaudas. Jei norite, galime kartu peržiūrėti jūsų konkretų importo-/REST-kelią ir nustatyti vietą, kurioje koduotė arba chunk’inimas žlunga: susisiekite.

    Šiam klausimui taip pat svarbūs JSON srautiniai parseriai. Straipsnis aiškiai išdėsto šiuos aspektus ir nurodo, kam kasdienėje praktikoje verta skirti dėmesį.

    Aptarti projektą ar modernizacijos užmojus su Net-Base.

    Sekantis žingsnis

    Kai iš temos tampa realus projektas, architektūrą, esamą aplinką ir eksploatavimą reikėtų anksti nagrinėti kartu.

    Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.

    • Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
    • REST, duomenų prieiga, portalai ir diegimas nebus atidedami į vėlesnes stadijas.
    • Jūs anksti matote, kuris kelias yra ekonomiškai ir įmonės veiklos požiūriu tvarus.

    Pasidalinti įrašu

    Tiesiogiai pasidalinti šiuo įrašu

    LinkedIn, X, XING, Facebook, WhatsApp ir el. paštas yra iš karto prieinami. Instagramui parengiame nuorodą ir trumpą tekstą nedelsiant.

    El. paštas

    Instagram atidaromas naujame skirtuke. Nuoroda ir trumpas tekstas iš anksto nukopijuojami į iškarpinę.