Данни във файлове
Проблем
Текст, а трябват числа
Складът изнася продуктите си във файл. Всеки ред е един продукт: име, цена и наличност, разделени със запетаи. Първият ред казва какво е във всяка колона:
Име,Цена,НаличностХляб,1.20,30Мляко,2.10,18Кафе на зърна,18.90,12Вече можеш да прочетеш файла. Но за програмата редът е само текст:
line = "Хляб,1.20,30"print(len(line))print(line[5])121Тя не знае, че 1.20 е цена, а 30 е брой. За да сметнеш колко струва стоката, трябва да превърнеш всеки ред в нещо с полета: в обект Product.
Идеята
CSV: ред към обект
Такъв файл се казва CSV (comma-separated values, „стойности, разделени със запетая“). Чете се от Excel, от бази данни, от всяка програма. Един ред става обект в три стъпки: split, превръщане на всяка част в нужния тип, конструктор.
@staticmethod.class Product: def __init__(self, name, price, stock): self.name = name self.price = price self.stock = stock
@staticmethod def from_csv(line): f = line.split(",") if len(f) != 3: raise ValueError(f"очаквам 3 колони, а са {len(f)}") return Product(f[0].strip(), float(f[1]), int(f[2]))
def to_csv(self): return f"{self.name},{self.price},{self.stock}"p = Product.from_csv("Кафе на зърна,18.90,12")print(f"{p.name}: {p.price:.2f} € x {p.stock} = {p.price * p.stock:.2f} €")Кафе на зърна: 18.90 € x 12 = 226.80 €from_csv е единственото място, което знае как изглежда редът. Ако форматът се промени, променяш само него. Проверката за броя на колоните хвърля ValueError с ясно съобщение, вместо програмата да спре по-късно с индекс извън списъка. float и int сами хвърлят ValueError, ако текстът не е число, и сами махат интервалите около числото.
Какво прави from_csv с реда, стъпка по стъпка, с тялото му, разписано ред по ред:
line = "Кафе на зърна,18.90,12"f = line.split(",")name = f[0]price = float(f[1])stock = int(f[2])p = Product(name, price, stock)print(f"{p.name}: {p.price} € x {p.stock}")print(f"{p.price * p.stock:.2f} €")Стъпка по стъпка, с думи
- Редът от файла е един текст. За програмата 18.90 в него е само четири знака.
- split реже реда при всяка запетая: списък от три текста.
- Името си остава текст.
- float превръща текста "18.90" в числото 18.9. С него вече може да се смята.
- int прави от "12" цялото число 12.
- Product(...) създава нов обект, __init__ слага трите стойности в полетата му, а p сочи към него.
- p.price се печата като 18.9: нулата в края беше само в текста.
- Цената по наличността: вече е сметка с числа.
Виж го
Целият файл
try: with open("products.csv", "w", encoding="utf-8") as file: file.write("Име,Цена,Наличност\nХляб,1.20,30\nМляко,2.10,18\nКафе на зърна,18.90,12\n") with open("products.csv", encoding="utf-8") as file: lines = file.read().splitlines() products = [] header = True for line in lines: if header: header = False continue products.append(Product.from_csv(line)) value = 0 for p in products: value += p.price * p.stock print(f"{len(products)} продукта, стока за {value:.2f} €")except OSError as e: print(f"Не мога да прочета products.csv: {e}")3 продукта, стока за 300.60 €productsзапочва като празен списък, аappendдобавя всеки нов продукт в края му.len(products)е колко са.headerеTrueсамо за първото завъртане. Тогава редът се пропуска сcontinue.
Повече за списъците има в модул „Колекции“.
Идеята
Грешни редове
Файловете ги пишат хора и други програми. Рано или късно идва ред без колона, дума вместо число или празен ред:
Име,Цена,НаличностХляб,1.20,30Мляко,2.10Кафе,седем,4
Чай,3.50,25Една грешка не трябва да спира зареждането на всичко останало. Всеки ред е в свой try, а съобщението казва номера на реда, за да може някой да го поправи:
try: with open("products.csv", "w", encoding="utf-8") as file: file.write("Име,Цена,Наличност\nХляб,1.20,30\nМляко,2.10\nКафе,седем,4\n\nЧай,3.50,25\n") with open("products.csv", encoding="utf-8") as file: lines = file.read().splitlines() products = [] row = 0 for line in lines: row += 1 if row == 1 or not line.strip(): continue try: products.append(Product.from_csv(line)) except ValueError as e: print(f"Ред {row}: {e}") print(f"Заредени: {len(products)}")except OSError as e: print(f"Не мога да прочета products.csv: {e}")Ред 3: очаквам 3 колони, а са 2Ред 4: could not convert string to float: 'седем'Заредени: 2- Празните редове се пропускат:
line.strip()маха интервалите, и ако не остане нищо,notдаваTrue. - Едно
except ValueErrorхваща и двете грешки: тази, коятоfrom_csvхвърля сама, и тази отfloat("седем"). В Python няма отделен вид изключение за грешно число. Кое е станало, казва съобщението. - Програмата казва „Заредени: 2“ и продължава с тях.
Пробвай
Празни колони
Какво ще отпечата програмата?
print(len("Кафе,,4".split(",")))print(len("Кафе,7.50,".split(",")))print(len("".split(",")))"", и по средата (,,), и в края: "Кафе,7.50," дава 3 части (в Java щяха да са 2). Дори празен текст дава една празна част, а не нула. Във from_csv това значи: празен ред е ред с 1 колона, а празната колона стига до float(""), който хвърля ValueError.Идеята
Обекти към CSV
Обратната посока е метод, който сглобява реда:
def to_csv(self): return f"{self.name},{self.price},{self.stock}"Записваш заглавния ред и после по един ред за всеки обект:
stock = [Product("Хляб", 1.2, 30), Product("Мляко", 2.1, 18)]try: with open("stock.csv", "w", encoding="utf-8") as out: out.write("Име,Цена,Наличност\n") for p in stock: out.write(p.to_csv() + "\n") with open("stock.csv", encoding="utf-8") as file: print(file.read(), end="")except OSError as e: print(f"Не мога да запиша stock.csv: {e}")Име,Цена,НаличностХляб,1.2,30Мляко,2.1,18from_csv може да прочете този файл обратно. Записът и четенето трябва да ползват един и същ формат.
В Java форматирането на числа следва езиковите настройки на компютъра и на български пише запетая, която разбутва колоните. f-низът в Python не ги следва: {price:.2f} пише точка на всеки компютър.
line = f"Хляб,{1.2:.2f},30"print(f"{line} -> {len(line.split(','))} колони")Хляб,1.20,30 -> 3 колониИстинският капан е запетая в самата стойност: „Сирене, краве“ става две колони. Модулът csv от стандартната библиотека знае правилото: такава стойност се огражда с кавички. csv.writer ги слага при запис, а csv.reader ги маха при четене и дава всеки ред като списък:
import csv
with open("cheese.csv", "w", encoding="utf-8", newline="") as out: writer = csv.writer(out, lineterminator="\n") writer.writerow(["Име", "Цена", "Наличност"]) writer.writerow(["Сирене, краве", 9.8, 5])with open("cheese.csv", encoding="utf-8", newline="") as file: print(file.read(), end="")with open("cheese.csv", encoding="utf-8", newline="") as file: for row in csv.reader(file): print(row)Име,Цена,Наличност"Сирене, краве",9.8,5['Име', 'Цена', 'Наличност']['Сирене, краве', '9.8', '5']newline=""трябва да е на всеки файл, който четеш или пишеш сcsv: модулът сам се грижи за новите редове.lineterminator="\n"пише редовете с\n. Без негоcsv.writerслага\r\n, както е в Windows и в стандарта за CSV.
Пробвай
Намери грешката
Програмата трябва да зареди трите продукта от products.csv, а спира още на първия ред:
ValueError: could not convert string to float: 'Цена'Избери реда с грешката.
row става 1 още в първото завъртане, преди проверката, затова row == 0 никога не е вярно. Заглавният ред не се пропуска и from_csv се опитва да превърне думата „Цена“ в число. Поправката е row == 1.
Идеята
Колони по име
Друга програма изнася същите данни, но с колоните в друг ред и с една повече. Ако пишеш f[1] за цената, при нов формат ще четеш грешна колона и няма да разбереш. По-сигурно е да намериш колоната по името ѝ в заглавния ред:
def column(header, name): for i in range(len(header)): if header[i].strip().lower() == name.lower(): return i raise ValueError(f"Няма колона {name}")
try: with open("export.csv", "w", encoding="utf-8") as file: file.write("Наличност,Име,Код,Цена\n30,Хляб,1001,1.20\n18,Мляко,1002,2.10\n") name_col = -1 price_col = -1 with open("export.csv", encoding="utf-8") as file: for line in file.read().splitlines(): f = line.split(",") if name_col == -1: name_col = column(f, "име") price_col = column(f, "цена") continue print(f"{f[name_col]}: {f[price_col]} €")except OSError as e: print(e)Хляб: 1.20 €Мляко: 2.10 €Първото завъртане само намира къде са колоните. След това всеки ред се чете по тези индекси. Ако колоната я няма, column хвърля изключение веднага, вместо да чете грешни данни.
По-точно, за любопитните
- Колони по име с
csv.csv.DictReaderправи търсенето по име вместо теб: първият ред дава имената, а всеки следващ ред е речник (dict) от името на колоната към стойността. Речниците са в модул „Колекции“.
import csv
with open("export.csv", "w", encoding="utf-8") as file: file.write("Наличност,Име,Код,Цена\n30,Хляб,1001,1.20\n18,Мляко,1002,2.10\n")with open("export.csv", encoding="utf-8", newline="") as file: for row in csv.DictReader(file): print(f"{row['Име']}: {row['Цена']} €")- Друг разделител. Excel на компютър с български настройки често записва CSV с
;, именно защото запетаята е десетичният знак. Тогава четеш сcsv.reader(file, delimiter=";"). - Сериализация. Модулът
pickleзаписва цял обект във файл. Файлът е двоичен: не се чете от човек, а и не бива да зареждашpickleфайл от непознат източник, защото при зареждане може да изпълни код. Текстовите формати като CSV и JSON (модулътjson) са по-предвидими и затова се ползват повече.
Упражнение
Данни, които оживяват
Реши задачите в редактора си. Програмата първо сама създава входния файл.
1. Анализ на CSV. Създай people.csv със съдържанието по-долу. Отпечатай средната възраст (целочислено деление), кой е най-възрастен и колко души има от всеки град, в реда, в който градовете се появяват. Градовете пази в един списък, а броевете в друг, на същите места.
Име,Възраст,ГрадИван,34,СофияМария,28,ПловдивГеорги,41,СофияЕлена,25,ВарнаСредна възраст: 32Най-възрастен: Георги, 41Брой хора от София: 2Брой хора от Пловдив: 1Брой хора от Варна: 12. Запис на героя. Клас Hero с име, ниво, живот и злато. Метод save(path) записва героя като редове ключ=стойност, а статичен метод load(path) създава нов Hero от такъв файл. Създай „Арин“ с ниво 6, 100 живот и 120 злато, вдигни му нивото с 1, махни 15 живот, дай му 220 злато, запиши го, отпечатай файла и го зареди обратно.
name=Аринlevel=7health=85gold=340Заредих: Арин, ниво 7, живот 85, злато 3403. Дублиран текст. Създай text.txt с текста по-долу. Раздели го на изречения по точката (и интервалите след нея) и отпечатай всяко изречение, което се среща повече от веднъж, по веднъж.
Това е текст. Това също е текст. Това е текст.Дублиран текст: "Това е текст."Още задачи
- Най-скъпият продукт. Зареди
products.csvот урока и отпечатай продукта с най-висока цена и колко продукта имат наличност под 20. - Поправени редове. Зареди файла с грешните редове и запиши верните в
products-clean.csv, а грешните, с номера и причината, вrejected.txt. - Отчет за продажбите. Файл
sales.csvс колониДата,Продукт,Брой,Цена. Редовете са подредени по дата. Отпечатай оборота за всеки ден с f-низ в колони.
Тест