Help us understand the problem. What is going on with this article?

新刊コミックの情報を返すAPIを作成する

More than 5 years have passed since last update.

漫画好きとしては日々発売される新刊のチェックは欠かせないのですが、正直面倒でもあります。
もっとお手軽に新刊チェックできないかと思い、iPhone用のアプリケーションを作ろうかと考えました。
しかしiPhoneアプリはまだまともに組んだことがないので、とりあえずiPhone上のアプリケーションに新刊コミックの情報をJSONで返すサーバー側のAPIをちゃっちゃと書いてみることにしました。

新刊コミックの情報を取得する

まず新刊コミックの情報をどこから取得するかですが、今回は「コミックナタリー」の「本日発売の単行本リスト」の記事をスクレイピングすることにしました。新刊コミックの情報を提供しているサイトは幾つかありますが、RSSを配信している、amazonへのリンクがある、HTMLの構造が単純でスクレイピングしやすいという理由で「コミックナタリー」に決めました。

処理の大まかな流れは、

1.「コミックナタリー」のRSSから「本日発売の単行本リスト」のURLを取得する
2.「本日発売の単行本リスト」ページの内容を取得する
3. HTMLをスクレイピングして新刊情報を取得する
4. 新刊情報をデーターベースに保存する。

という感じでしょうか。

ほぼ流れのままにrubyでサクッと書いてみました。

get_new_comics.rb
# コミックナタリーの「本日発売の単行本リスト」より書籍情報をスクレイピングして
# MySQLに登録する

require 'rss'
require 'open-uri'
require 'nokogiri'
require 'mysql2'

# コミックナタリーのRSSから新刊情報のURLを取得する
def get_urls(feed_url)
  rss = RSS::Parser.parse(feed_url)
  rss.items.select {|item| item.title.content =~ /本日発売の単行本リスト/}
           .map {|item| item.link.href}
end

# URLからアフィリエイトIDをカットする
def strip_affiliate_id(url)
    url.gsub(/[^\/]+$/, "")
end

# amazonの商品URLからASIN値を取得する
def asin(url)
  if url =~ /ASIN\/(\d+)\//
    $1
  else
    ""
  end
end

# 新刊書籍情報を取得する
def get_comic_info(url)

  books = []

  html = open(url).read
  doc = Nokogiri::HTML.parse(html)

  release_date = nil
  if doc.title =~ /【(\d+)月(\d+)日付】/
    release_date = Time.local(Time.now.year, $1.to_i, $2.to_i)
  end

  doc.xpath("//div[@class='NA_articleBody']").each do |div|
    publisher = nil
    div.elements.each do |elm|
      if elm.name == "h4"
        publisher = elm.inner_text
      end
      if elm.name == "p" && !publisher.nil?
        elm.inner_html.split("<br>").each do |line|
          if line =~ /^<a href="(.+?)".*>.*「(.+)」.*<\/a>(.+)$/
            books << {
                title: $2.strip,
                author: $3.strip,
                publisher: publisher,
                link: strip_affiliate_id($1),
                asin: asin($1),
                release_date: release_date
              }
          end
        end
      end
    end
  end

  books
end

# 同一のASINを持つデータがDB上に存在するかチェック
def is_exists(asin, client)
    escaped_string = client.escape(asin)
    sql = "SELECT COUNT(*) as count FROM books WHERE asin = '#{escaped_string}'"
    client.query(sql).each do |row|
        return row['count'] > 0
    end
    false
end

# 挿入用のSQL文を生成する
def insert_sql(book, client)
  fields = book.keys
  values = []
  fields.each do |f|
    if (f == :release_date)
      if (book[f])
        values << "'#{book[f].strftime('%Y-%m-%d')}'"
      else
        values << "NULL"
      end
    else
      escaped_string = client.escape(book[f])
      values << "'#{escaped_string}'"
    end
  end
  "INSERT INTO books (#{fields.map(&:to_s).join(',')}) " +
  "VALUES (#{values.join(',')})"
end

##
## PROGRAM START
##

# コミックナタリーのRSS
FEED_URL = "http://natalie.mu/comic/feed/news"
# MySQL接続用のアカウントとパスワード
DB_USER = "XXXXX"
DB_PASSWORD = "XXXXX"

# MySQLに接続する
client = Mysql2::Client.new(host: 'localhost', username: DB_USER, password: DB_PASSWORD, database: 'comic_db')

# URLのリストを順次処理する
urls = ARGV.empty? ? get_urls(FEED_URL) : ARGV
urls.each do |url|
  # コミックナタリーの新刊情報ページより書籍情報を取得する
  get_comic_info(url).each do |book|
    # 同一ASINの書籍がDBに存在しなければ登録する
    unless is_exists(book[:asin], client)
      client.query(insert_sql(book, client))
    end
  end
end

取得した新刊情報はMySQL上のbooksというテーブルに保存するようにしました。

CREATE TABLE `books` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(250) DEFAULT NULL,
  `author` varchar(250) DEFAULT NULL,
  `publisher` varchar(250) DEFAULT NULL,
  `link` varchar(250) DEFAULT NULL,
  `asin` varchar(20) DEFAULT NULL,
  `release_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `IDX_ASIN` (`asin`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

このrubyのスクリプトをcronで毎日実行してやれば、日々発売される新刊コミックの情報がサーバー上のデータベースに勝手に溜まっていくという寸法です。

新刊情報を取得するAPI

続いて、サーバー上のデータベースに保存された新刊情報をJSON形式で返すAPIです。
パラメーターとして日付を渡して、該当日に発売されるコミックの情報をJSONで返すという単純なものです。
フレームワークとか大袈裟なものは使わず素のPHPでぱっぱと書いてしまいます。

パラメータを取得するとことかもっとマシに書けそうな気がしますが気にしないでおきます。

comic_list.php
<?php
    # 指定した日付の新刊書籍のリストをJSONで返す。
    # Using : http://foo.bar/api/comic_list.php?dt=20150101
    # 引数
    #  dt:8桁(年4桁、月2桁、日2桁)の日付を示す数値(省略時は当日)

    # DB接続用の定数
    const DB_CONNECTION_STRING = "mysql:host=localhost;dbname=comic_db;charset=utf8";
    const DB_USER = "XXXXX";
    const DB_PASSWORD = "XXXXX";

    # パラメーターを取得する
    function getParams()
    {
        if (empty($_GET["dt"]))
        {
            $dt = new DateTimeImmutable();
        }
        else
        {
            if (preg_match("/^(\d{4})(\d{2})(\d{2})$/", $_GET["dt"], $m))
                $dt = new DateTimeImmutable("{$m[1]}-{$m[2]}-{$m[3]}");
            else
                throw new Exception("Invalid format");
        }
        return [":dt" => $dt->format("Y-m-d")];
    }

    # DBより書籍情報を取得する
    function getBooks($params)
    {
        $books = [];
        $conn = new PDO(DB_CONNECTION_STRING,DB_USER,DB_PASSWORD);
        $sql = "SELECT * FROM books WHERE release_date = :dt ORDER BY id";
        $stmt = $conn->prepare($sql);
        if ($stmt->execute($params))
        {
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
            {
                $books[] = $row;
            }
        }
        return $books;
    }

    #
    # PROGRAM START
    #
    try
    {
        # パラメータを取得する
        $params = getParams();

        # 書籍情報を取得する。
        $books = getBooks($params);

        # 書籍情報をJSONで返す。
        header("Content-Type: application/json");
        echo json_encode($books, JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES);
    } 
    catch (Exception $e)
    {
        # 例外が発生した
        exit($e->getMessage());
    }

サーバーに設置してアクセスしてみると、

[
{"id":"13","title":"侯爵ともつれた愛の糸","author":"綾部瑞穂/バーバラ・カートランド","publisher":"宙出版","link":"http://www.amazon.co.jp/exec/obidos/ASIN/4776739143/","asin":"4776739143","release_date":"2015-01-05 00:00:00"},
{"id":"14","title":"平安ちょこっと恋絵巻(1)","author":"卯崎ひとみ","publisher":"宙出版","link":"http://www.amazon.co.jp/exec/obidos/ASIN/4776739186/","asin":"4776739186","release_date":"2015-01-05 00:00:00"},
{"id":"15","title":"ハリウッドスターの恋人","author":"酒井美羽","publisher":"宙出版","link":"http://www.amazon.co.jp/exec/obidos/ASIN/4776739135/","asin":"4776739135","release_date":"2015-01-05 00:00:00"},{"id":"16","title":"復讐の味は恋の味","author":"橋本多佳子/シャーリー・ジャンプ","publisher":"宙出版","link":"http://www.amazon.co.jp/exec/obidos/ASIN/4776739151/","asin":"4776739151","release_date":"2015-01-05 00:00:00"},
{"id":"17","title":"天に恋う(4)","author":"望月桜/梨千子","publisher":"宙出版","link":"http://www.amazon.co.jp/exec/obidos/ASIN/4776739178/","asin":"4776739178","release_date":"2015-01-05 00:00:00"},
... 略 ...
]

という感じのJSONが取得できたので、何とか動いているようだ。

次は、このAPIを呼び出すiPhone用のアプリケーションだけど、デベロッパー契約するとこから始めないと...

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした