Kodomo

Пользователь

CSV (comma-separated values)

Таблицы в формате CSV – это просто!

Для начала вспоминаем, как просто прочитать файл.

   1 file = open("eg.csv")
   2 print(file.read())

В питоне есть и другой способ: когда файл используется в том контексте, где питон ожидал бы список, файл воспринимается как список его строк:

   1 file = open("eg.csv")
   2 for line in file:
   3     print(line)

Теперь каждую строку нам нжуно разбить по разделителю. Английский эксель использует запятую, русский – точку с запятой.

Например, попробуем взять таблицу, содержащую только числа, и просуммировать числа в строке.

Мы уже умеем всё, что для этого нужно: разбивать строку по разделителю.

Попробуем:

   1 file = open("eg.csv")
   2 for line in file:
   3     elems = line.split(';')
   4     sum = 0
   5     for element in elems:
   6         sum += element
   7     print(sum)

Упс, не работает. На первой же строке у него в sum лежит 0, а в element лежит "3", то есть sum += element расшивровывается как sum = sum + element, то есть sum = 0 + "3"

Ошибочка!

Добавляем преобразование строки в число:

   1 file = open("eg.csv")
   2 for line in file:
   3     elems = line.split(';')
   4     print(line, elems)
   5     sum = 0
   6     for element in elems:
   7         sum += int(element)
   8     print(sum)
   9 file.close()

Оффтопик: with

И тут обнаруживаем, что в нашем файле с примером были буквы, и всё снова сломалось.

Заметим заодно, что питон не закрыл за собой файл. Например, поэтому windows не разрешает нам подсунуть вместо него другой.

   1 file = open("eg.csv")
   2 for line in file:
   3     elems = line.split(';')
   4     print(line, elems)
   5     sum = 0
   6     for element in elems:
   7         sum += int(element)
   8     print(sum)
   9 file.close()

И даже если мы ишем file.close(), ничего не меняется. Дело в том, что программа ломается в строчке sum += int(element), и до строки file.close() просто не доходит.

Ровно для таких случаев в питоне есть конструкция with: она гарантирует, что файл будет закрыт в тот момент, когда питон выйдет за её пределы – то ли потому, что исполнение дошло до следующей за ней строкой, то ли потому, что случилась ошибка.

Пишется это так:

   1 with open("eg.csv") as file:
   2     for line in file:
   3         elems = line.split(';')
   4         sum = 0
   5         for element in elems:
   6             if element.isdigit():
   7                 sum += int(element)
   8         print(sum)

Хорошая привычка просто работать с файлами всегда чере неё.

Возвращаемся к CSV

Наша задача была просуммировать числа в каждой строке.

Мы разобрались, что намнужно что-то делать с ячейками, в которых не числа. Давайте это напишем:

   1 with open("eg.csv") as file:
   2     for line in file:
   3         elems = line.split(';')
   4         sum = 0
   5         for element in elems:
   6             if element.isdigit():
   7                 sum += int(element)
   8         print(sum)

Но что-то получается маловато. Если приглядеться, обнаруживаем, что питон не прибавляет значение последней ячейки в строке.

Если ещё пристальнее приглядеться (например, воткнуть куда-нибудь print()), мы обнаружим, что в последней ячейке лежит значение '3\n' – этот перенос строки обманывает проверку isdigit().

То есть нам нужно отрезать из конца строки лишние переносы строки:

   1 with open("eg.csv") as file:
   2     for line in file:
   3         line = line.rstrip()
   4         elems = line.split(';')
   5         sum = 0
   6         for element in elems:
   7             if element.isdigit():
   8                 sum += int(element)
   9         print(sum)

Словари

Словарь – это как список, только в списке мы можем в качестве индекса элемента использовать только число, а в словаре мы можем использовать в качестве индекса почти что угодно:

   1 data = {'name': 'Adam'}
   2 data['height'] = 220
   3 print(data)
   4 print(data['name'])

Когда мы словарь используем в том месте, где питон ожидает список, то питон воспринимает словарь как список его ключей:

   1 if 'weight' in data:
   2         print("weight is", data['weight'])
   3 for key in data:
   4         print("his", key, "is", data[key])

Для словарей не определён порядок элементов (и он может меняться в процессе жизни словаря), поэтому для него бессмысленно понятие слайсов.

   1 old_height = data.pop('height')
   2 data['height'] = 180

Кортежи

Питон запрещает в качестве ключа в словаре использовать изменяемые сущности: списки и словари.

Поэтому в питоне есть специальный тип данных – кортеж – который ведёт себя в точности как списки, но его нельзя менять. Пишется он как просто несколько значений через запятую (а если возникает омонимия, то его нужно взять в круглые скобки):

   1 x = 1, 2, 3
   2 print(x[0], x[1:2])
   3 for value in x:
   4         print(value)

С кортежами мы можем хранить более сложные отношения. Например:

   1 data['height', 10] = 100 # рост Адама в 10 лет
   2 data['height', 18] = 200
   3 print(data)

Пример: собираем данные из нескольких табличек

Для примера попробуем взять несколько табличек: в первой каждая строка описывает качество человека, при этом про одного человека может быть несколько качеств.

qualities.csv

В другой табличке есть список людей, про которых мы хотим узнать их качества. Можно считать это миниатюрным запросом к очень простой базе данных, которую мы храним в виде CSV.

people.csv

Итого нам нужно:

   1 with open("qualities.csv") as file:
   2     qualities = {}
   3     for line in file:
   4         line = line.rstrip()
   5         elems = line.split(',')
   6         name = elems.pop(0)
   7         if name not in qualities:
   8             qualities[name] = []
   9         qualities[name].append(elems)
  10 
  11 with open("people.csv") as file:
  12     for line in file:
  13         line = line.rstrip()
  14         print(line, qualities[line])

assert

Маленький оффтопик.

Суть строк

   1 if name not in qualities:
   2     qualities[name] = []

состоит в том, чтобы гарантировать, что после них в qualities есть ключ name, и что qualities[name] есть список (возможно, пустой).

Мы это условие можем записать на питоне: name in qualities and type(qualities[name]) == list

Бывает полезным воткнуть в прогамму проверку, что это всегда так – и если эта проверка сломается, то мы будем хорошо понимать, где мы ошиблись.

Для этого существует конструкция assert: мы ей даём условие, и если это условие не выполнилось, то она рушится с ошибкой:

   1 with open("qualities.csv") as file:
   2     qualities = {}
   3     for line in file:
   4         line = line.rstrip()
   5         elems = line.split(',')
   6         name = elems.pop(0)
   7         if name not in qualities:
   8             qualities[name] = []
   9         assert name in qualities and type(qualities[name]) == list
  10         qualities[name].append(elems)
  11 
  12 with open("people.csv") as file:
  13     for line in file:
  14         line = line.rstrip()
  15         print(line, qualities[line])

Частотный список

Следующая задача: построим частотный список слов.

Для этого нужно начать с того, что открыть файл, прочитать его и токенизировать. Это мы уже умеем:

   1 with open("karenina.htm", encoding="cp1251") as file:
   2     tokens = file.read().split()
   3 
   4 clean_tokens = []
   5 for token in tokens:
   6     token = token.strip("/:'!?-,.\";<>()")
   7     clean_tokens.append(token)
   8 
   9 print(clean_tokens[20000:][:100])

В нашем файле оказалось много мусора из-за того, что это html, а не txt, пробуем чистить и его:

   1 with open("karenina.htm", encoding="cp1251") as file:
   2     tokens = file.read().split()
   3 
   4 clean_tokens = []
   5 for token in tokens:
   6     token = token.strip("/:'!?-,.\";<>()")
   7     if token != '' and token != 'dd>&nbsp;&nbsp':
   8         clean_tokens.append(token)
   9 
  10 print(clean_tokens[20000:][:100])

И обнаруживаем, что кроме очень частотных тэгов есть и другой мусор. Значит нам нужно завести список мусорных тэгов и выкидывать все токены, которые в него попадают:

   1 with open("karenina.htm", encoding="cp1251") as file:
   2     tokens = file.read().split()
   3 
   4 garbage = ['', 'dd>&nbsp;&nbsp', 'b', 'p',
   5            'align="center',
   6 ]
   7 
   8 clean_tokens = []
   9 for token in tokens:
  10     token = token.strip("/:'!?-,.\";<>()")
  11     if token not in garbage:
  12         clean_tokens.append(token)

Теперь стало похоже на чистый корпус.

Теперь считаем частоты.

Самый простой способ: заводим словарь, в котором ключ – слово, значение – его частота. И каждый раз, когда видим слово, добавляем 1 к его частоте. Но ещё нужно сказать, что когда мы видим слово первый раз, его частота равна 1:

   1 with open("karenina.htm", encoding="cp1251") as file:
   2     tokens = file.read().split()
   3 
   4 garbage = ['', 'dd>&nbsp;&nbsp', 'b', 'p',
   5            'align="center',
   6 ]
   7 
   8 clean_tokens = []
   9 for token in tokens:
  10     token = token.strip("/:'!?-,.\";<>()")
  11     if token not in garbage:
  12         clean_tokens.append(token)
  13 
  14 frequency = {}
  15 for token in clean_tokens:
  16     if token not in frequency:
  17         frequency[token] = 1
  18     else:
  19         frequency[token] += 1
  20 
  21 print(sorted(frequency)[:100])

Можно условие чуть-чуть сократить: если мы слово не видели, то его частота 0, а потом в любом случае прибавляем к нему 1. (Это тот же приём, что мы использовали для ассоциации качеств с людьми.

   1 with open("karenina.htm", encoding="cp1251") as file:
   2     tokens = file.read().split()
   3 
   4 garbage = ['', 'dd>&nbsp;&nbsp', 'b', 'p',
   5            'align="center',
   6 ]
   7 
   8 clean_tokens = []
   9 for token in tokens:
  10     token = token.strip("/:'!?-,.\";<>()")
  11     if token not in garbage:
  12         clean_tokens.append(token)
  13 
  14 frequency = {}
  15 for token in clean_tokens:
  16     if token not in frequency:
  17         frequency[token] = 0
  18     frequency[token] += 1
  19 
  20 print(sorted(frequency)[:100])

Или можем переписать так: сделаем переменную x, в которой лежит 0, если мы до этого слова не видели, и его прежняя частота, если мы его видели:

   1 with open("karenina.htm", encoding="cp1251") as file:
   2     tokens = file.read().split()
   3 
   4 garbage = ['', 'dd>&nbsp;&nbsp', 'b', 'p',
   5            'align="center',
   6 ]
   7 
   8 clean_tokens = []
   9 for token in tokens:
  10     token = token.strip("/:'!?-,.\";<>()")
  11     if token not in garbage:
  12         clean_tokens.append(token)
  13 
  14 frequency = {}
  15 for token in clean_tokens:
  16     if token not in frequency:
  17         x = 0
  18     else:
  19         x = frequency[token]
  20     frequency[token] = x + 1
  21 
  22 #print(sorted(frequency))

В питоне есть функция, которая ровно это и делает. Метод dict.get(key, defaultvalue) делает ровно это: если ключ key в словаре есть, то он возвращает значение dict[key], а если его не было, то возвращает defaultvalue.

С ним мы можем переписать подсчёт частот в три строки:

   1 with open("karenina.htm", encoding="cp1251") as file:
   2     tokens = file.read().split()
   3 
   4 garbage = ['', 'dd>&nbsp;&nbsp', 'b', 'p',
   5            'align="center',
   6 ]
   7 
   8 clean_tokens = []
   9 for token in tokens:
  10     token = token.strip("/:'!?-,.\";<>()")
  11     if token not in garbage:
  12         clean_tokens.append(token.lower())
  13 
  14 frequency = {}
  15 for token in clean_tokens:
  16     frequency[token] = frequency.get(token, 0) + 1
  17 
  18 print(sorted(frequency, key=frequency.get)[-100:])
  19 print(list(reversed(sorted(frequency, key=frequency.get)))[:100])
  20 print(sorted(frequency, key=frequency.get, reverse=True)[:100])