玩式草子─ソフトウェアとたわむれる日々

第57回「らじる★らじる」もう一度(その2)

前回NHKのNow On Air情報(以下NOA情報)のURLにアクセスすれば、⁠らじる★らじる」で放送している番組のタイトルや曲目情報等がJSON形式で入手できることを紹介しました。

入手できる情報は番組によって異なるものの、たいていの音楽番組では放送した曲目一覧を提供しているので、それらをファイル名と紐づけて記録しておけば、聞きたい番組や曲を探す時に便利でしょう。そう考えて、NOA情報から得た番組情報をデータベース化するスクリプトを書いてみました。

番組情報データベースの設計

かってエア・チェックを趣味にしていた人間としては、前回紹介した専門誌の番組表のように、出演者や曲目を簡単に一覧できることが理想です。

そのため番組情報をテキストベースで集積していこうかとも考えたものの、多数の曲目リストを収めなければならない音楽番組と、せいぜい出演者名しか不要な語学番組を1つの表に収めるのも見通しが悪そうです。

そこで項目数を固定にした番組名のリスト任意数の曲目を登録できる内容リストは別の表として整理することにしました。表(テーブル)形式のデータを操作するなら、SQLで利用できるデータベースの形にしてしまうのが簡単そうです。

前回の最後に、NOAから得られる番組情報の一覧を示しました。これらのうち、indexやhashtag、linkやrebroad等の項目は特に保存しておく必要はなさそうです。また、subtitleやcontentとして登録されている情報はfreeの一部を取り出しているだけなので、freeを記録するようにすれば不要でしょう。そのあたりを考慮して、番組名リストと内容リストは以下のような形式にしました。

番組名リスト
番組IDファイル名日付チャンネルタイトル出演者
内容リスト
番組ID曲名 1
番組ID曲名 2
番組ID曲名 3

前者の番組名リストは1つの番組を1行に記録し、各番組には一意のID番号を振ります。一方、後者の内容リストは、free行に記録された曲目等の情報が、番組名リストのIDに紐づけられて任意の数だけ並んでいく、というシンプルな構造です。

だいたいのアイデアがまとまったので、これらの操作を使いなれたPythonとSQLite3で書いてみることにしました。

DB登録用スクリプト

まずデータベースを作成(初期化)する部分を作ります。SQLite3の場合、データベースは1つのファイルに保存されるので、使用するファイル名を引数として受けとることにしました。その中に含まれる番組名リストはtitlesというテーブル名、内容リストはcontentsという名前にしました。

def init_db(dbname):
    conn = sqlite3.connect(dbname)
    conn.isolation_level = None
    cursor = conn.cursor()
    cursor.execute('''create table titles
       (filename text, date text, ch text, title text, act text)''')
    cursor.execute('''create table contents
       (id int, list text)''')
    return conn

create tableしている部分を見ると気づくように、番組名リスト(titlesテーブル)の定義の中に先に示した「番組ID」はありません。これはSQLite3の場合、各行には自動的にROWIDと呼ばれるID番号が振られるので、このROWIDを番組IDに流用すればいいだろう、と判断したためです。

次に、作成したテーブルにデータを登録する部分を作りました。テーブルはtitlesとcontentsに分かれているので、登録用の関数も2つに分けています。Pythonの場合、複数のデータをひとかたまりにしたタプルと呼ばれるデータ構造が利用できるので、登録すべきデータはタプル(t)で渡し、書き込み先のデータベースはカーソルオブジェクト(cursor)で渡しています。タプル内のデータは、スクリプト中に"?"で指定したプレースホルダーに展開されるため、テーブルの構造に従ったタプルはこの関数を呼び出す側で用意することにして、登録用関数では最低限の処理に留めています。

def insert_title(cursor, t):
    try:
        cursor.execute('insert into titles values(?, ?, ?, ?, ?)', t)
    except sqlite3.Error, e:
        print "An error occurred:", e.args[0]

contentsテーブルにデータを登録する関数もほぼ同じですが、登録すべき項目は番組IDと曲目だけなので、登録するデータは2つだけです。

def insert_contents(cursor, t):
     try:
         cursor.execute('insert into contents values(?, ?)', t)
     except sqlite3.Error, e:
         print "An error occurred:", e.args[0]

次に、前回紹介した操作を元に、JSON形式でNOA情報を取り込む部分を作りました。urllib2.urlopen()は指定したURLを開くための関数で、インターネット上のURLをファイルと同じように操作できるオブジェクトを返します。このオブジェクトにread()メソッドを適用し、必要なデータを読み出します。

前回紹介したように、NOAサイトから読み出したデータには冒頭と末尾に余計なデータが付いているので、それらをはぎとって正式なJSON形式にしてからjson.loads()で読みこみ、Pythonの辞書型データとして返します。

def get_json_data():
    req = urllib2.urlopen('http://www2.nhk.or.jp/hensei/api/noa.cgi?c=3&wide=1&mode=jsonp')
    response = req.read()
    data = json.loads(response.lstrip('nowonair(').rstrip(');'))
    return data

NOA情報にはラジオ第一、第二、FMの3チャンネル分の番組情報が含まれているので、必要なデータを取り出す際にはどのチャンネルのデータを使うかを決める必要があります。また、データベースに登録するファイル名も外部から与える必要があるので、このスクリプトでは引数としてチャンネルファイル名を受けとることにしました。

def main():
    channel = sys.argv[1]
    title = sys.argv[2]

    if channel == 'fm':
        ch = '001netfm0'
    elif channel == 'r1':
        ch = '001netr10'
    elif channel == 'r2':
        ch = '001netr20'
    else:
        print("{} is not a valid channel(fm, r1, r2)".format(channel))
        sys.exit(1)

最初の引数で指定するchannelはfm、r1、 r2の3択で、それぞれをNOA情報のチャンネル名に変換しています。また、必要なのは現在放送中の番組情報のみなので、末尾のインデックスは0に揃えました。

データベースと接続する部分はこんな感じにしてみました。データベースファイルは~/MP3/radiru_titles.sql3とし、このファイルが無ければ前述の初期化処理でデータベースを作成し、ファイルがあればデータベースとして接続します。

    dbname = os.path.expanduser("~/MP3/radiru_titles.sql3")
    if os.access(dbname, os.R_OK) == False:
        connection = init_db(dbname)
        cursor = connection.cursor()
    else:
        connection = sqlite3.connect(dbname)
        cursor = connection.cursor()

あとは実際にNOA情報を読み込み、指定したチャンネルの情報を取り出して、データベースに記録する作業です。

まずは get_json_data() を使ってNOA情報を取り込み、指定したチャンネル(ch)の情報から、⁠番組タイトル(title⁠⁠出演者(act⁠⁠録音日時(date⁠⁠」の情報を取り出します。

    data = get_json_data()
    title = check_char(data[ch]['title'])
    act = check_char(data[ch]['act'])
    date = data[ch]['starttime']

前回も見たように、JSON形式で使用する文字コードはUTF-8で、SQLite3も保存するテキストデータの文字コードもUTF-8なので、JSON形式から切り出したデータはそのまま(文字コード変換なしに)記録することができます。

一方、コマンドラインから引数として渡されるファイル名はロケールで指定された文字コードになるため、SQLite3に記録するためには文字コードを変換する必要があります。

書き込むべきデータが揃えば、それらをinsert_title()に渡してデータベースに書き込んだ後、書き込んだ行のROWIDを取り出しておきます。

    filename = title.decode('euc-jp')
    insert_title(cursor, (filename, date, ch, title, act))
    connection.commit()
    rowid = cursor.lastrowid

次に、番組情報のうちfree行の部分を取り出して、改行記号が2つ並ぶ(=1行空け)ごとに分割して、それぞれをtitlesテーブルのROWIDと紐づけて、contentsテーブルに書きこんで行きます。

    txt = check_char(data[ch]['free'])
    cols = txt.split('\\n\\n')
    for i in cols:
        tmp = i.replace('\\n', '\n')
        insert_contents(cursor, (rowid, tmp))
        connection.commit()

動作確認

以上、紹介したリストを1つにまとめ、モジュールのimport処理やmain()関数の呼び出し処理を付けたスクリプト全体をリスト1に示します。なおリスト1には、前節では紹介を省いたcheck_char()の処理と共に、デバッグ用に情報をプリントアウトする処理もいくつか追加しています。

このスクリプトを実際に動かすと、このような動作になります。

$ ./radiru_noa.py r1 test-r1
channel: 001netr10
title:午後のまりやーじゅ▽ロックンローラー近田春夫の歌謡曲って何だ?
act:
date:2014-04-25 16:05:00
ROWID:1
1:山田まりや
近田春夫
中倉隆道
1:▽ロックンローラー近田春夫の歌謡曲って何だ?
「まりやの歌謡曲って何だ?」
1:<ニュース(4:30)>

$ ./radiru_noa.py r2 test-r2
channel: 001netr20
title:みんなのうた「きみのほっぺ」「おいら歌舞伎のぬらりんひょん」
act:井上あずみ, YuReeNa, ひまわり屋
date:2014-04-25 16:25:00
ROWID:2
2:

$ ./radiru_noa.py fm test-fm
channel: 001netfm0
title:オペラ・ファンタスティカ -メトロポリタン歌劇場“ウェルテル”-
act:
date:2014-04-25 14:00:00
ROWID:3
3:堀内修
- メトロポリタン歌劇場“ウェルテル” -
3:「歌劇“ウェルテル”第1幕」 マスネ作曲
(41分32秒)
「歌劇“ウェルテル”第2幕」 マスネ作曲
(32分10秒)
「歌劇“ウェルテル”第3幕、第4幕」 マスネ作曲
(57分57秒)
ウェルテル…(テノール)ヨナス・カウフマン
シャルロッテ…(メゾ・ソプラノ)ソフィー・コッシュ
アルベルト…(バリトン)デヴィッド・ビジック
役人…(バス)ジョナサン・サマーズ
シュミット…(テノール)トニー・スティーヴンソン
...

radiru_noa.pyスクリプトが記録するSQLite3形式のデータベースはsqlite3コマンドを用いて直接眺めることもできるので、データベースにどのようなデータが登録されたのかを調べてみましょう。

なお、Plamo Linuxの場合、デフォルトの文字コードはEUC-JPになっていて、そのままではsqlite3コマンドの出力が文字化けするので、端末の文字コードをUTF-8に変更して操作する必要があります。

$ sqlite3 radiru_titles.sql3 
SQLite version 3.8.0.2 2013-09-03 17:11:13
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select * from titles;
test-r1|2014-04-25 16:05:00|001netr10|午後のまりやーじゅ▽ロックンローラー近田春夫の歌謡曲って何だ?|
test-r2|2014-04-25 16:25:00|001netr20|みんなのうた「きみのほっぺ」「おいら歌舞伎のぬらりんひょん」|井上あずみ, YuReeNa, ひまわり屋
test-fm|2014-04-25 14:00:00|001netfm0|オペラ・ファンタスティカ -メトロポリタン歌劇場“ウェルテル”-|

$ sqlite> select * from contents where id=1;
1|山田まりや
近田春夫
中倉隆道
1|▽ロックンローラー近田春夫の歌謡曲って何だ?
「まりやの歌謡曲って何だ?」
1|<ニュース(4:30)>

$ sqlite> select * from contents where id=3;
3|堀内修
- メトロポリタン歌劇場“ウェルテル” -
3|「歌劇“ウェルテル”第1幕」 マスネ作曲
(41分32秒)
「歌劇“ウェルテル”第2幕」 マスネ作曲
(32分10秒)
「歌劇“ウェルテル”第3幕、第4幕」 マスネ作曲
(57分57秒)
ウェルテル…(テノール)ヨナス・カウフマン
シャルロッテ…(メゾ・ソプラノ)ソフィー・コッシュ
アルベルト…(バリトン)デヴィッド・ビジック
役人…(バス)ジョナサン・サマーズ
シュミット…(テノール)トニー・スティーヴンソン
...

無事、NOA情報がデータベースファイルに記録されているようです。

以前紹介したように、手元では「らじる★らじる」のタイマー録音は、radiru_rec.pyスクリプトが生成するシェルスクリプトを、atコマンドで指定した時刻に実行することで実現しています。そのため、radiru_rec.pyスクリプトが生成するシェルスクリプトに、今回作成したradiru_noa.pyスクリプトを呼び出す処理を追加すれば、録音した番組の情報をデータベースに自動的に追加していくことができます。まずはこうしてタイマー録音した番組の情報をデータベースに記録するようにしてみました。

リスト1 radiru_noa.py
  1  #! /usr/bin/python
  2  # -*- coding: utf-8 -*-;
  3  
  4  import sqlite3, os, sys, json, urllib2
  5  
  6  def check_char(s):
  7      table = { 
  8          u'/':u'/',
  9          u'\u2014':u'-',
 10          u'\u2015':u'-',
 11          u'\u2160':u'I',
 12          u'\u2161':u'II',
 13          u'\u2162':u'III',
 14          u'\u2163':u'IV',
 15          u'\u2164':u'V',
 16          u'\u2165':u'VI',
 17          u'\u2166':u'VII',
 18          u'\u2167':u'VIII',
 19          u'\u2168':u'IX',
 20          u'\u2169':u'X',
 21          u'\u216A':u'XI',
 22          u'\u216B':u'XII',
 23          u'\u216C':u'L',
 24          u'\u216D':u'C',
 25          u'\u216E':u'D',
 26          u'\u216F':u'M',
 27          u'\u2170':u'i',
 28          u'\u2171':u'ii',
 29          u'\u2172':u'iii',
 30          u'\u2173':u'iv',
 31          u'\u2174':u'v',
 32          u'\u2175':u'vi',
 33          u'\u2176':u'vii',
 34          u'\u2177':u'viii',
 35          u'\u2178':u'ix',
 36          u'\u2179':u'x',
 37          u'\u217A':u'xi',
 38          u'\u217B':u'xii',
 39          u'\u217C':u'l',
 40          u'\u217D':u'c',
 41          u'\u217E':u'd',
 42          u'\u217F':u'm',
 43          u'\u2180':u'1000',
 44          u'\u2181':u'5000',
 45          u'\u2182':u'10000',
 46          u'\u2183':u'r100',
 47          u'\uff5e':u'~',
 48          u'\u2460':u'(1)',
 49          u'\u2461':u'(2)',
 50          u'\u2462':u'(3)',
 51          u'\u2463':u'(4)',
 52          u'\u2464':u'(5)',
 53          u'\u2465':u'(6)',
 54          u'\u2466':u'(7)',
 55          u'\u2467':u'(8)',
 56          u'\u2468':u'(9)',
 57          u'\uff0d':u'-',
 58          u'\ufeff':u' ',
 59          }
 60  
 61      L=[]
 62      for i in table.keys():
 63          L.append(i)
 64  
 65      new_str = ''
 66      for i in s[:]:
 67          if i in L:
 68              new_str = new_str + table[i]
 69          else:
 70              new_str = new_str + i
 71  
 72      return new_str
 73  
 74  def init_db(dbname):
 75      conn = sqlite3.connect(dbname)
 76      conn.isolation_level = None
 77      cursor = conn.cursor()
 78      cursor.execute('''create table titles
 79         (filename text, date text, ch text, title text, act text)''')
 80      cursor.execute('''create table contents
 81         (id int, list text)''')
 82      return conn
 83  
 84  def insert_title(cursor, t):
 85      try:
 86          # print "inserting ", t
 87          cursor.execute('insert into titles values(?, ?, ?, ?, ?)', t)
 88      except sqlite3.Error, e:
 89          print "An error occurred:", e.args[0]
 90  
 91  def insert_contents(cursor, t):
 92      try:
 93          # print "inserting ", t
 94          cursor.execute('insert into contents values(?, ?)', t)
 95      except sqlite3.Error, e:
 96          print "An error occurred:", e.args[0]
 97  
 98  def get_json_data():
 99      req = urllib2.urlopen('http://www2.nhk.or.jp/hensei/api/noa.cgi?c=3&wide=1&mode=jsonp')
100      response = req.read()
101      data = json.loads(response.lstrip('nowonair(').rstrip(');'))
102      return data
103  
104  def main():
105      channel = sys.argv[1]
106      title = sys.argv[2]
107  
108      if channel == 'fm':
109          ch = '001netfm0'
110      elif channel == 'r1':
111          ch = '001netr10'
112      elif channel == 'r2':
113          ch = '001netr20'
114      else:
115          print("{} is not a valid channel(fm, r1, r2)".format(channel))
116          sys.exit(1)
117  
118      dbname = os.path.expanduser("~/MP3/radiru_titles.sql3")
119      if os.access(dbname, os.R_OK) == False:
120          connection = init_db(dbname)
121          cursor = connection.cursor()
122      else:
123          connection = sqlite3.connect(dbname)
124          cursor = connection.cursor()
125  
126      data = get_json_data()
127      title = check_char(data[ch]['title'])
128      act = check_char(data[ch]['act'])
129      date = data[ch]['starttime']
130  
131      print("channel: {}".format(ch))
132      print("title:{}".format(title.encode('euc-jp')))
133      print("act:{}".format(act.encode('euc-jp')))
134      print("date:{}".format(date))
135  
136      filename = title.decode('euc-jp')
137      insert_title(cursor, (filename, date, ch, title, act))
138      connection.commit()
139      rowid = cursor.lastrowid
140      print("ROWID:{}".format(rowid))
141  
142      txt = check_char(data[ch]['free'])
143      cols = txt.split('\\n\\n')
144      for i in cols:
145          tmp = i.replace('\\n', '\n')
146          insert_contents(cursor, (rowid, tmp))
147          print("{}:{}".format(rowid, tmp.encode('euc-jp')))
148          connection.commit()
149  
150  if __name__ == "__main__":
151      main()

おすすめ記事

記事・ニュース一覧