Перейти к содержимому

Fingercomp's Playground

  • записей
    87
  • комментария
    452
  • просмотров
    311 248

Парсер CSV

Fingercomp

2 184 просмотра

CSV идёт от Comma-Separated Values, что, в общем, довольно точно описывает этот формат хранения таблиц. Вот типичная таблица:

aaa,bbb,ccc,dddeee,fff,ggg,hhh


Как видно, строки отделяются \n, а ячейки ­— запятой. Последняя строка может иметь или не иметь \n.

 

Формат очень простой. Описывается он в RFC 4180. Там всего 7 пунктов. Ну а раз простой, давайте соорудим парсер.

 

Вот у нас есть строка aaa,bbb,ccc,ddd\neee,fff,ggg,hhh. Задача: сделать из неё

[ [ "aaa", "bbb", "ccc", "ddd" ], [ "eee", "fff", "ggg", "hhh" ]]

 

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

def parse_csv(s):    # Сюда идёт результат    result = []    # Текущая строка    row = []    # Текущая ячейка    cell = ""    # Проходимся по строке    for i in range(len(s)):	    # Текущий символ	    c = s[i]	    if c == ",":		    # Если символ — запятая, закрываем ячейку		    row.append(cell)		    cell = ""	    elif c == "\n":		    # Если это перевод строки, то закрываем ячейку и строку		    row.append(cell)		    cell = ""		    result.append(row)		    row = []	    else:		    # Любой другой символ добавляем в ячейку		    cell += c    # Возвращаем результат    return result


Запускаем:

>>> parse_csv("aaa,bbb,ccc,ddd\neee,fff,ggg,hhh\n")[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']] >>> parse_csv("aaa,bbb,ccc,ddd\neee,fff,ggg,hhh")[['aaa', 'bbb', 'ccc', 'ddd']]

 

Действительно, в конце может и не быть \n. Давайте поправим:

def parse_csv(s):    result = []    row = []    cell = ""    for i in range(len(s)):	    c = s[i]	    if c == ",":		    row.append(cell)		    cell = ""	    elif c == "\n":		    row.append(cell)		    cell = ""		    result.append(row)		    row = []	    else:		    cell += c    # Если ячейка не пуста    if cell:	    # Закрываем ячейку и строку	    row.append(cell)	    result.append(row)    return result


Проверяем:

>>> parse_csv("aaa,bbb,ccc,ddd\neee,fff,ggg,hhh\n")[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']] >>> parse_csv("aaa,bbb,ccc,ddd\neee,fff,ggg,hhh")[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']]


Замечательно.

 

Почему я проверяю только ячейку, а не строку ещё? Просто пустая ячейка и непустая строка может быть только тогда, когда на конце строки висит запятая. aaa,bbb,. А это явно запрещено по RFC.

 


В текущем виде в ячейке у нас не получится хранить \n и ,. Если первый символ ещё кое-как, то без запятой как-то совсем не весело, верно?
На наше счастье, в спецификации есть и это. Ячейку можно поместить в двойные кавычки (", кто не понял), тогда до следующей кавычки обрабатываться \n и , не будут.

 

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

def parse_csv(s):    result = []    row = []    cell = ""    # Начиналась ли текущая ячейка с кавычки    quoted = False    for i in range(len(s)):	    c = s[i]	    if quoted:		    if c == '"':			    # Закрывающая кавычка			    quoted = False		    else:			    cell += c	    else:		    if c == '"':			    if not cell:				    # Открывающая кавычка в начале ячейки				    quoted = True			    else:				    # Кавычка в середине строки: запрещено				    return False		    elif c == ",":			    row.append(cell)			    cell = ""		    elif c == "\n":			    row.append(cell)			    cell = ""			    result.append(row)			    row = []		    else:			    cell += c    if cell:	    if quoted:		    # Где-то не закрыли кавычки		    return False	    row.append(cell)	    result.append(row)    return result

 

Проверяем:

>>> print(parse_csv(...     """aaa,bbb,ccc,ddd\neee,fff,ggg,hhh\n"""... ))...[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']]>>> print(parse_csv(...     """aaa,bbb,ccc,ddd\neee,fff,ggg,hhh"""... ))...[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']] >>> print(parse_csv(...     """aaa,bbb,ccc,"ddd\neee",fff,ggg,hhh"""... ))...[['aaa', 'bbb', 'ccc', 'ddd\neee', 'fff', 'ggg', 'hhh']]>>> print(parse_csv(...     """aaa,bbb,c"cc,ddd\neee,fff,ggg,hhh"""... ))...False>>> print(parse_csv(...     """aaa,bbb,"ccc,ddd\neee,fff,ggg,hhh"""... ))...False >>> print(parse_csv(...     """aaa,bbb,"cc"c,ddd\neee,fff,ggg,hhh"""... ))...[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']]


 

 

Всё верно, кроме последнего. В середине строки в закавыченных строках эти самые кавычки должны быть экранированы вот так: "". Например: "aaa""bbb,ccc",ddd,eee. Давайте починим и это.

def parse_csv(s):    result = []    row = []    cell = ""    quoted = False    # Является ли предыдущий символ кавычкой    prevQuote = False    for i in range(len(s)):	    c = s[i]	    if quoted:		    if c == '"':			    # Помечаем, что у нас есть кавычка в середине строки.			    # Она может быть экранированной.			    prevQuote = True			    quoted = False		    else:			    cell += c	    else:		    if c == '"':			    if not cell:				    quoted = True			    else:				    if prevQuote:					    # Если у нас прошлый символ был кавычкой,					    # то получаем экранированную кавычку.					    cell += '"'					    quoted = True					    prevQuote = False				    else:					    return False		    elif c == ",":			    row.append(cell)			    cell = ""			    # Кавычка была закрывающей			    prevQuote = False		    elif c == "\n":			    row.append(cell)			    cell = ""			    result.append(row)			    row = []			    # Кавычка была закрывающей			    prevQuote = False		    else:			    if prevQuote:				    # Мы ждали кавычку или закрытие ячейки.				    return False			    cell += c    if cell:	    if quoted:		    return False	    row.append(cell)	    result.append(row)    return result

 

Опять тестируем:

>>> print(parse_csv(...	 """aaa,bbb,ccc,ddd\neee,fff,ggg,hhh\n"""... ))[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']]>>> print(parse_csv(...	 """aaa,bbb,ccc,ddd\neee,fff,ggg,hhh"""... ))[['aaa', 'bbb', 'ccc', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']]>>> print(parse_csv(...	 """aaa,bbb,ccc,"ddd\neee",fff,ggg,hhh"""... ))[['aaa', 'bbb', 'ccc', 'ddd\neee', 'fff', 'ggg', 'hhh']]>>> print(parse_csv(...	 """aaa,bbb,c"cc,ddd\neee,fff,ggg,hhh"""... ))False>>> print(parse_csv(...	 """aaa,bbb,"ccc,ddd\neee,fff,ggg,hhh"""... ))False>>> print(parse_csv(...	 """aaa,bbb,"cc"c,ddd\neee,fff,ggg,hhh"""... ))False>>> print(parse_csv(...	 """aaa,bbb,"cc""c,ddd\neee,fff,ggg,hhh"""... ))False>>> print(parse_csv(...	 """aaa,bbb,"cc""c",ddd\neee,fff,ggg,hhh"""... ))[['aaa', 'bbb', 'cc"c', 'ddd'], ['eee', 'fff', 'ggg', 'hhh']]


 

 

Вот и всё. 44 строки кода на Python — и мы можем парсить CSV.
Я также переписал парсер на Lua, опубликовал его в OPPM под libcsv. Можете качать и радоваться. Вот сырцы.

 

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

  • Нравится 2


3 комментария


Рекомендуемые комментарии

Я также переписал парсер на Lua, опубликовал его в OPPM под libcsv

 

Буржуй что ли? Публикуй в нашей репе! :P

Поделиться комментарием


Ссылка на комментарий
Гость
Добавить комментарий...

×   Вы вставили отформатированное содержимое.   Удалить форматирование

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отобразить как ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...