2010年9月30日 星期四

OPDS Catalog 1.0 筆記

OPDS Catalog,一種制式的書籍資訊的呈列方式,其他對應的例子以 RSS 最廣為人知。而 RSS 也是一種制式格式,常用在 Blog 文章、新聞發布,當然,連現在流行的微網誌也有部份提供,讓各家閱讀器可以透過統一的格式讀取資訊,讓使用者可以只用一款閱讀器軟體,把他想要追蹤的資訊通通透過 RSS 訂閱起來,十分方便。而 OPDS Catalog 也是要提供這種制式的規範,以便各家閱讀器可以方便處理,只是,OPDS Catalog 著眼的是書籍資訊的傳播,產生的 OPDS Catalog 則是常用在電子書閱讀器上。


因為 OPDS Catalog 規範的事宜包括電子書可提供的交流方式,因此,除了呈列書單外,還可提供額外的連結導向到書籍供應者,例如直接下載、借書等等,在 Catalog 1.0 就定義了以下可能的情境:


http://opds-spec.org/acquisition
http://opds-spec.org/acquisition/open-access
http://opds-spec.org/acquisition/borrow
http://opds-spec.org/acquisition/buy
http://opds-spec.org/acquisition/sample
http://opds-spec.org/acquisition/subscribe


其中 acquisition 算是通用的,若要細分,則有公開書籍(open-access)、借書(borrow)、買書(buy)、部份內容(sample),而 subscribe 有時也用在其他未定義的項目。因此,關於一本書的來源之範例描述:


<link rel="http://opds-spec.org/acquisition" href="/content/free/4561.epub" type="application/epub+zip"/>
<link rel="http://opds-spec.org/acquisition" href="/content/free/4561.mobi" type="application/x-mobipocket-ebook"/>
<link rel="http://opds-spec.org/acquisition/buy" href="/content/4561.pdf" type="application/pdf">
    <opds:price currencycode="USD">18.99</opds:price>
    <opds:price currencycode="GBP">11.99</opds:price>
</link>


其中 rel 代表這本書來源的狀況,而 href 則是連結位置,type 則是檔案的型態,可以用在讓電子書閱讀器挑選它支援的格式,常見格式:


application/epub+zip
application/pdf
application/x-mobipocket-ebook


另外,書籍來源要付費時,此例是 http://opds-spec.org/acquisition/buy,則可以使用 opds:price 的標籤進行價格和貨幣的描述。


接著,既然在電子書閱讀器上使用,那來個書籍封面的顯示也不為過吧!來個一張大圖,一張小圖,讓閱讀器依狀況使用:


<link rel="http://opds-spec.org/image" href="/covers/4561.lrg.png" type="image/png"/>
<link rel="http://opds-spec.org/image/thumbnail" href="/covers/4561.thmb.gif" type="image/gif"/>


經過上述的描述後,也差不多都有提到關鍵的東西,接著看一下一則書籍的描述吧,此時就將顯示很多想得到的資訊,如書籍標題、作者、發佈日期、語言、分類、摘要、簡短內容等等:


  <entry>
    <title>Bob, Son of Bob</title>
    <id>urn:uuid:6409a00b-7bf2-405e-826c-3fdff0fd0734</id>
    <updated>2010-01-10T10:01:11Z</updated>

    <author>
      <name>Bob the Recursive</name>
      <uri>http://opds-spec.org/authors/1285</uri>
    </author>
    <dc:language>en</dc:language>
    <dc:issued>1917</dc:issued>
    <category scheme="http://www.bisg.org/standards/bisac_subject/index.html"
              term="FIC020000"
              label="FICTION / Men's Adventure"/>

    <summary type="text">The story of the son of the Bob and the gallant part
      he played in the lives of a man and a woman.</summary>
    <content type="text">The story of the son of the Bob and the gallant part
      he played in the lives of a man and a woman. Bob begins his humble life
      under the wandering eye of his senile mother, but quickly learns how to
      escape into the wilder world. Follow Bob as he uncovers his father's past
      and uses those lessons to improve the lives of others.</content>

    <link rel="http://opds-spec.org/image"
          href="/covers/4561.lrg.png"
          type="image/png"/>
    <link rel="http://opds-spec.org/image/thumbnail"
          href="/covers/4561.thmb.gif"
          type="image/gif"/>

    <link rel="self"
          href="/opds-catalogs/entries/4571.complete.xml"
          type="application/atom+xml;type=entry;profile=opds-catalog"/>

    <link rel="http://opds-spec.org/acquisition"
          href="/content/free/4561.epub"
          type="application/epub+zip"/>
    <link rel="http://opds-spec.org/acquisition"
          href="/content/free/4561.mobi"
          type="application/x-mobipocket-ebook"/>
 </entry>


接著,就是關於把一堆書整理成清單的事情了,常見的清單有熱門書籍、最新書籍、依字母順序等等的,你想的到的都差不多有了:


http://opds-spec.org/sort/new
http://opds-spec.org/sort/popular
http://opds-spec.org/featured
http://opds-spec.org/crawlable
http://opds-spec.org/shelf
http://opds-spec.org/subscriptions
subsection


其中 sort/new 顧名思義就是照發佈日期排序,而 sort/popular 則是熱門程度,其他的就沒有順序的要求,但也可以用 subsection 提供依照字母排序的清單,上述比較特別的有 crawlable 這個字樣,提供像 Search Engines 這類服務撈資料用的。


當書單很大時,該怎麼辦? OPDS Catalog 是建立在 Atom 上,因此可以提供分頁功能:


<link rel="self" href="alpha_1.xml" type="application/atom+xml;profile=opds-catalog"/>
<link rel="first" href="alpha.xml" type="application/atom+xml;profile=opds-catalog"/>
<link rel="last" href="alpha_2.xml" type="application/atom+xml;profile=opds-catalog"/>
<link rel="previous" href="alpha.xml" type="application/atom+xml;profile=opds-catalog"/>
<link rel="next" href="alpha_2.xml" type="application/atom+xml;profile=opds-catalog"/>


其中,每一頁應該都會提供 self, first 和 last 的資訊,剩下的就依照資料量跟頁數的資訊,提供 previous 跟 next 的資訊,例如第一頁不該有 previous 而最後一頁也不該有 next 等。



以上範例等資訊來自於:


http://opds-spec.org/specs/opds-catalog-1-0


更多細節請參考 OPDS Catalog 1.0


相機配腳架

相機配腳架


這是跟同事借的腳架,才過沒幾次,就覺得還挺不錯的,更多細節可以參考這邊:Joby Gorillapod - 章魚小腳架


這台相機是我家第二台數位相機,第一台相機已經壞了,是 Acer 5xxx 的相機!我家買相機都是買最便宜的,直到最近工作穩定才買了稍微好一點的相機,大概五千元吧!不多說,再放幾張這兩天是拍的,都是用夜景模式。


2010/09/30 18:10 公園宿舍 2010/09/29 公園宿舍


有時看到美景會迫不及待地想要拍,但轉個角度想想,難道在路上看到正妹就要去搭訕嗎?所以,一切還是隨緣拍吧


[Python] 使用 Comma-separated values (CSV)

Comma-separated values (CSV) 就是以逗號為欄位分隔,並以 line-based 的儲存格式,細節可參考 Wikipedia - Comma-separated values


在 Python 裡也有對應得 lib 可以用,只需 import csv 就行啦


範例:


csvfile = open( target_file , 'rb' )
dialect = csv.Sniffer().sniff(csvfile.read(1024))
dialect.escapechar = '\\'
dialect.lineterminator = '\n'

csvfile.seek(0)
for row in list(csv.reader( csvfile , dialect=dialect , delimiter=',' ) ):
        print "====" , len(row) , "===="  , row


安裝與修正 Hinedo 線上廣播軟體 @ Ubuntu 10.04

Hinedo Hinedo


Hinedo 是一套國人開發的免費廣播軟體,個人覺得十分夠用。然而在 Ubuntu 10.04 上安裝後,跑起來會有問題,其錯誤訊息:


$ /usr/bin/hinedo
Traceback (most recent call last):
  File "/usr/lib/hinedo/update", line 119, in <module>
    os.execl( dir_path + 'update_menu' )
  File "/usr/lib/python2.6/os.py", line 312, in execl
    execv(file, args)
ValueError: execv() arg 2 must not be empty


Hinedo


這應該只是 Python 版本的問題,僅需稍微修正即可:


$ sudo vim /usr/lib/hinedo/update
將最後一行 os.execl( dir_path + 'update_menu' ) 更新成


os.execl( dir_path + 'update_menu' , '' )


只是多加一個空白的參數而已,如此一來就能正常使用囉


另外,安裝上若想要編原始碼,那就安裝一下其他部分:


$ sudo apt-get install build-essential libgtk2.0-dev
$ wget http://www.openfoundry.org/of/download_path/hinedo/2007.11.18/hinedo-0.4.tar.bz2
$ tar -xvf hinedo-0.4.tar.bz2 && cd hinedo-0.4 && make && make install


2010年9月27日 星期一

[Python] MARC21 與 ISO 2709 筆記

這陣子接觸圖書館服務,其中關於書目清單底層匯出的格式採用 MARC 格式,也是 ISO 2709 格式,相關資料如下:



花一點時間,總算看懂了。請看 Library of Congress >> MARC >> Authority >> LeaderMARC的結構 來對照,因為有時我竟然看不太懂中文!


圖書館系統理論上都支援 MARC 的匯出,其中匯出的資料採用 ISO 2709 格式,而 ISO 2709 就是以前磁帶備份的格式。MARC 匯出的資料格式,如同 Wikipedia - ISO_2709 底部那個看不懂的範例,因為 MARC 本身就叫 MAchine-Readable Cataloging 而非 Human-Readable Cataloging,但也有接近人眼看得懂得 MARC XML 格式,但不在這篇的討論。


MARC的結構 看看老故事,得知資料都是 Sequence 並且每一筆前 24 bytes 就等同於 record begin delimiter。而 MARC 每一筆 Record 共分成 header + dictionary + data 三個部份。而 header 裡 12-16 bytes 就是紀錄接下來的 dictionary 的大小是多少,當然也可以用它計算出直接取得 data 位置。而 dictionary 主要都是 12 bytes 為單位,分別是 3 bytes, 4 bytes, 5 bytes,但 dictionary 紀錄的大小是 "12 的倍數 + 1",細節可在 MARC的結構 得知。


切 Records:


def pre_process():
        target = 'marc_data'
        f = open( target , 'rb' )
        rec_cnt = 0
        total_size = 0
        print "### 012345678901234567890123 ###"
        while True:
                header = f.read(24)
                total_size  = total_size + len( header )
                if not header:
                        break

                record_size = int( header[0:5] )
                record_data = f.read( record_size - 24 )

                total_size  = total_size + len( record_data )
                rec_cnt = rec_cnt + 1

                print "---",header,"---",record_size
                if False :
                        o = open( '/tmp/marc.'+str(rec_cnt) , 'wb' )
                        o.write( header )
                        o.write( record_data )
                        o.close()
                #print record_data

        print "Total:",total_size,", Record Cnt:",rec_cnt
        f.close


從 header 這 24 bytes 資料,其前五個 Bytes 記錄的就是該 Record 大小(包括header)


對指定的 Record 分析 Header & 回傳指定 field 的 values:


def getFieldValue( rawdata , field = None , dictField = None ):
        if dictField is None:

                header = rawdata[0:24]
                field_length = int( header[20:21] )
                field_offset = int( header[21:22] )
                data_begin_offset = int( header[12:17] )
                raw_field_info = rawdata[24:data_begin_offset - 1]      # skip field end delimiter

                dictField = {}
                for i in range( 0 , len(raw_field_info) , 12 ):
                        begin = i
                        end = i+3
                        sub_field_name = raw_field_info[ begin : end ]

                        begin = end
                        end = begin + field_length
                        sub_field_data_length = raw_field_info[ begin : end ]

                        begin = end
                        end = begin + field_offset
                        sub_field_data_offset = raw_field_info[ begin : end ]

                        if sub_field_name not in dictField:
                                dictField[ sub_field_name ] = []
                        dictField[ sub_field_name ].append( [ int(sub_field_data_length) , int(sub_field_data_offset) + data_begin_offset ] )

        out = []
        if field is not None and field in dictField:
                #print dictField[field]
                for data_length_and_offset in dictField[field]:
                        out.append( rawdata[ data_length_and_offset[1] : data_length_and_offset[0] + data_length_and_offset[1] ] )

        return ( out , dictField )


用法:


tmp = None
value , tmp = getFieldValue( rawdata , '003' , tmp )
value , tmp = getFieldValue( rawdata , '005' , tmp )

...


其中 rawdata 是完整的資料,包括 header + dinctionary + data 三部分;value 是一個 array ,因為有些指定的 field name 可能出現多次,所以就用 array 記錄; tmp 是用來暫存 dictionary 資料,可以省下重新處理來增加效率的


建個 class 使用:


class MARC( object ):
        def __init__ ( self , file_list=[] ):
                self.file_list = file_list if file_list is not None and len(file_list) > 0 else []
                self.fd = None
                self.RE_FIELD_DATA = re.compile( '\x1f.([^\x1e\x1f]+)' )

        def get_raw_entries( self , cnt = None ):
                out = []
                cnt = int(cnt) if cnt is not None else 0
                while True:
                        if self.fd is None:
                                if  self.file_list is None or len( self.file_list ) == 0 :
                                        return out
                                try:
                                        self.fd = open( self.file_list[0] , 'rb' )
                                        self.file_list = self.file_list[1:]
                                except Exception as inst:
                                        print inst
                                        return out
                        try:
                                header = self.fd.read( 24 )
    
                                if not header:  # EOF
                                        self.fd.close()
                                        self.fd = None
                                else:
                                        record_size = int( header[0:5] )
                                        record_data = self.fd.read( record_size - 24 )
                                        out.append( header + record_data )
                        except Exception as inst:
                                print inst
                                return out
    
                        if cnt != 0 and len(out) == cnt:
                                return out

        def get_field_value( self , rawdata , field , dictField = None ):
                if dictField is None:

                        header = rawdata[0:24]
                        field_length = int( header[20:21] )
                        field_offset = int( header[21:22] )
                        data_begin_offset = int( header[12:17] )
                        raw_field_info = rawdata[24:data_begin_offset - 1]      # skip field end delimiter

                        dictField = {}
                        for i in range( 0 , len(raw_field_info) , 12 ):
                                begin = i
                                end = i+3
                                sub_field_name = raw_field_info[ begin : end ]

                                begin = end
                                end = begin + field_length
                                sub_field_data_length = raw_field_info[ begin : end ]

                                begin = end
                                end = begin + field_offset
                                sub_field_data_offset = raw_field_info[ begin : end ]

                                if sub_field_name not in dictField:
                                        dictField[ sub_field_name ] = []
                                raw_value = [ int(sub_field_data_length) , int(sub_field_data_offset) + data_begin_offset ]
                                dictField[ sub_field_name ].append( raw_value )

                out = []
                if field is not None and field in dictField:
                        for data_length_and_offset in dictField[field]:
                                out.append( rawdata[ data_length_and_offset[1] : data_length_and_offset[0] + data_length_and_offset[1] ] )

                return ( out , dictField )


使用方式:


marc = MARC( [target_file] )

for rawdata in marc.get_raw_entries():
        tmp = None
        value , tmp = marc.get_field_value( rawdata , 'FIELD_ID' , tmp )
        if len(value) > 0:
                for raw in re.findall( marc.RE_FIELD_DATA , value[0] ):
                        print raw
                        break


最後一提,其實有 pymarc libary 可以用:http://pypi.python.org/pypi/pymarc/,而我要做的事也差不多搞定,所以就不用那個 lib 囉