Приветствую, я уже писал 2 статьи (на geektimes тыц тыц ) по поводу форматов MARC.
Сегодня у меня статья с техническими подробностями, я прибрал код своего решения, убрал оттуда магию и вообще причесал.
Под катом: дружба go и js, ненависть к marc-форматам
И так, начнём с с «ядра» — пакета, для работы с marc форматами, пакет, написан на go, покрытие тестами 63%.
https://github.com/t0pep0/marc21
«Голова» всего пакета — структура MarcRecord
И всего два метода работающие с ней, это
На них особо останавливаться, честно говоря, не вижу смысла. Единственное, что ReadRecord при достижении конца Reader'а возвращает err == io.EOF.
Смотрим дальше, нас интересуют структуры Leader и VariableField, а так же то, почему VariableField сделанно слайсом а не хэшмапом (потому, что в противовес всяким стандартам и здравому смыслу ситуация существования двух разных полей (по содержимому), с одним тэгом — возможна, забегая вперед скажу, что для SubField это тоже справедливо)
Структура лидера, право слово, ничего интересного, просто набор флагов, а то, что не экспортируется используется только для сериализации\десериализации. К ней привязаны два метода — сериализации и десериализации, вызываются из {Read,Write}Record (для остальных структур это так-же справедливо.
Структура «переменного поля». Сразу хочу отметить несколько интересных моментов — тэги трехсимовльные, RawData — можно было сделать строкой, но лично для меня было удобней работать с массивом байт. При сериализации, если у поля нет подполей (len(Subfields)==0), то записывается RawData, иначе RawData игнорируется
Name — один символ, обрезается
Data — опять таки можно было использовать строку, но я так решил…
Особых ньюансов в пакете нет, с ходу могу сказать только одно — перед добавлением поля удостоверьтесь, что у поля есть хоть что-то, кроме тэга, иначе рискуете потратить много времени размышляя о высоком и пытаясь понять почему не проходит экспорт в OPAC\IRBIS.
Пример кода, который не меняет данные, а, по факту, просто копирует один файл записей в другой
Теперь перейдем к https://github.com/HerzenLibRu/BatchMarc
По факту — это js интерпретатор https://github.com/robertkrimen/otto/ с подключенной к нему библиотекой, о которой говорилось выше.
Отличие от преведушего кода только в том, что здесь мы открываем файл с js, и создаем js машину, передавая её правила.
Давайте более подробно посмотрим на js машину и её конструктор.
Как мы видим — всё просто и банально, встраивание не использовал сознательно.
В стандартную поставку otto добавляются две функции — LoadSource и WriteResult, плюс добавляются констуркторы классов (MarcRecord, Leader, VariableField, VariableSubField)
детально расписывать реализации функция я не буду, но обращу внимание на интересный момент в otto есть тип Object, к которому можно свести все переменные js. У типа Object есть метод Call (то же самое касается методов Set/Get), который позволяет вызвать метод переменной. Дак вот — Object.Call не позволяет вызвать метод у вложенного класса.
Примечательно тем, что ругается на ошибку типа, и из-за этого верное решение долго шло в голову.
Пару слов о JS. Искуственно созданых переменных — нет, просто создаете инстанс класса из конструктора MarcRecord и загружаете его LoadSource(instance), что бы отдать изменения в go в конце скрипта указываете WriteResult(instance).
PullRequest\IssueRequest — приветствуются.
Сегодня у меня статья с техническими подробностями, я прибрал код своего решения, убрал оттуда магию и вообще причесал.
Под катом: дружба go и js, ненависть к marc-форматам
И так, начнём с с «ядра» — пакета, для работы с marc форматами, пакет, написан на go, покрытие тестами 63%.
https://github.com/t0pep0/marc21
«Голова» всего пакета — структура MarcRecord
type MarcRecord struct {
Leader *Leader
directory []*directory
VariableFields []*VariableField
}
И всего два метода работающие с ней, это
func ReadRecord(r io.Reader) (record *MarcRecord, err error)
func (mr *MarcRecord) Write(w io.Writer) (err error)
На них особо останавливаться, честно говоря, не вижу смысла. Единственное, что ReadRecord при достижении конца Reader'а возвращает err == io.EOF.
Смотрим дальше, нас интересуют структуры Leader и VariableField, а так же то, почему VariableField сделанно слайсом а не хэшмапом (потому, что в противовес всяким стандартам и здравому смыслу ситуация существования двух разных полей (по содержимому), с одним тэгом — возможна, забегая вперед скажу, что для SubField это тоже справедливо)
type Leader struct {
length int
Status byte
Type byte
BibLevel byte
ControlType byte
CharacterEncoding byte
IndicatorCount byte
SubfieldCodeCount byte
baseAddress int
EncodingLevel byte
CatalogingForm byte
MultipartLevel byte
LengthOFFieldPort byte
StartCharPos byte
LengthImplemenDefine byte
Undefine byte
}
Структура лидера, право слово, ничего интересного, просто набор флагов, а то, что не экспортируется используется только для сериализации\десериализации. К ней привязаны два метода — сериализации и десериализации, вызываются из {Read,Write}Record (для остальных структур это так-же справедливо.
type VariableField struct {
Tag string
HasIndicators bool
Indicators []byte
RawData []byte
Subfields []*SubField
}
Структура «переменного поля». Сразу хочу отметить несколько интересных моментов — тэги трехсимовльные, RawData — можно было сделать строкой, но лично для меня было удобней работать с массивом байт. При сериализации, если у поля нет подполей (len(Subfields)==0), то записывается RawData, иначе RawData игнорируется
type SubField struct {
Name string
Data []byte
}
Name — один символ, обрезается
Data — опять таки можно было использовать строку, но я так решил…
Особых ньюансов в пакете нет, с ходу могу сказать только одно — перед добавлением поля удостоверьтесь, что у поля есть хоть что-то, кроме тэга, иначе рискуете потратить много времени размышляя о высоком и пытаясь понять почему не проходит экспорт в OPAC\IRBIS.
Пример кода, который не меняет данные, а, по факту, просто копирует один файл записей в другой
package main
import (
"github.com/t0pep0/marc21"
"io"
"os"
)
func main() {
orig := os.Args[1]
result := os.Args[2]
origFile, _ := os.Open(orig)
resultFile, _ := os.Create(result)
for {
rec, err := marc21.ReadRecord(origFile)
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
//А здесь - делайте что хотите....
err = rec.Write(resultFile)
if err != nil {
panic(err)
}
}
}
Теперь перейдем к https://github.com/HerzenLibRu/BatchMarc
По факту — это js интерпретатор https://github.com/robertkrimen/otto/ с подключенной к нему библиотекой, о которой говорилось выше.
func main() {
marcFile, err := os.Open(os.Args[1])
outFile, _ := os.Create(os.Args[2])
jsFile, _ := os.Open(os.Args[3])
jsBytes, _ := ioutil.ReadAll(jsFile)
jsRules := string(jsBytes)
if err != nil {
return
}
for {
rec, err := marc21.ReadRecord(marcFile)
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
if rec == nil {
break
}
res := new(marc21.MarcRecord)
js := NewJSMachine(rec, res)
err = js.Run(jsRules)
if err != nil {
panic(err)
}
res.Write(outFile)
}
}
Отличие от преведушего кода только в том, что здесь мы открываем файл с js, и создаем js машину, передавая её правила.
Давайте более подробно посмотрим на js машину и её конструктор.
type jsMachine struct {
otto *otto.Otto
source *marc21.MarcRecord
destination *marc21.MarcRecord
}
func NewJSMachine(source, destination *marc21.MarcRecord) (js *jsMachine) {
js = new(jsMachine)
js.otto = otto.New()
js.otto.Run(classJS)
js.otto.Set("LoadSource", js.fillSource)
js.otto.Set("WriteResult", js.getResult)
js.source = source
js.destination = destination
return js
}
func (js *jsMachine) Run(src string) (err error) {
_, err = js.otto.Run(src)
if err != nil {
return err
}
return nil
}
Как мы видим — всё просто и банально, встраивание не использовал сознательно.
В стандартную поставку otto добавляются две функции — LoadSource и WriteResult, плюс добавляются констуркторы классов (MarcRecord, Leader, VariableField, VariableSubField)
детально расписывать реализации функция я не буду, но обращу внимание на интересный момент в otto есть тип Object, к которому можно свести все переменные js. У типа Object есть метод Call (то же самое касается методов Set/Get), который позволяет вызвать метод переменной. Дак вот — Object.Call не позволяет вызвать метод у вложенного класса.
source := call.Argument(0)
if !source.IsObject() {
return otto.FalseValue()
}
object := source.Object()
//Вот так правильно
jsValue, _ := object.Get("VariableField")
jsVariableFields := jsValue.Object()
jsValue, _ = jsVariableFields.Call("length")
//А вот так - не правильно
jsValue, _ = object.Call("VariableField.length")
Примечательно тем, что ругается на ошибку типа, и из-за этого верное решение долго шло в голову.
Пару слов о JS. Искуственно созданых переменных — нет, просто создаете инстанс класса из конструктора MarcRecord и загружаете его LoadSource(instance), что бы отдать изменения в go в конце скрипта указываете WriteResult(instance).
PullRequest\IssueRequest — приветствуются.
Поделиться с друзьями
Shaz
Кстати раз уж подробности, а почему выбор пал именно на связку Go + JS?
t0pep0
Go — «домашний» язык программирования, задачи «для себя» и около них по привычке решаю на нём.
JS — выбран как самый распространенный язык, в том числе и в библиотечной среде.
Причина добавить js — проста, что бы правила могли писать почти все, не смотря на мою любовь к Go, я признаю, что он не самый распространенный язык и ради одной конкретной задачи учить его никто не будет, к тому же хотелось иметь возможность изменять поведение приложения не рекомпилируя его. В итоге выбор пал на js — знают многие, в меру прост, хотя и изрядно (ИМХО) неудобен и костылен.