Приветствую, я уже писал 2 статьи (на geektimes тыц тыц ) по поводу форматов MARC.

Сегодня у меня статья с техническими подробностями, я прибрал код своего решения, убрал оттуда магию и вообще причесал.

Под катом: дружба 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 — приветствуются.
Поделиться с друзьями
-->

Комментарии (2)


  1. Shaz
    22.06.2016 00:05

    Кстати раз уж подробности, а почему выбор пал именно на связку Go + JS?


    1. t0pep0
      22.06.2016 08:14

      Go — «домашний» язык программирования, задачи «для себя» и около них по привычке решаю на нём.
      JS — выбран как самый распространенный язык, в том числе и в библиотечной среде.
      Причина добавить js — проста, что бы правила могли писать почти все, не смотря на мою любовь к Go, я признаю, что он не самый распространенный язык и ради одной конкретной задачи учить его никто не будет, к тому же хотелось иметь возможность изменять поведение приложения не рекомпилируя его. В итоге выбор пал на js — знают многие, в меру прост, хотя и изрядно (ИМХО) неудобен и костылен.