Парсер CSV
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. Можете качать и радоваться. Вот сырцы.
Ну и надеюсь, это было менее сложно, чем мои записи про пакетные менеджеры до этого, и вы смогли прочитать это .
- 2
3 комментария
Рекомендуемые комментарии