Excel o'qish: bitta iflos katak butun faylni to'xtatadi
Kod yozishdan oldin — o'lchash
Ikkinchi qism bitta qoida haqida: biror narsani qurishdan oldin, uning eng xavfli joyini o'lchab ko'r. Bunday kichik, tashlab yuboriladigan tajribani spike deymiz. Spike ning maqsadi — mahsulot emas, javob. Uning kodi keyin o'chirilishi ham mumkin. Uning raqamlari esa qoladi va qarorlarni boshqaradi.
Tezda birinchi xavfli joy — eng birinchi qadam: faylni ochish. Agar Tez tahlilchining faylini ochishda qotsa yoki yiqilsa, qolgan hamma narsa — model, grammar, engine — ahamiyatsiz.
Spike 1 ning savoli:
Qaysi reader (o'quvchi) katta Excel faylni eng tez va eng kam xotira bilan ochadi, va qaysi biri eski
.xlsformatini o'qiy oladi?
Bu bobdagi hamma raqamlar shu kitob yig'ilayotgan paytda spike ning o'z kodi bilan qayta
o'lchanadi. Kod Tez papkasida turibdi: spikes/spike1_excel_read.
Sinov fayli
Bank faylini ishlata olmaymiz — demak bank fayliga o'xshash soxta fayl yasaymiz. Spike 1 ning
generatori ustunlarni spec.json dagi tavsifdan yasaydi. Uning ichida ataylab haqiqiy hayotdagi
iflosliklar bor:
import json SPIKE = Path("/mnt/c/Users/Admin/Desktop/Offline/spikes/spike1_excel_read") spec = json.load(open(SPIKE / "fixture/spec.json", encoding="utf-8")) for ustun in spec["columns"]: tafsil = {k: v for k, v in ustun.items() if k not in ("name", "kind")} print(f"{ustun['name']:16s} {ustun['kind']:9s} {json.dumps(tafsil, ensure_ascii=False)[:95]}")
tranzaksiya_id sequence {"start": 1000001}
sana date {"start": "2025-01-01", "end": "2026-09-30", "formats": ["%d.%m.%Y", "%Y-%m-%d", "%d/%m/%Y"]}
filial choice {"values": ["Toshkent", "Samarqand", "Buxoro", "Andijon", "Namangan", "Farg'ona", "Qarshi", "Nu
mijoz_id integer {"min": 100000, "max": 999999}
mijoz_ismi choice {"values": ["Aliyev Jasur", "Karimova Dilnoza", "Rahimov Bobur", "Yusupova Madina", "Tursunov S
hisob_raqami digits {"length": 20}
operatsiya_turi choice {"values": ["Kirim", "Chiqim", "O'tkazma", "Kredit to'lovi", "Komissiya"], "weights": [30, 30,
valyuta choice {"values": ["UZS", "USD", "EUR", "RUB"], "weights": [80, 12, 5, 3]}
summa amount {"min": 1000, "max": 50000000, "decimals": 2, "text_ratio": 0.05, "text_thousands": " ", "text_
komissiya amount {"min": 0, "max": 50000, "decimals": 2, "null_ratio": 0.1}
kanal choice {"values": ["Filial", "Mobil ilova", "Bankomat", "Internet bank"]}
xodim_id integer {"min": 100, "max": 999, "null_ratio": 0.2}
holat choice {"values": ["Bajarildi", "Bekor qilindi", "Kutilmoqda"], "weights": [90, 6, 4]}
izoh choice {"values": ["", "Oylik maosh", "Ijara to'lovi", "Kommunal xizmatlar", "Qarz qaytarish", "Возвра
Bir nechtasiga e'tibor bering:
sana— uch xil formatda:16.05.2025,2025-05-16,16/05/2025. Bitta ustunda.filial— "Toshkent", "Тошкент", "Ташкент" va "Toshkent " (oxirida bo'sh joy). Bitta shahar, to'rt yozilish.summa— besh foiz qiymat matn sifatida yozilgan:19 113 274,33. Bo'sh joy bilan ajratilgan minglar, vergul bilan kasr. Bu Rossiya va O'zbekistondagi ko'p tizimlar eksport qiladigan shakl.hisob_raqami— 20 xonali raqam. Ba'zilari nol bilan boshlanadi.
Endi faylni yasaymiz: 400 000 qatorli .xlsx va xuddi shu ma'lumot .csv da, hamda eski
format uchun 65 535 qatorli .xls (eski formatda bundan ko'p qator sig'maydi):
SPIKE=/mnt/c/Users/Admin/Desktop/Offline/spikes/spike1_excel_read export PYTHONDONTWRITEBYTECODE=1 .venv/bin/python $SPIKE/fixture/generate.py --spec $SPIKE/fixture/spec.json --rows 400000 --formats xlsx csv --out-dir . --name medium .venv/bin/python $SPIKE/fixture/generate.py --spec $SPIKE/fixture/spec.json --rows 65535 --formats xls --out-dir . --name legacy
built 400000 rows x 14 columns in 3.7s wrote medium.xlsx: 37.3 MB in 14.2s wrote medium.csv: 57.2 MB in 3.0s built 65535 rows x 14 columns in 0.7s wrote legacy.xls: 15.4 MB in 1.7s
To'rt o'quvchi
Sinovga Excel uchun to'rtta o'quvchi chiqdi, va CSV uchun bitta taqqoslash o'lchovi:
| O'quvchi | Nima |
|---|---|
duckdb_xlsx |
DuckDB ning read_xlsx funksiyasi, ustun turlarini o'zi aniqlaydi |
duckdb_xlsx_text |
Xuddi shu, lekin hamma ustunni matn sifatida o'qiydi (all_varchar) |
fastexcel |
Rust da yozilgan calamine kutubxonasi ustidagi Python o'rami |
openpyxl |
Pythonda eng mashhur Excel kutubxonasi — taqqoslash uchun asos |
duckdb_csv |
CSV uchun DuckDB ning read_csv — "eng tez nima bo'lishi mumkin" degan o'lchov |
Har bir o'quvchi alohida process (jarayon) da ishga tushadi — xotira o'lchovi boshqasiga aralashmasin. Har biri uch marta, mediana yoziladi. Xotira — butun jarayonning eng yuqori nuqtasi, Python va DuckDB bilan birga:
SPIKE=/mnt/c/Users/Admin/Desktop/Offline/spikes/spike1_excel_read export PYTHONDONTWRITEBYTECODE=1 .venv/bin/python $SPIKE/bench/run.py --files medium.xlsx medium.csv legacy.xls --runs 3 --watch sana summa hisob_raqami komissiya
| File | Reader | Threads | Rows | Seconds (median) | Peak MiB (median) | Watched types | |---|---|---|---|---|---|---| | medium.xlsx | duckdb_xlsx | all | error: _duckdb.InvalidInputException: Invalid Input Error: read_xlsx: Failed to parse cell 'I9': Could not convert string '19 113 274,33' to DOUBLE | | | | | medium.xlsx | duckdb_xlsx_text | all | 400,000 | 2.68 | 193 | sana=VARCHAR, summa=VARCHAR, hisob_raqami=VARCHAR, komissiya=VARCHAR | | medium.xlsx | fastexcel | all | 400,000 | 3.70 | 586 | sana=VARCHAR, summa=VARCHAR, hisob_raqami=VARCHAR, komissiya=DOUBLE | | medium.xlsx | openpyxl | all | 400,000 | 29.96 | 691 | sana=VARCHAR, summa=VARCHAR, hisob_raqami=VARCHAR, komissiya=DOUBLE | | medium.csv | duckdb_csv | all | 400,000 | 0.33 | 301 | sana=VARCHAR, summa=VARCHAR, hisob_raqami=VARCHAR, komissiya=DOUBLE | | legacy.xls | fastexcel | all | 65,535 | 0.21 | 218 | sana=VARCHAR, summa=VARCHAR, hisob_raqami=VARCHAR, komissiya=DOUBLE |
Jadvalni o'qiymiz.
Birinchi qator — yiqilish. DuckDB ning odatiy read_xlsx funksiyasi faylni umuman o'qimadi.
Sabab — bitta katak: summa ustunida birinchi qatorlar son edi, DuckDB ustunni son deb qaror
qildi, keyin birinchi matnli summaga — 19 113 274,33 ga — duch keldi va butun import
to'xtadi. 400 000 qatordan bittasi tufayli.
Matn rejimida xuddi shu funksiya hamma narsani o'qidi — tez va eng kam xotira bilan. Lekin
narxi bor: hamma ustun matn, hatto toza komissiya ham.
fastexcel ham shu darajada — bir necha soniya. Farqi — u aqlliroq: aralash ustunni (summa) matn qildi, toza
sonli ustunni (komissiya) esa son qoldirdi. Lekin xotirani ikki barobardan ko'proq oladi.
openpyxl — o'n barobar sekinroq. U Python da katakma-katak o'qiydi; boshqalar esa Rust yoki C++ da.
CSV — hammasidan tez. Bu kutilgan narsa: CSV oddiy matn, .xlsx esa ichida XML fayllari bor
ZIP arxiv. Afsuski, tahlilchilar bizga CSV emas, Excel beradi.
.xls — eski format. Uni o'qiy olgan yagona o'quvchi — fastexcel. DuckDB ham, openpyxl ham
.xls ni umuman o'qimaydi, shuning uchun jadvalda ular yo'q.
Bitta muhim izoh: bu raqamlar spike dagidan farq qiladi. Spike hisobotida (09-spikes.md)
matn rejimi 1.78 soniya, fastexcel 1.79, openpyxl 20.06, CSV 0.16 soniya edi. Kitob fayllari esa
Windows diskida (/mnt/c) turibdi va WSL ularni ko'prik orqali o'qiydi — shuning uchun bu yerda
hammasi sekinroq. Lekin qarorga ta'sir qilgan narsalar saqlandi: turli read_xlsx yiqiladi,
openpyxl taxminan o'n barobar sekin, .xls ni faqat fastexcel o'qiydi.
Bitta katak
Yiqilishga qaytamiz. Muammo qanchalik keng tarqalgan? Faylni matn rejimida o'qib, sanaymiz:
import duckdb con = duckdb.connect() con.execute("load excel") con.execute(f"create table xom as select * from read_xlsx('{ISH / 'medium.xlsx'}', all_varchar = true)") jami, matnli = con.execute("select count(*), count(*) filter (where summa like '% %') from xom").fetchone() print(f"qatorlar: {jami:,}") print(f"summa ustunida matn sifatida yozilgan qiymatlar: {matnli:,}") show(con.execute("select tranzaksiya_id, sana, filial, summa from xom limit 8").df())
qatorlar: 400,000 summa ustunida matn sifatida yozilgan qiymatlar: 20,050
| tranzaksiya_id | sana | filial | summa |
|---|---|---|---|
| 1000001 | 2025-05-16 | Самарқанд | 28758014.63 |
| 1000002 | 2025-09-12 | Nukus | 4076999.73 |
| 1000003 | 27.09.2025 | Buxoro | 39668103.7 |
| 1000004 | 2026-07-23 | Тошкент | 8185657.47 |
| 1000005 | 09/02/2025 | Toshkent | 37087009.24 |
| 1000006 | 07/11/2025 | Nukus | 47852742.66 |
| 1000007 | 2026-04-17 | Самарқанд | 2791026.89 |
| 1000008 | 09/04/2026 | Ташкент | 19 113 274,33 |
Har yigirma qatordan biri. Haqiqiy bank faylida bu ulush boshqacha bo'lishi mumkin — lekin bitta bunday katak ham yetarli. Excel fayli qo'lda tahrirlanadi, nusxa ko'chiriladi, turli tizimlardan yig'iladi. Bunday katakning bo'lmasligiga hech kim kafolat bermaydi.
Sanalar ham xuddi shu jadvalda ko'rinib turibdi: bitta ustunda uch xil format. Agar o'quvchi
sanani "aqlli" aniqlashga urinsa, 09/02/2025 — 9-fevralmi yoki 2-sentabrmi? Bu savolga javobni
fayl o'zi bermaydi.
Avval matn, keyin tur
Spike dan chiqqan birinchi va eng muhim qoida:
Foydalanuvchi faylini hech qachon "turlarni o'zing aniqla" rejimida o'qima. Hammasini matn sifatida o'qi, keyin turni DuckDB ichida, ko'rinadigan qadam bilan o'zgartir.
Nega? Chunki turni o'zgartirish — bu tozalash, va tozalash foydalanuvchi ko'z oldida bo'lishi kerak. Yashirin "aqlli" o'quvchi xato qilsa, xato ko'rinmaydi. Ko'rinadigan qadam xato qilsa — uni ko'rish va bekor qilish mumkin.
Tezda bu qadam — fix_numbers tooli. Uni haqiqiy Tez engine bilan matnli summa ga
qo'llaymiz:
from accuracy.execute import run_plan sozlamalar = json.load(open(TEZ / "accuracy/settings.json", encoding="utf-8")) run_plan(con, "xom", [{"tool": "fix_numbers", "args": {"column": "summa"}}], sozlamalar, keep_as="toza") turi = dict((nom, tur) for nom, tur, *_ in con.execute("describe toza").fetchall())["summa"] qatorlar, sonlar = con.execute("select count(*), count(summa) from toza").fetchone() print(f"summa ustunining yangi turi: {turi}") print(f"qatorlar: {qatorlar:,}, son bo'lgan qiymatlar: {sonlar:,}") show(con.execute(""" select distinct xom.summa as oldin, toza.summa as keyin from xom join toza using (tranzaksiya_id) where xom.summa like '% %' order by xom.summa limit 4 """).df())
summa ustunining yangi turi: DOUBLE qatorlar: 400,000, son bo'lgan qiymatlar: 400,000
| oldin | keyin |
|---|---|
| 1 003 660,82 | 1 003 660.82 |
| 1 010 394,55 | 1 010 394.55 |
| 1 012 284,27 | 1 012 284.27 |
| 1 013 514,80 | 1 013 514.8 |
Hamma qiymat son bo'ldi, bittasi ham yo'qolmadi. Bo'sh joyli minglar va vergulli kasrni qanday
tushunishni fix_numbers sozlamalardan oladi (thousands_separators, decimal_comma) — kodda
qattiq yozilgan qoida yo'q.
Endi teskari xavf. "Raqamga o'xshagan hamma narsani songa aylantir" degan qoida ham xato:
nol_bilan = con.execute("select count(*) from xom where hisob_raqami like '0%'").fetchone()[0] misol = con.execute("select hisob_raqami from xom where hisob_raqami like '0%' limit 1").fetchone()[0] print(f"nol bilan boshlanadigan hisob raqamlari: {nol_bilan:,}") print(f"matn sifatida: {misol}") print(f"son sifatida: {int(misol)}")
nol bilan boshlanadigan hisob raqamlari: 39,709 matn sifatida: 08277854790345377450 son sifatida: 8277854790345377450
Hisob raqami — raqamlardan iborat, lekin u son emas. Uni songa aylantirsangiz, boshidagi nol yo'qoladi va raqam boshqa hisobga aylanadi. Spike dagi hamma o'quvchi bu ustunni to'g'ri — matn sifatida — qoldirdi. Lekin "aqlli" tozalash uni osongina buzib qo'yishi mumkin edi. Yana bir sabab: tur haqidagi qaror foydalanuvchining ko'z oldida bo'lsin.
Kuchsiz kompyuter haqida bitta tuzoq
Spike da biz kuchsiz kompyuterni "taqlid" qilishga urindik: o'quvchilarni faqat 2 ta oqim bilan ishga tushirdik. Spike hisobotidagi natija: hech narsa o'zgarmadi, farq 0.05 soniya ichida. Undan chiqarilgan xulosa: "Excel o'qish bitta yadro tezligiga bog'liq". Shu kitob uchun qayta o'lchaymiz:
SPIKE=/mnt/c/Users/Admin/Desktop/Offline/spikes/spike1_excel_read export PYTHONDONTWRITEBYTECODE=1 .venv/bin/python $SPIKE/bench/run.py --files medium.xlsx --readers duckdb_xlsx_text fastexcel --threads all 2 --runs 3
| File | Reader | Threads | Rows | Seconds (median) | Peak MiB (median) | Watched types | |---|---|---|---|---|---|---| | medium.xlsx | duckdb_xlsx_text | all | 400,000 | 2.78 | 195 | | | medium.xlsx | duckdb_xlsx_text | 2 | 400,000 | 2.57 | 194 | | | medium.xlsx | fastexcel | all | 400,000 | 2.83 | 586 | | | medium.xlsx | fastexcel | 2 | 400,000 | 3.11 | 589 | |
Yuqoridagi jadval — shu yig'ishning natijasi; kitob har safar qayta yig'ilganda u qaytadan o'lchanadi. Bu bobni birinchi marta yig'ganimizda natija spike bilan bir xil chiqmagan edi: DuckDB matn rejimi deyarli o'zgarmadi (2.65 va 2.75 soniya), fastexcel esa 2 ta oqimda sezilarli sekinlashdi — 3.40 dan 5.60 soniyaga. Bir xil kod, bir xil kompyuter — spike da farq yo'q edi, bu yerda bor edi. Sababini aniq bilmaymiz: farq fayllar turgan diskdan ham, boshqa narsadan ham bo'lishi mumkin. Biz buni taxmin bilan tushuntirmaymiz.
Lekin bu o'lchovdan ham, spike dan ham bitta xulosa chiqadi: kuchsiz kompyuterni taqlid qilib bo'lmaydi. 2 ta oqimli i9 — baribir i9 ning tez yadrolari, uning xotirasi va diski. Haqiqiy kuchsiz noutbukda hammasi boshqacha. Spike dagi "bitta yadroga bog'liq" degan xulosa esa fastexcel uchun endi shubha ostida. Kuchsiz kompyuterni faqat haqiqiy kuchsiz kompyuterda o'lchash mumkin. Biz buni hali qilmaganmiz (26-bob).
Qaror
Spike 1 dan to'rtta xulosa chiqdi:
| # | Xulosa | Nimaga ta'sir qildi |
|---|---|---|
| 1 | Turli aniqlaydigan read_xlsx foydalanuvchi faylida ishlatilmaydi |
Bitta katak butun importni to'xtatmasin |
| 2 | Avval hammasi matn, keyin tur DuckDB ichida | Tur o'zgarishi — ko'rinadigan tozalash qadami (fix_numbers, fix_dates) |
| 3 | calamine (fastexcel) — .xls uchun yagona yo'l va .xlsx da DuckDB darajasida tez |
O'quvchi sifatida calamine tanlandi |
| 4 | openpyxl o'n barobar sekin | Chiqarildi |
Keyinchalik Tezning Excel loaderi calamine ni python-calamine orqali to'g'ridan-to'g'ri
ishlatadigan bo'ldi va unga yana ko'p narsa qo'shildi: sarlavha qatorini topish, ikki qatorli
sarlavhalarni birlashtirish, jadval ustidagi nomlar va "Jami" qatorlarini ajratish. Bu 11-bob
mavzusi.
Hali qilinmagan ish
Spike 1 hisobotining oxirida bitta qator bor: "Windows Python da va haqiqiy kuchsiz noutbukda takrorlash — o'quvchini yakuniy tanlashdan oldin". Bu hali qilinmagan. Barcha raqamlar WSL ichida, kuchli kompyuterda o'lchangan. Biz buni yashirmaymiz va 26-bobda qaytamiz.
Xulosa
- Spike — qurishdan oldin eng xavfli joyni o'lchaydigan kichik tajriba. Kodi tashlanishi mumkin, raqamlari qoladi.
- Bitta matnli katak (
19 113 274,33) DuckDB ning turliread_xlsximportini butunlay to'xtatdi. - Shuning uchun Tez avval hammasini matn sifatida o'qiydi, keyin turni ko'rinadigan qadam bilan o'zgartiradi. Hisob raqami kabi ustunlar matn bo'lib qolishi kerak.
- calamine
.xlsni o'qiydigan yagona o'quvchi bo'ldi va.xlsxda ham tez; openpyxl o'n barobar sekin. - Oqimlarni kamaytirish kuchsiz kompyuterni taqlid qilmaydi: spike da hech narsa o'zgarmadi, bu kitobni yig'ishda esa fastexcel sekinlashdi. Haqiqiy kuchsiz kompyuterda o'lchash hali oldinda.
Keyingi bobda ikkinchi spike ning bir qismi: ma'lumot bazaga qanday kiradi va qanday chiqadi — PostgreSQL bilan ikki tomonga.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.