Edited at

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

More than 3 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用のアプリケーションだけど、デベロッパー契約するとこから始めないと...