В этом посте мы безтрепетной рукой вырвем с мясом из зотеры список книг, засунутых туда через графический, скажем так, интерфейс. В этом нам поможет язык Tcl (Тикль), Debian и SQLite3.
Нам, тем не менее, НЕ ПОМОЖЕТ на редкость убогая документация (пародия на неё), которая в целом рекомендует нам читать исходники и проваливать к такой-то матери. Поэтому мы решим проблему с помощью Tcl и Sqlite3.
Установка SQLite
В нашем Debian Linux все очень просто:# apt-get install sqlite3 libsqlite3-dev libsqlite3-tclименно так, поскольку установка только sqlite3 недостаточна. Теперь у нас есть все средства для работы с SQLite, на котором построена зотера.
Вкушая SQlite...
SQLite есть движок для баз данных - простой, не требующий перечитывания томика квантовой механики и использования синхрофазотрон-конструкций. Десять минут листания отличных туториалов по SQLite дадут нам всё, чтобы взять- 15 SQLite3 SQL Commands Explained with Examples
- Tcl SQLite quickstart
- Excellent tutorial from zetcode
Подключение к базе данных
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
Марш учить 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
Писал на вскидку, могут быть ошибки. Но смысл думаю и так очевиден.
@Анонимный комментирует...
ОтветитьУдалитьМарш учить SQL!
Это самое, Анонимус, ты прав, конечно, но здесь есть два момента:
1. пост не про SQL как таковой, а про выдёргивание оттуда данных средствами TCL.
2. структура базы данных мне была неизвестна.
Я просто описал ход своих действий, чтобы потом самому не забыть.
А вот просто експортировать из Zotero в Bibtex вам чем не подошло?
ОтветитьУдалитьЯ так делал, работает