Към съдържанието
wannacode

Данни във файлове

Проблем

Текст, а трябват числа

Складът изнася продуктите си във файл. Всеки ред е един продукт: име, цена и наличност, разделени със запетаи. Първият ред казва какво е във всяка колона:

products.csv
Име,Цена,Наличност
Хляб,1.20,30
Мляко,2.10,18
Кафе на зърна,18.90,12

Вече можеш да прочетеш файла. Но за програмата редът е само текст:

line = "Хляб,1.20,30"
print(len(line))
print(line[5])
Изход
12
1

Тя не знае, че 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} €")
Стъпка по стъпка, с думи
  1. Редът от файла е един текст. За програмата 18.90 в него е само четири знака.
  2. split реже реда при всяка запетая: списък от три текста.
  3. Името си остава текст.
  4. float превръща текста "18.90" в числото 18.9. С него вече може да се смята.
  5. int прави от "12" цялото число 12.
  6. Product(...) създава нов обект, __init__ слага трите стойности в полетата му, а p сочи към него.
  7. p.price се печата като 18.9: нулата в края беше само в текста.
  8. Цената по наличността: вече е сметка с числа.

Виж го

Целият файл

Зареждаш всички продукти в списък и смяташ стойността на стоката:
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.

Повече за списъците има в модул „Колекции“.

Идеята

Грешни редове

Файловете ги пишат хора и други програми. Рано или късно идва ред без колона, дума вместо число или празен ред:

products.csv
Име,Цена,Наличност
Хляб,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(",")))
Отговор

Идеята

Обекти към 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,18

from_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: 'Цена'

Избери реда с грешката.

Идеята

Колони по име

Друга програма изнася същите данни, но с колоните в друг ред и с една повече. Ако пишеш 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 със съдържанието по-долу. Отпечатай средната възраст (целочислено деление), кой е най-възрастен и колко души има от всеки град, в реда, в който градовете се появяват. Градовете пази в един списък, а броевете в друг, на същите места.

people.csv
Име,Възраст,Град
Иван,34,София
Мария,28,Пловдив
Георги,41,София
Елена,25,Варна
Изход
Средна възраст: 32
Най-възрастен: Георги, 41
Брой хора от София: 2
Брой хора от Пловдив: 1
Брой хора от Варна: 1

2. Запис на героя. Клас Hero с име, ниво, живот и злато. Метод save(path) записва героя като редове ключ=стойност, а статичен метод load(path) създава нов Hero от такъв файл. Създай „Арин“ с ниво 6, 100 живот и 120 злато, вдигни му нивото с 1, махни 15 живот, дай му 220 злато, запиши го, отпечатай файла и го зареди обратно.

Изход
name=Арин
level=7
health=85
gold=340
Заредих: Арин, ниво 7, живот 85, злато 340

3. Дублиран текст. Създай text.txt с текста по-долу. Раздели го на изречения по точката (и интервалите след нея) и отпечатай всяко изречение, което се среща повече от веднъж, по веднъж.

text.txt
Това е текст. Това също е текст. Това е текст.
Изход
Дублиран текст: "Това е текст."

Още задачи

  • Най-скъпият продукт. Зареди products.csv от урока и отпечатай продукта с най-висока цена и колко продукта имат наличност под 20.
  • Поправени редове. Зареди файла с грешните редове и запиши верните в products-clean.csv, а грешните, с номера и причината, в rejected.txt.
  • Отчет за продажбите. Файл sales.csv с колони Дата,Продукт,Брой,Цена. Редовете са подредени по дата. Отпечатай оборота за всеки ден с f-низ в колони.

Тест

Провери се

1. Какво е първият ред на повечето CSV файлове?
2. from_csv хвърля ValueError при грешен брой колони, а float("седем") хвърля при грешно число. Колко except трябват, за да хванеш и двете?
3. Колко е len("Кафе,7.50,".split(","))?
4. Името на продукта е „Сирене, краве“. Как да го запишеш в CSV, без да стане две колони?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·