1/06/2014

Вырываем список книг для чтения из zotero с мясом, Tcl-ем и SQlite-ом

В этом посте мы продолжим беспощадную борьбу с кошмарным интерфейсом недо-системы управления библиографией под названием zotero с целью получить список книг для чтения. Даже для такой простой вещи, как получения списка книг, находящихся в базе zotero, нужно брать в руки автоген, скальпель и кувалду. Линуксоидов этим, конечно, не напугать, но маководов от экранов просьба удалиться во избежание.

В этом посте мы безтрепетной рукой вырвем с мясом из зотеры список книг, засунутых туда через графический, скажем так, интерфейс. В этом нам поможет язык Tcl (Тикль), Debian и SQLite3.


Нам, тем не менее, НЕ ПОМОЖЕТ на редкость убогая документация (пародия на неё), которая в целом рекомендует нам читать исходники и проваливать к такой-то матери. Поэтому мы решим проблему с помощью Tcl и Sqlite3.

Установка SQLite

В нашем Debian Linux все очень просто:
# apt-get install sqlite3 libsqlite3-dev libsqlite3-tcl
именно так, поскольку установка только sqlite3 недостаточна. Теперь у нас есть все средства для работы с SQLite, на котором построена зотера.

Вкушая SQlite...

SQLite есть движок для баз данных - простой, не требующий перечитывания томика квантовой механики и использования синхрофазотрон-конструкций. Десять минут листания отличных туториалов по SQLite дадут нам всё, чтобы взять от жизни от zotero всё:
Сначала можно потренироваться на базе данных через консоль SQLite3, давая команды и изучая внутреннюю структуру зотеровских кишок.
Подключение к базе данных zotero.sqlite делается командой в консоли  Linux:

starscream@dot:~/READ$ sqlite3 ZoteroLibrary/zotero.sqlite
SQLite version 3.7.3
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>

По умолчанию, вывод будет сжатым и не слишком дружественным к гуманоидам:

sqlite>  select * from itemTypes;
1|note||0
2|book||2
3|bookSection|2|2
4|journalArticle||2
 
Дабы сделать нашу жуткую, как интерфейс Гнома3, жизнь немного слаще, включим немного свистелок и мигалок:
sqlite> .mode column
и
sqlite> .headers on
что повернёт SQLite к нам лицом, а к лесу - задом, и заставит печатать заголовки таблиц:

sqlite>  select * from itemTypes;
itemTypeID  typeName    templateItemTypeID  display
----------  ----------  ------------------  ----------
1           note                            0
2           book                            2
3           bookSectio  2                   2
4           journalArt                      2

и это намного понятнее.

Ковыряемся в реляционно-базоданных кишках zotero

Наша цель - выдрать из базы данных зотеры список всех книг, которые нам бы хотелось прочитать, в виде простого текстового списка (точнее, в виде списка Markdown, который мы потом конвертируем в латех). Можно, конечно, экспортировать всё это в BiBTeX и потом вручную выковыривать оттуда книги с помощью JabRef, но мы выбираем Путь Самурая и делаем всё скриптами. Да, это ёпенсорс, детка...

Структура SQLite базы данных zotero

Сначала мы пробуем выяснить, что содержится в базе данных и в каком порядке. Для этого мы посмотрим на таблицы, которые есть в базе  с помощью команды .tables которая выдаст нам всю правду:

sqlite> .tables
annotations                itemNotes
baseFieldMappings          itemSeeAlso
baseFieldMappingsCombined  itemTags
charsets                   itemTypeCreatorTypes
collectionItems            itemTypeFields
collections                itemTypeFieldsCombined
creatorData                itemTypes
creatorTypes               itemTypesCombined
creators                   items
customBaseFieldMappings    libraries
customFields               proxies
customItemTypeFields       proxyHosts
customItemTypes            relations
deletedItems               savedSearchConditions
fieldFormats               savedSearches
fields                     settings
fieldsCombined             storageDeleteLog
fileTypeMimeTypes          syncDeleteLog
fileTypes                  syncObjectTypes
fulltextItemWords          syncedSettings
fulltextItems              tags
fulltextWords              transactionLog
groupItems                 transactionSets
groups                     transactions
highlights                 translatorCache
itemAttachments            users
itemCreators               version
itemData                   zoteroDummyTable
itemDataValues 
  
Да, всё именно настолько плохо. Но мы не унываем и попробуем поискать что-нибудь съедобное, перебирая таблицу за таблицей. Сначала выясним код, которым обозначаются в этом селе книжки:

 sqlite> select * from itemTypes;
 itemTypeID  typeName    templateItemTypeID  display
----------  ----------  ------------------  ----------
2           book                            2
15          report                          1
......

Так, мы ищем все элементы с itemTypeID=2. Посмотрим, есть ли в нашей базе книги - они должны быть:

sqlite> select * from items where itemTypeID=2;
itemID      itemTypeID  dateAdded            dateModified         clientDateModified   libraryID   key
----------  ----------  -------------------  -------------------  -------------------  ----------  ----------
1           2           2013-09-01 02:22:15  2013-09-01 02:22:43  2013-09-01 02:22:43              G8AH8ZTS
16          2           2013-09-16 04:56:38  2013-09-16 04:56:38  2013-09-16 04:56:38              APIRGRKB
18          2           2013-09-09 01:34:38  2013-09-10 10:42:17  2013-09-10 10:42:17              SEHQJ38X
42          2           2013-09-09 08:15:52  2013-09-15 07:30:03  2013-09-15 07:30:03              ZDFQ5W25
48          2           2013-09-09 08:19:28  2013-09-15 07:32:49  2013-09-15 07:32:49              DGIPD6QJ

Дизайн базы данных, конечно, феерический, но кое-что удалось выдрать: теперь у нас в руках itemID каждой книги. Неплохой старт, но нам хотелось бы заголовки книг (Titles), которые очевидно хранятся где-то ещё.

Немного поматюгавшись и перебрав ещё таблиц, мы натыкаемся на:
 
sqlite>  select * from fields;
fieldID     fieldName   fieldFormatID
----------  ----------  -------------
1           url
2           rights
3           series
......
110         title
......

Ага, теперь мы ищем  fieldID=110 в которых зарыты все названия (Title) книжек и статей. Чуть раньше мы нашли книжку  itemID=48 и теперь для примера мы хотим выудить её название (Title) зарытое в fieldID=110. Это можно сделать вот так:

sqlite> select * from itemData where itemID=48;
itemID      fieldID     valueID
----------  ----------  ----------
48          7           89
48          8           90
48          11          91
48          14          92
48          62          24
48          87          25
48          110         93

Это подводит нас совсем близко к нашей цели - ещё один окоп, ещё рывок и победа так близка! Заглавия элементов (в том числе книг) хранятся в  valueID, так что нам нужно смотреть в поле itemDataValues в котором всё свалено в одну большую кучу:

sqlite> select * from itemDataValues;
valueID     value
----------  -------------------------------
1           Numerical computing with Matlab
2           Atmospheric_Turbulence_SPIE.pdf
3           TR2011-056-parallelQP-onGPU.ann
4           A Parallel Quadratic Programmin

Ещё немного терпения, ещё один запрос к базе данных:

sqlite> select * from itemDataValues where valueID=93;
valueID     value
----------  -------------------
93          Matrix inequalities

Ага! Вот оно! Мы у цели! Можно ещё посмотреть, есть ли у книги прикреплённый нами честно купленный PDF файл:

sqlite> select * from itemAttachments where sourceitemID=48;    
itemID      sourceItemID  linkMode    mimeType         charsetID   path                                                   originalPath  syncState   storageModTime  storageHash
----------  ------------  ----------  ---------------  ----------  -----------------------------------------------------  ------------  ----------  --------------  -----------
252         48            0           application/pdf              storage:Zhan_X_Matrix_inequalities__Springer_2002.pdf                0

Есть, и это именно та книжка, которую мы собираемся читать. Отлично, закрываем базу данных:
sqlite> .quit
и идём писать на коленке скрипт на Tcl для генерации списка книг.

Подключаем TCL к базе данных SQLite

За что мы любим Tcl, так это за философию batteries included - все батарейки уже в комплекте, и ещё немного туториалов:
дают нам всё необходимое для общения с зотеровской базой данных, от которой мы теперь уж точно возьмём всё. Команды очень просты, и к примеру вот этот код на Tcl:
sqlite3 db1 ./testdb
db1 eval {CREATE TABLE t1(a int, b text)}
создаст нам таблицу с названием  t1 и двумя колонками  a и b. Но нам нужно просто вытаскивать данные из таблицы, так что задача ещё проще.

Tcl спешит на помощь!

Немного усилий, и мы имеет следующий скрипт:
#!/usr/bin/tclsh

### This script connects to the SQLite database and extracts all the necessary data from it.
package require sqlite3

### First, select the type of the document to output
### sqlite> select * from itemTypes;
# itemTypeID  typeName    templateItemTypeID  display
#----------  ----------  ------------------  ----------
#2           book                            2
#15          report                          1

set doc_type_select 2 ;#this is code for the zotero SQLite base

set field_of_interest 110 ;# 110 is a Title of the book

############## This is output files in LaTeX and Markdown
set mdown_prefix "- "
set reading_list_filename "./Projects/actionsProject-BooksToRead"

set tex_reading_list_filename ""
append tex_reading_list_filename $reading_list_filename ".tex"

set mdown_reading_list_filename ""
append mdown_reading_list_filename $reading_list_filename "_mdown.tex"

set write_fp [open $mdown_reading_list_filename w ]
############## This is output files in LaTeX and Markdown

set user_name [lindex [split [pwd] "/"] 2] ;# from the working directory, split the name and get only the second one.
sqlite3 db "/home/$user_name/READ/ZoteroLibrary/zotero.sqlite" ;# associate the SQLite database with the object __db__

### Now find all the items of the type selected in $doc_type_select
# sqlite> select * from items where itemTypeID=15;
#itemID      itemTypeID  dateAdded            dateModified         clientDateModified   libraryID   key
#----------  ----------  -------------------  -------------------  -------------------  ----------  ----------
#46          15          2013-09-09 08:19:06  2013-09-15 07:33:26  2013-09-15 07:33:26              ZJ4SKMQP
#68          15          2013-09-10 00:25:09  2013-09-10 00:26:30  2013-09-10 00:26:30              AQ2A3NWW

#set get_itemIDs_for_the_doc_type [db eval {select * from items where itemTypeID=15} ]
set cmd "set substituteMe_SQLiteCommand {select * from items where itemTypeID=$doc_type_select}" ;# here we glue the stings together to make a dynamically regenerable command
eval $cmd  ;# evaluating the string above as a command, and thus setting regexp

set get_itemIDs_for_the_doc_type [db eval $substituteMe_SQLiteCommand ]

set counter 1
# tmp_itemID tmp_itemTypeID  tmp_dateAdded tmp_dateModified tmp_clientDateModified tmp_libraryID  tmp_key
foreach {tmp_itemID tmp_itemTypeID  tmp_dateAdded tmp_dateModified tmp_clientDateModified tmp_libraryID  tmp_key} $get_itemIDs_for_the_doc_type {
#    puts $tmp_itemID
    set booksarray($counter) $tmp_itemID
    incr counter
}

############ The title is stored in the fieldID=110.
############ Now figure out the valueID for each of the items
foreach { num itemID } [array get booksarray] {

set cmd "set substituteMe_SQLiteCommand {select * from itemData where itemID=$itemID}"
eval $cmd
set get_valueIDs_for_the_doc_type [db eval $substituteMe_SQLiteCommand ]

    #itemID      fieldID     valueID
    #----------  ----------  ----------

    foreach { tmp_itemID tmp_fieldID tmp_valueID } $get_valueIDs_for_the_doc_type  {

        set is_rightField [string compare -nocase $tmp_fieldID $field_of_interest]

        if { $is_rightField == 0 } {

        set cmd "set substituteMe_SQLiteCommand {select * from itemDataValues where valueID=$tmp_valueID}"
        eval $cmd
        set get_Title [db eval $substituteMe_SQLiteCommand ]

####### Finally, we have the book titles!
foreach { numm book_title } $get_Title {

    regsub -all "_" $book_title " " book_title
    regsub -all {\x5B} $book_title "" book_title ;# replacing underbrace and square brackets
    regsub -all {\x5D} $book_title ", " book_title ;# replacing underbrace and square brackets

    puts $write_fp "$mdown_prefix $book_title"
} ;####### Finally, we have the book titles!

        } ;# if { $is_rightField == 0 }

    } ;# foreach { tmp_itemID tmp_fieldID tmp_valueID } $get_valueIDs_for_the_doc_type

}

close $write_fp
db close ;# close the SQLite database

exec pandoc -f markdown -t latex $mdown_reading_list_filename -o $tex_reading_list_filename

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

Немного о трюках в коде скрипта. Самый простой - сделать скрипт независимым от машины, на которой он исполняется. То есть мы берём имя пользователя скриптом и выдираем его командой  pwd :

set user_name [lindex [split [pwd] "/"] 2] ;# from the working directory, split the name and get only the second one.
sqlite3 db "/home/$user_name/READ/ZoteroLibrary/zotero.sqlite" ;# associate the SQLite database with the object __db__

Ещё трюк:

#set get_itemIDs_for_the_doc_type [db eval {select * from items where itemTypeID=15} ]
set cmd "set substituteMe_SQLiteCommand {select * from items where itemTypeID=$doc_type_select}" ;# here we glue the stings together to make a dynamically regenerable command
eval $cmd  ;# evaluating the string above as a command, and thus setting regexp

здесь иллюстрируются могучие способности Tcl в плане обработки строк: мы формируем команду как строку, а потом исполняем её, как команду. То есть  мы подставляем itemTypeID динамически, и потом выполняем с помощью  eval.

Немного регекспов для удаления скобок из названий книг:

regsub -all {\x5B} $book_title "" book_title ;

люлистрирует возможности регекспов в Tcl по замене через ASCII-коды (ибо \x5B есть символ ]  ).

Последний кусочек - конвертирование списка из Markdown в  LaTeX:
exec pandoc -f markdown -t latex $mdown_reading_list_filename -o $tex_reading_list_filename

полностью автоматизирует нашу задачу

Всё.

Пост иллюстрирует убедительную и беспощадную победу Tcl над Zotero и показывает немного трюков по работе с простой базой данных SQLite.
Код раскрашен с помощью hilite.me

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

  1. Марш учить SQL!

    select itemDataValues.value
    from itemTypes,
    items,
    itemData,
    itemDataValues
    where itemTypes.typeName="book"
    and itemTypes.itemTypeID=items.itemTypeID
    and items.itemID=itemData.itemID
    and itemData.valueID=itemDataValues.valueID

    Писал на вскидку, могут быть ошибки. Но смысл думаю и так очевиден.

    ОтветитьУдалить
  2. @Анонимный комментирует...
    Марш учить SQL!
    Это самое, Анонимус, ты прав, конечно, но здесь есть два момента:
    1. пост не про SQL как таковой, а про выдёргивание оттуда данных средствами TCL.
    2. структура базы данных мне была неизвестна.

    Я просто описал ход своих действий, чтобы потом самому не забыть.

    ОтветитьУдалить
  3. А вот просто експортировать из Zotero в Bibtex вам чем не подошло?
    Я так делал, работает

    ОтветитьУдалить