У меня есть функция, которая выполняет действия с 12 строками данных CSV.
12 строк считываются из большего файла CSV. Когда он заканчивается, процесс повторяется еще 5 раз для последующих партий из 12 строк.
(defun main ()
(send-actions 0 12 "client-one")
(send-actions 12 24 "client-two")
....
(send-actions 60 72 "client-six")
...)
(defun send-actions (start end account)
(check-upload-send (subseq (read-csv #P"~/Desktop/data.csv") start end) account))
(defun check-upload-send (table account)
(function to check for duplicates....)
(function to perform action 1....)
...)
Это работает очень хорошо. Большую часть времени.
Время от времени он будет выдавать повторяющуюся ошибку. Это потому, что он будет перечитывать строку 25 (это первый элемент из запущенного (send-actions 24 36 "client-three")
.
Есть ли лучшая функция или подход к чтению группы строк csv и выполнению над ней действия? С возможностью перехода на следующую партию из 12 строк?
Спасибо
примечание - read-csv
из cl-csv
@DavidHodge нет диапазонов, как в Python - отсчет начинается с 0 - первое число означает «включительно», а последнее число - здесь 12 - означает «исключительно». Итак, от 0 до 12 это 0, 1, 2, ..., 11.
Я не уверен, в чем ваша проблема. Одно улучшение, которое вы можете сделать, это прочитать CSV-файл один раз, а затем действовать в загруженном списке; таким образом вы знаете, что он не меняется между вызовами send-actions. Другое возможное улучшение — избавиться от повторных вызовов вручную с помощью цикла или каким-либо другим способом. Но если вы знаете, что это небольшое фиксированное количество партий, зачем беспокоиться?
Хороший вопрос, Томас. План — это масштаб рабочего решения в конечном итоге.
Давайте используем этот /tmp/test.csv
файл:
01,02,03,04,05
za,zb,zc,zd,ze
11,12,13,14,15
aa,ab,ac,ad,ae
21,22,23,24,25
ba,bb,bc,bd,be
31,32,33,34,35
ca,cb,cc,cd,ce
41,42,43,44,45
da,db,dc,dd,de
51,52,53,54,55
ea,eb,ec,ed,ee
(у меня 6 записей, сгруппированных по 2 рядам)
При использовании cl-csv:read-csv
с аргументом row-fn
строки не собираются, а передаются функции обратного вызова.
Я знаю, что иногда это называется «Ад обратных вызовов», когда существует слишком много уровней обратных вызовов, но мне нравится этот подход, потому что его легко составить.
Кроме того, вы читаете файл только один раз и вам не нужно хранить неопределенное количество данных в памяти (хотя здесь это незначительно).
Например, я могу написать функцию высшего порядка make-group-reader
, которая создает замыкание, которое собирает строки в группы размера size
, а затем вызывает другой обратный вызов group-fn
, когда группа завершена.
Обратите внимание, что здесь групповой обратный вызов никогда не вызывается для последнего фрагмента, если он неполный (но при необходимости вы можете что-то взломать):
(defun make-group-reader (size group-fn &key (sharedp t))
(check-type size (integer 1))
(let ((group (make-array size :initial-element nil :fill-pointer 0)))
(let ((limit (1- size)))
(lambda (row)
(when (= limit (vector-push row group))
(funcall group-fn (if sharedp group (copy-seq group)))
(setf (fill-pointer group) 0))))))
Аргумент sharedp
по умолчанию равен T и напрямую передает внутренний вектор обратному вызову. Если вы предпочитаете, вы можете сначала скопировать вектор с помощью COPY-SEQ
, установив для него значение NIL.
> (cl-csv:read-csv #P"/tmp/test.csv" :row-fn (make-group-reader 2 #'print))
#(("01" "02" "03" "04" "05") ("za" "zb" "zc" "zd" "ze"))
#(("11" "12" "13" "14" "15") ("aa" "ab" "ac" "ad" "ae"))
#(("21" "22" "23" "24" "25") ("ba" "bb" "bc" "bd" "be"))
#(("31" "32" "33" "34" "35") ("ca" "cb" "cc" "cd" "ce"))
#(("41" "42" "43" "44" "45") ("da" "db" "dc" "dd" "de"))
#(("51" "52" "53" "54" "55") ("ea" "eb" "ec" "ed" "ee"))
Затем я бы сделал еще один обратный вызов, который извлекает элементы из списка клиентов для каждой входящей группы. Это используется для связывания группы с клиентом и вызова другой функции:
(defun make-clients-callback (callback clients)
(lambda (group)
(funcall callback group (pop clients))))
Определим примерный список клиентов:
> (defvar *clients* (loop for i from 1 to 6 collect (format nil "client ~r" i)))
*CLIENTS*
> *clients*
("client one" "client two" "client three" "client four" "client five" "client six")
Также debug-client-cb
за тесты:
> (defun debug-client-cb (group client)
(print `(:client ,client :group ,group)))
DEBUG-CLIENT-CB
Затем следующий вызов группирует строки по 2 и вызывает нашу функцию отладки для каждой группы с соответствующим клиентом.
> (cl-csv:read-csv
#P"/tmp/test.csv"
:row-fn (make-group-reader 2 (make-clients-callback 'debug-client-cb
*clients*)))
(:CLIENT "client one" :GROUP #(("01" "02" "03" "04" "05") ("za" "zb" "zc" "zd" "ze")))
(:CLIENT "client two" :GROUP #(("11" "12" "13" "14" "15") ("aa" "ab" "ac" "ad" "ae")))
(:CLIENT "client three" :GROUP #(("21" "22" "23" "24" "25") ("ba" "bb" "bc" "bd" "be")))
(:CLIENT "client four" :GROUP #(("31" "32" "33" "34" "35") ("ca" "cb" "cc" "cd" "ce")))
(:CLIENT "client five" :GROUP #(("41" "42" "43" "44" "45") ("da" "db" "dc" "dd" "de")))
(:CLIENT "client six" :GROUP #(("51" "52" "53" "54" "55") ("ea" "eb" "ec" "ed" "ee")))
Вы можете немного упростить ситуацию следующим образом:
(defun make-my-csv-reader (&optional (clients *clients*))
(make-group-reader 2 (make-clients-callback #'check-upload-send clients)))
И передать (make-my-csv-reader)
как :row-fn
.
Мне любопытно, почему бы использовать этот подход вместо подхода subseq? Основная причина в том, что данные считываются только один раз?
Да, я стараюсь не загружать весь файл при обработке данных, немного надежнее обрабатывать данные как поток значений и постепенно строить результат, чем иметь шаг, на котором мне нужен неопределенный объем памяти.
В некоторых случаях это не самый рациональный подход, потому что проще/быстрее прочитать весь файл за один проход и обработать его.
Есть ли другие причины?
Мне нравится, как сложность разбита по слоям: сначала группировка элементов, потом их обработка и т. д. с возможностью добавления в пайплайн любой промежуточной функции для отладки и т. д.
Кроме того, обратные вызовы являются замыканием, которое можно использовать для контроля завершения обработки. Вместо того, чтобы читать все строки и обрабатывать их, я могу вернуться из закрытия раньше:
> (block :find-row
(cl-csv:read-csv #P"/tmp/test.csv"
:row-fn (lambda (row)
(when (find "aa" row :test #'string=)
(return-from :find-row row)))))
("aa" "ab" "ac" "ad" "ae")
Дефо что-то изучать. Мне любопытно, почему бы использовать этот подход вместо подхода subseq? Основная причина в том, что данные считываются только один раз? Есть ли другие причины.
@Vinn Я попытался добавить несколько объяснений того, почему мне нравится этот подход, есть и другие способы сделать это, но я думаю, что это хорошо работает с моим мозгом.
Мне любопытно, почему в функции make-group-reader
вы помещаете let внутри let? Вместо того, чтобы использовать только один let для определения group
и limit
?
Я думаю, что это случайно, я решил добавить limit
в какой-то момент, но я торопился закончить ответ и не обратил внимания. И/или из-за мышечной памяти.
Понятно. Спасибо за уточнение. Ваши ответы очень познавательны.
(subseq 0 12 ) 13 строк, не так ли? поэтому начало и конец перекрываются для перечисленных случаев