Asosiy qismga o'tish

Excel o'qish: bitta iflos katak butun faylni to'xtatadi

4-bob 7 kod bloki 2333 so'z

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 .xls formatini 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:

python7 qator
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]}")
Chiqish
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):

bash4 qator
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
Chiqish (qaytish kodi 0, 24783 ms)
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:

bash3 qator
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
Chiqish (qaytish kodi 0, 157556 ms)
| 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:

python9 qator
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())
Chiqish
qatorlar: 400,000
summa ustunida matn sifatida yozilgan qiymatlar: 20,050
tranzaksiya_idsanafilialsumma
10000012025-05-16Самарқанд28758014.63
10000022025-09-12Nukus4076999.73
100000327.09.2025Buxoro39668103.7
10000042026-07-23Тошкент8185657.47
100000509/02/2025Toshkent37087009.24
100000607/11/2025Nukus47852742.66
10000072026-04-17Самарқанд2791026.89
100000809/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:

python16 qator
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())
Chiqish
summa ustunining yangi turi: DOUBLE
qatorlar: 400,000, son bo'lgan qiymatlar: 400,000
oldinkeyin
1 003 660,821 003 660.82
1 010 394,551 010 394.55
1 012 284,271 012 284.27
1 013 514,801 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:

python5 qator
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)}")
Chiqish
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:

bash3 qator
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
Chiqish (qaytish kodi 0, 63710 ms)
| 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 turli read_xlsx importini 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 .xls ni o'qiydigan yagona o'quvchi bo'ldi va .xlsx da 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.

Telegramda yozish Narxlar