青空文庫API(アルファ版)を改良してみた

昨日公開した青空文庫API(アルファ版)ですが、id:hyukix あたりに「作品データがまったく取れないじゃないか!」と怒られそうな気がするので、作品データや青空文庫作品ページ等々の情報を吐き出すように修正してみました。

  • API的なアレはこちらです

http://plasticscafe.sakura.ne.jp/aozora/

青空文庫データまるごと公開

ちなみにパースした青空文庫情報(SQLite)は下記URLからダウンロード出来るようにしてみました。そこら辺の情報を使って何かやりたいんだけど、パースすんのはめんどいデス( ー`дー´)キリッ、という一ヶ月前の俺みたいな人はご自由にどうぞー

http://plasticscafe.sakura.ne.jp/aozora/aozora_db.tgz

実装小話

前回のエントリーで載せたインチキ実装コードでは、ランキングの処理結果のみデータ更新時にキャッシュファイルを生成していますデス。ランキングはカウントやらJOINやらで重たいSQLになる反面、検索結果が変わらないという性質なのでキャッシュしてみることにしてみました。

当初は毎回クエリー発行してたのですが、実際のところ1クエリー1分程度という泣きたくなるような速度だったのでしょうがなく…でもサービス提供系の実装を考えれば結果の変わらない処理は値を保持しておいたほうが効率的だよな…と思うようになったのでいいきっかけでしたとさ…まあそもそものSQLの作り方がアホという可能性は大ですが(´・ω・`)

他の検索なんかもある程度値をキャッシュするように考えていきたいところですが…まあ、時間があったらというところで。

公開周りの処理

前回はパーサー系の処理を載せたので、今回は公開系の処理をばデロデロと載せてみます。実際にはApache CGI de Python(造語)から下記クラスをインポートして利用している感じです…Python的にアウトな部分や、そもそもこの書き方おかしい的箇所があればお知らせいただければ…多そう(´;ω;`)なのでちょっとgkbrですが

#!/usr/bin/python                                                               
# -*- coding: utf-8 -*-

import sqlite3
import json
import re
import os
from xml.dom import minidom

class AozoraAPI:
  def __init__(self, work_dir=None):
    if work_dir == None:
      work_dir = os.path.dirname(os.path.abspath(__file__)) + '/'
    self.work_dir = work_dir
    self.con = sqlite3.connect(work_dir + 'db/aozora.db')
    self.file_url = 'http://www.aozora.gr.jp/cards/'
  # execute ranking sql
  def ranking(self, category=None):
    cache_dir = self.work_dir + 'cache/' 
    if category == 'html':
      f = open(cache_dir + 'ranking_html.json')
    elif category == 'text':
      f = open(cache_dir + 'ranking_text.json')
    else:
      f = open(cache_dir + 'ranking.json')
    return f.read()
  # create artists search sql
  def artists(self, keyword=None):
    sql ="""
    SELECT id, name, url 
    FROM artists 
    WHERE status != 2 
    """
    if keyword == None:
      return self.con.execute(sql).fetchall()
    else:
      search_sql = """
      AND (name LIKE '%s'
      OR name LIKE '%s'
      OR kana LIKE '%s'
      OR kana LIKE '%s'
      )
      """
      sql = sql + search_sql
      keyword_katakana = '%' + self.convertKana(keyword) + '%'
      keyword = '%' + keyword + '%'
      rows = self.con.execute(sql % (keyword, keyword_katakana, keyword, keyword_katakana)).fetchall()
      # format records
      results = []
      for row in rows:
        results.append({
          'artist_id':row[0],
          'artist_name':row[1],
          'url':row[2]
        })
      return results
  # execute artist get sql
  def artist(self, id=None):
    if id != None:
      sql ="""
      SELECT id, name, url 
      FROM artists 
      WHERE status != 2 AND id = '%s'
      """
      rows = self.con.execute(sql % (id)).fetchall()
      # format records
      results = []
      for row in rows:
        results.append({
          'artist_id':row[0],
          'artist_name':row[1],
          'url':row[2]
        })
      return results
    return None
  # execute articles search sql
  def articles(self, keyword=None):
    sql ="""
    SELECT ac.id, ac.name, ats.id, ats.name, ac.url, ac.zip_url, ac.html_url
    FROM articles AS ac
    LEFT JOIN artists AS ats ON ac.artist_id = ats.id
    WHERE ac.status != 2
    """
    if keyword == None:
      return self.con.execute(sql).fetchall()
    else:
      search_sql = """
      AND (ac.name LIKE '%s' 
      OR ac.name LIKE '%s'
      OR ac.kana LIKE '%s'
      OR ac.kana LIKE '%s')
      """
      sql = sql + search_sql
      keyword_katakana = '%' + self.convertKana(keyword) + '%'
      keyword = '%' + keyword + '%'
      rows = self.con.execute(sql % (keyword, keyword_katakana, keyword, keyword_katakana)).fetchall()
      # format records
      results = []
      for row in rows:
        results.append({
          'article_id':row[0],
          'article_name':row[1],
          'artist_id':row[2],
          'artist_name':row[3],
          'url':row[4],
          'zip':self.file_url + row[2]  + '/' + row[5],
          'html':self.file_url + row[2]  + '/' + row[6]
        })
      return results
  # execute article get sql
  def article(self, id=None):
    if id == None:
      sql ="""
      SELECT ac.id, ac.name, ats.id, ats.name, ac.url, ac.zip_url, ac.html_url
      FROM articles AS ac
      LEFT JOIN artists AS ats ON ac.artist_id = ats.id
      WHERE ac.status != 2 AND ac.id = '%s'
      """
      rows =  self.con.execute(sql % (id)).fetchall()
      # format records
      results = []
      for row in rows:
        results.append({
          'article_id':row[0],
          'article_name':row[1],
          'artist_id':row[2],
          'artist_name':row[3],
          'url':row[4],
          'zip':self.file_url + row[2]  + '/' + row[5],
          'html':self.file_url + row[2]  + '/' + row[6]
        })
      return results
    return None
  # execute article get sql
  def articlesFromArtist(self, id=None):
    if id == None:
      sql ="""
      SELECT ac.id, ac.name, ats.id, ats.name, ac.url, ac.zip_url, ac.html_url
      FROM articles AS ac
      LEFT JOIN artists AS ats ON ac.artist_id = ats.id
      WHERE ac.status != 2 AND ats.id = '%s'
      """
      rows = self.con.execute(sql % (id)).fetchall()
      # format records
      results = []
      for row in rows:
        results.append({
          'article_id':row[0],
          'article_name':row[1],
          'artist_id':row[2],
          'artist_name':row[3],
          'url':row[4],
          'zip':self.file_url + row[2]  + '/' + row[5],
          'html':self.file_url + row[2]  + '/' + row[6]
        })
      return results
    return None
  # execute all search sql
  def search(self, keyword=None):
    sql ="""
    SELECT ac.id, ac.name, ats.id, ats.name, ac.url, ac.zip_url, ac.html_url
    FROM articles AS ac
    LEFT JOIN artists AS ats ON ac.artist_id = ats.id
    WHERE ac.status != 2
    """
    if keyword == None:
      return self.con.execute(sql).fetchall()
    else:
      search_sql = """
      AND (ac.name LIKE '%s' 
      OR ac.name LIKE '%s'
      OR ac.kana LIKE '%s'
      OR ac.kana LIKE '%s'
      OR ats.name LIKE '%s' 
      OR ats.name LIKE '%s'
      OR ats.kana LIKE '%s' 
      OR ats.kana LIKE '%s')
      """
      sql = sql + search_sql
      keyword_katakana = '%' + self.convertKana(keyword) + '%'
      keyword = '%' + keyword + '%'
      rows = self.con.execute(sql % (keyword, keyword_katakana, keyword, keyword_katakana, keyword, keyword_katakana, keyword, keyword_katakana)).fetchall()
      # format records
      results = []
      for row in rows:
        results.append({
          'article_id':row[0],
          'article_name':row[1],
          'artist_id':row[2],
          'artist_name':row[3],
          'url':row[4],
          'zip':self.file_url + row[2]  + '/' + row[5],
          'html':self.file_url + row[2]  + '/' + row[6]
        })
      return results
  # execute newly articles
  def newly(self, limit=None):
    if limit == None:
        limit = '100'
    sql ="""
    SELECT ac.id, ac.name, ats.id, ats.name, ac.public_date, ac.url, ac.zip_url, ac.html_url
    FROM articles AS ac
    LEFT JOIN artists AS ats ON ac.artist_id = ats.id
    WHERE ac.status != 2
    ORDER BY ac.public_date DESC LIMIT '%s'
    """
    rows = self.con.execute(sql % (limit)).fetchall()
    # format records
    results = []
    for row in rows:
      results.append({
        'article_id':row[0],
        'article_name':row[1],
        'artist_id':row[2],
        'artist_name':row[3],
        'public_time':row[4],
        'url':row[5],
        'zip':self.file_url + row[2]  + '/' + row[6],
        'html':self.file_url + row[2]  + '/' + row[7]
      })
    return results
  # execute api command
  def api(self, command=None, param=None, display='plain'):
    if command == None:
      print('Content-type: text/plain; charset=UTF-8')
      print('')
      return False
    elif command == 'ranking':
      data = self.ranking(param)
      if display == 'json':
        print('Content-type: text/javascript; charset=UTF-8')
        print('')
        #print(json.dumps(data))
        print(data)
        return True
      elif display == 'plain':
        print('Content-type: text/plain; charset=UTF-8')
        print('')
        print(json.loads(data))
        return True
        
    elif command == 'artists':
      data = self.artists(param)
    elif command == 'artist':
      data = self.artist(param)
    elif command == 'articles':
      data = self.articles(param)
    elif command == 'article':
      data = self.article(param)
    elif command == 'articlesFromArtist':
      data = self.articlesFromArtist(param)
    elif command == 'search':
      data = self.search(param)
    elif command == 'newly':
      data = self.newly(param)
    else:
      data = False

    if display == 'plain':
      self.plain(data)
    elif display == 'json':
      self.json(data)
    #elif display == 'xml':
    #  self.xml(data)
    return True
        
  # display json
  def json(self, data):
    print('Content-type: text/javascript; charset=UTF-8')
    print('')
    print(json.dumps(data))
  # display plain text
  def plain(self, data):
    print(data)
  ##################################################
  # Utility

  # Convert String
  def convertKana(self, text):
    # dictionary of kana
    kana = {
      'ア':'あ', 'イ':'い', 'ウ':'う', 'エ':'え', 'オ':'お',
      'カ':'か', 'キ':'き', 'ク':'く', 'ケ':'け', 'コ':'こ',
      'サ':'さ', 'シ':'し', 'ス':'す', 'セ':'せ', 'ソ':'そ',
      'タ':'た', 'チ':'ち', 'ツ':'つ', 'テ':'て', 'ト':'と',
      'ナ':'な', 'ニ':'に', 'ヌ':'ぬ', 'ネ':'ね', 'ノ':'の',
      'ハ':'は', 'ヒ':'ひ', 'フ':'ふ', 'ヘ':'へ', 'ホ':'ほ',
      'マ':'ま', 'ミ':'み', 'ム':'む', 'メ':'め', 'モ':'も',
      'ヤ':'や', 'ユ':'ゆ', 'ヨ':'よ', 'ラ':'ら', 'リ':'り',
      'ル':'る', 'レ':'れ', 'ロ':'ろ', 'ワ':'わ', 'ヲ':'を',
      'ン':'ん',
      'ガ':'が', 'ギ':'ぎ', 'グ':'ぐ', 'ゲ':'げ', 'ゴ':'ご',
      'ザ':'ざ', 'ジ':'じ', 'ズ':'ず', 'ゼ':'ぜ', 'ゾ':'ぞ',
      'ダ':'だ', 'ヂ':'ぢ', 'ヅ':'づ', 'デ':'で', 'ド':'ど',
      'バ':'ば', 'ビ':'び', 'ブ':'ぶ', 'ベ':'べ', 'ボ':'ぼ',
      'パ':'ぱ', 'ピ':'ぴ', 'プ':'ぷ', 'ペ':'ぺ', 'ポ':'ぽ',
      'ァ':'ぁ', 'ィ':'ぃ', 'ゥ':'ぅ', 'ェ':'ぇ', 'ォ':'ぉ',
      'ャ':'ゃ', 'ュ':'ゅ', 'ョ':'ょ',
      'ヴ':'ゔ', 'ッ':'っ', 'ヰ':'ゐ', 'ヱ':'ゑ',
    }
    re_convert = re.compile("|".join(map(re.escape, kana)))
    return re_convert.sub(lambda x:kana[x.group(0)], text)

####################################################
# define main function
def main():
  ### テストコード
  import sys
  import codecs
  sys.stdout = codecs.getwriter('utf_8')(sys.stdout)
  aozora = AozoraAPI()
  aozora.api(None, None, 'json')
# when exexute this script
if __name__ == "__main__":
  main()