JavaScript: アロー関数で再帰

あけおめ、ことよろ。

正月早々、奇妙なものを見つけた。元記事は去年(というか昨日)のもので、タイトルの通りなんだけど、こんなのだった。

> const fibonacci = ((fb = n => n > 1 ? n * fb(n - 1) : 1) => fb)();

どう見ても階乗を求める関数なのに名前が fibonacci っておかしいだろ、というのは置いといて。確かに期待通りに動作する。

> fibonacci(5);
120 

元記事の説明によると:

  • アロー関数 n => n > 1 ? n * fb(n - 1) : 1 を変数 fb に代入し
  • その代入した関数を即時関数によって関数 fb として返すように
  • 代入(fibonacci)する

となってて、まあその通りではある。でも、その即時関数っていうのが単に引数をそのまま返してるだけなんだから、それ、いらねんじゃね?

というわけで即時関数なしでやってみると、やっぱり、ちゃんと期待通りに動作するじゃん。

> const fact = n => n > 1 ? n * fact(n - 1) : 1;
undefined
> fact(5);
120 

と、ここまで書いて気がついた。上では即時関数が云々と書いたけど、要するにその即時関数の引数であるアロー関数を変数 fb に代入してそれを呼び出してるだけだ。即時関数は何の関係もない。元記事の筆者がどこでネタを仕入れたのか知らないけど、不必要にトリッキーなだけだ。それとも以前のバージョンの JavaScript ではこう書かなきゃできなかったのかな。

ちなみに確認したのは Node v14.15.3。

[email protected]: takatoh > node -v
v14.15.3

というわけで、元記事から得たものといえば、関数呼び出しの引数部分に代入式が書けることか。使うかな、これ。

あ、あと、ブログのネタにはなった。

カテゴリー: JavaScript | コメントする

ReactのuseState、useEffectにハマる

……いや、React というより Fetch API のせいなのかもしれない。

ここのところで React で web アプリを作ってる。例によって自分で使うためだけのもの。

JSON を返す API を2つバックエンドに置いて、両方から取ってきたデータを合わせて表示する Collections コンポーネントを書いてたんだけど、いい書き方がわからずにハマった。

Collections コンポーネントは次のように動作する(のを期待している)。

  • 一方のAPIからコレクションのリストを取得する
  • リスト中のコレクションそれぞれについて、もう一方のAPIから詳細を取得する
  • 2つのAPIから取得したデータを合わせて、コレクションのリストとしてテーブル表示する

コレクションというのは、プログラミングにおけるデータ構造のことじゃなくて買い集めたコレクション(CDとかDVDみたいな)のこと。renderCollections 関数でレンダリングしているテーブルの行のうち、collection.id と collection.buy_date が一方のAPIから取得したデータ、collection.title と collection.brand がもう一方のAPIから取得したデータだ。

import React, { useState, useEffect } from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';


const Collections = (props) => {
  const perPage = 10;
  const [collections, setCollections] = useState([]);
  const [currentPage, setPage] = useState(0);

  const page = props.query.page ? parseInt(props.query.page) : 1;
  useEffect(
    () => {
      setPage(page);
      const offset = perPage * (page - 1);
      let colls = [];
      const fetchProduct = async (coll) => {
        const result = await fetch(`${props.api2Endpoint}/products/${coll.product_id}`)
          .then(response => response.json())
          .then(data => data["products"][0]);
        colls = [...colls, {...coll, title: result.title, brand: result.brand.name}];
        setCollections(colls);
      };
      const fetchData = async () => {
        const result = await fetch(`${props.api1Endpoint}/collections?limit=${perPage}&offset=${offset}`, { mode: "cors" })
          .then(response => response.json())
          .then(data => data["collections"]);
        for (const c of result) {
          fetchProduct(c);
        }
      };
      fetchData();
    },
    []
  );

  return (
    <div>
      <h2>Collections</h2>
      <TableContainer component={ Paper }>
        <Table aria-label="collection table">
          <TableHead>
            <TableRow>
              <TableCell>ID</TableCell>
              <TableCell>Title</TableCell>
              <TableCell>Brand</TableCell>
              <TableCell>Buy date</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            { renderCollections(collections) }
          </TableBody>
        </Table>
      </TableContainer>
    </div>
  );
}

const renderCollections = (collections) => {
  return (
    collections.map(collection => (
      <TableRow key={ collection.id }>
        <TableCell>{ collection.id }</TableCell>
        <TableCell>{ collection.title }</TableCell>
        <TableCell>{ collection.brand }</TableCell>
        <TableCell>{ collection.buy_date }</TableCell>
      </TableRow>
    ))
  );
}


export default Collections;

いろいろググりながらなんとか書いた上のコードで一応動くようにはなった。けど、表示されるコレクションの順番が不定になってしまう。APIからリストを取得した段階では ID 順に並んでいるのは確認したので、たぶん、個々の詳細データを取得する fetchProduct 関数内でリスト(変数 collections)を更新するタイミングのせいだ。つまり、ループの中で使ってる fetch 関数が非同期なせいだ(たぶん)。

あぁ、JavaScript ってこういうところがなんかやりづらいんだよなぁ。

なんかいい書き方はないものか。あとからソートすればいいのはわかるんだけど。

カテゴリー: JavaScript | コメントする

Ruby 3.0.0 リリース

とうとうリリースされた。

いろいろと新機能やパフォーマンス改善があって楽しそうだけど、正直ついていけてないんだよなぁ。メジャーバージョンアップだから、これを機会に力入れて取り組んでみようか。

カテゴリー: Ruby | コメントする

CentOS8終了

今頃気づいたんだけど、CentOS 8 が開発を終了するんだと。今月8日付のブログで発表されてる。

日本語のニュースやブログ:

今後、プロジェクトは CentoOS Stream の開発に注力し、CentoOS 8 は2021年末でサポートを終了とのこと。

ウチのローカルネットでは CentoOS 8 のサーバを1台運用してるけど、まだ1年ちょっとしか経ってない。CentoOS Stream は Red Hat Enterprise Linux(RHEL)と Fedora の中間に位置する、RHEL の開発ブランチという位置づけなので、今までの CentoOS のかわりというわけにはいかない。さくらインターネットで借りてる VPS が CentoOS なので慣れるためもあって運用してたけど、もう Ubuntu に統一するかな。

なお、CentoOS 7 のサポートは2024年6月30日まで。

カテゴリー: CentOS | コメントする

aptあるいはDockerの怪

apt コマンドのせいなのか Docker のせいなのかわからないけど、とにかく不可解な現象に遭遇したので記録しておく。

TL;DR

  • 先月から自宅のサーバで動かしている web サービスを少しずつ Docker 上に移行する作業をしている。
  • 開発用のマシン(Ubuntu 20.04 LTS)で期待通り動作する設定(Dockerfile と docker-compose.yml)ができたのでサーバ(Ubuntu 18.04 LTS)に持っていったら Docker イメージのビルドでコケた。
  • Dockerfile の中の apt update を apt-get update に変えたら通った。

開発用のマシンにて

開発用の環境は次の通り:

  • Ubuntu 20.04.1 LTS
  • Docker version 19.03.8
  • docker-compose version 1.25.0

で、期待通り動作するように書き上げた Dockerfile はこう:

FROM ubuntu:20.04

LABEL maintainer="takatoh"

RUN apt update
RUN apt install -y \
    ruby \
    ruby-dev \
    gcc \
    g++ \
    make \
&& rm -rf /var/lib/apt/lists/*

ENV GEM_HOME /usr/local/bundle
ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH
RUN gem install bundler unicorn

ADD ./files/lcstorage-2.1.0.tar.gz /usr/
WORKDIR /usr/lcstorage
RUN bundle install

CMD [ "unicorn", "-c", "/var/lcstorage/unicorn.conf" ]

Ruby で書いた web アプリを unicorn で動かしている。この Dockerfile でイメージをビルドして、ちゃんと期待通りに動作するのを確認した。

サーバにて

サーバの環境は次の通り:

  • Ubuntu 18.04.5 LTS
  • Docker version 19.03.6
  • docker-compose version 1.17.1

開発用の環境よりもバージョンが旧いといえばそのとおりだけど、OS はともかく Docker や docker-compose はそんなに旧いわけではない。実際、別の web アプリを同じようにサーバで動かしていて、それをビルドしたときには何の問題もなかった。

ところが、今回この Dockerfile をもとにサーバでイメージをビルドすると途中でエラーが発生した。どうも apt コマンドでパッケージをインストールしている途中でコケるようだ。

仕方がないので手順をひとつずつ手動でやってみることにした。ubuntu:20.04 のイメージからコンテナを起動して、apt update し、パッケージを順にひとつずつ apt install した。が、なんの問題もなくすべてのパッケージのインストールができてしまった。

どういうこと?

Dockerfile を使ってビルドしたときにはパッケージのインストールのところでエラーが出てたんだから、手動でひとつずつインストールすればどのパッケージのインストールでエラーが出るのか判断できる、と考えてた。けど、手動でやったらエラーが出ずに終わってしまった。これじゃ手がかりがない。

唯一の手がかりは Dockerfile でビルドしたときのエラーログだ。次のように出ていた。

E: Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/l/linux/linux-libc-dev_5.4.0-56.62_amd64.deb  404  Not Found [IP: 91.189.88.152 80]
 E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?

http://security.ubuntu.com/ubuntu/pool/main/l/linux/linux-libc-dev_5.4.0-56.62_amd64.deb が見つからない、と言ってるけど、ブラウザで見てみると確かにこの URL が示すファイルがない。

でも、じゃあなんで開発用のマシンでは問題なかったんだ?

いろいろググってはみたものの、有力な手掛かりは見つからなかった。

と、もういちどエラーメッセージを見ると、apt-get update を実行しろみたいなことが書いてある。apt じゃなくて apt-get だ。apt コマンドは apt-get コマンドの置き換えなんだからそんなの関係あるか?と思いながら、他に手掛かりがないので Dockerfile を修正してみた。こうだ:

- RUN apt update
+ RUN apt-get update

すると、どういうわけかエラーなくビルドできてしまった。

はぁ?

結論

何が起きたのかよくわからん。いや、さっぱりわかんない。

でも、とにかく動くようになったのでひとまずは良しとする。が、やっぱり釈然としない。原因を追求してみたいけど、手に負えるかなぁ……

カテゴリー: misc, Ubuntu | コメントする

MariaDBのデータベースをdump

このあいだの、MediaWiki のデータベースとして MariaDB を採用したわけだけど、バックアップのために dump するには MySQL と同じように mysqldump が使える。

こんな感じ:

$ mysqldump -P 3306 -u root -p testwiki > testwiki.sql
カテゴリー: misc | コメントする

docker-composeでMediaWikiを動かす

ローカルネットワークで wiki を運用してるんだけど、Docker 上に移行すべく、今日はそのテスト。

環境

  • Ubuntu 20.04
  • Docker 19.03.8
  • docker-compose 1.25.0

Dockerイメージ

MediaWiki も MariaDB も Docker Hub に公式イメージが有るのでそれを使わせてもらう。データベースは、はじめは MySQL を試したんだけどうまく動かなかった(原因不明)ので MariaDB に変えた。

  • mediawiki:1.35.0
  • mariadb:10.5.6-focal

ディレクトリ構成とファイル

用意した構成はこんなの:

[email protected]:testwiki$ tree .
.
├── docker-compose.yml
├── mysql
│   └── db
└── wiki
    └── images

docker-compose.yml はこう:

version: '3'

services:
  testwiki:
    container_name: testwiki
    image: mediawiki:1.35.0
    restart: always
    ports:
      - 8888:80
    volumes:
      - ./wiki/images:/var/www/html/images
#      - ./wiki/LocalSettings.php:/var/www/html/LocalSettings.php

  mysql:
    container_name: db
    image: mariadb:10.5.6-focal
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpasswd
      MYSQL_DATABASE: testwiki
      MYSQL_USER: mysqluser
      MYSQL_PASSWORD: mypassword
    ports:
      - 3306:3306
    volumes:
      - ./mysql/db:/var/lib/mysql

コメントアウトしてある行は MediaWiki の設定ファイル。これはインストールが済んでから使う。

コンテナの起動とMediaWikiのインストール

docker-compose コマンドでコンテナを起動する。

[email protected]:testwiki$ docker-compose up -d

ブラウザで http://localhost:8888/ にアクセスして MediaWiki のインストール(というかセットアップというか)をする。その際、データベース関係は docker-compose.yml の記述に合わせる。

  • データベースのホスト:db
  • データベース名:testwiki
  • インストールで使用する利用者アカウント→データベースのユーザ名:mysqluser
  • インストールで使用する利用者アカウント→データベースのパスワード:mypassword

その他はそれなりに設定すればいい。最後に LocalSettings.php ファイル(設定ファイル)をダウンロードしてインストールは終わり。

設定の反映

いったんコンテナを止める。

[email protected]:testwiki$ docker-compose down

ダウンロードした設定ファイルを配置。

[email protected]:testwiki$ cp ~/Downloads/LocalSettings.php wiki

docker-compose.yml のコメントをはずす(該当行だけ示す)。

      - ./wiki/LocalSettings.php:/var/www/html/LocalSettings.php

コンテナを再起動。

[email protected]:testwiki$ docker-compose up -d

これで無事起動した。

参考にしたページ

[追記:2020/11/5] データベースの移行

旧い wiki からデータを移行する手順。

  • データは wiki.sql ファイルにダンプしてあるものとする
  • 旧いほうの MediaWiki のバージョンは 1.27.1

wiki.sql ファイルを Docker コンテナと共有しているディレクトリにコピーする。

[email protected]:testwiki$ cp wiki.sql ./mysql/db

./mysql/db ディレクトリは、データベースの Docker コンテナ(コンテナ名は db)からは /var/lib/mysql として認識されている(前述の docker-compose.yml ファイルを参照)。なのでデータベースのコンテナに接続して、データを流し込む。

[email protected]:testwiki$ docker exec -it db bash
[email protected]:/# cd /var/lib/mysql
[email protected]:/var/lib/mysql# ls *.sql
wiki.sql
[email protected]:/var/lib/mysql# mysql -u mysqluser -p testwiki < wiki.sql
Enter password:
[email protected]:/var/lib/mysql# exit
exit

これでデータベース側での作業は終了。ただ、このままだと MediaWiki でエラーになる。バージョンが上がっているので MediaWiki の使用するデータベーススキーマとかも変わっているからだ。

そこで、MediaWiki のコンテナに接続して更新スクリプトを実行する。更新スクリプトは /var/www/html/maintenance/update.php だ。

[email protected]:testwiki$ docker exec -it testwiki bash
[email protected]:/var/www/html# cd maintenance
[email protected]:/var/www/html/maintenance# php update.php

これで完了。

[追記]

11/7、本番環境も無事 Docker 上に移行した。

カテゴリー: misc, Ubuntu | コメントする

PythonスクリプトをパッケージングしてPyPIに公開するまでのあれこれ

このあいだ作った、画像ファイルから EPUB ファイルをつくるスクリプトを、wheel でパッケージングして PyPI に公開した。バージョンは v0.3.0。ソースは GitHub に上げてある。

ファイル構成は変わったし、実行コマンドに click を導入して img2epub build コマンドで EPUB を生成するようにしたけど、機能的にはこのあいだのスクリプトと大して変わってないので改めては説明しない。必要なら上のリンク先を見てほしい。

代わりに、Python でパッケージを開発して PyPI に公開するまでの作業で、これまで知らなかったりあやふやだったところを調べたのでその辺についてメモしておく。以前とはやり方が変わったりしてて、知識はアップデートしなきゃならんと思った。

環境

  • Windows 10 Pro
  • PowerShell 7
  • Python 3.8.6

venv

開発用の仮想環境。以前は virtualenv というサードパーティのパッケージを使ってたけど、Python 3.3 から venv というのが標準でついてくる。プロジェクトのディレクトリで次のようにすると仮想環境が作られる。

[email protected]: myproject > python -m venv .venv

.venv とういのが仮想環境の名前。なんでもいいようだけど .venv とすることが多いみたい。仮想環境をアクティベートするには:

[email protected]: myproject > .venv/Scripts/Activate.ps1
(.venv) [email protected]: myproject >

アクティベートされると2行目のようにプロンプトに仮想環境名が表示される。仮想環境から抜けるにはこう:

(.venv) [email protected]: myproject > deactivate

作ったばかりの仮想環境には pip と setuptools しか入ってない。

(.venv) [email protected]: myproject > pip list
Package    Version
---------- -------
pip        20.2.1
setuptools 49.2.1
WARNING: You are using pip version 20.2.1; however, version 20.2.4 is available.
You should consider upgrading via the 'c:\users\takatoh\documents\w\myproject.venv\scripts\python.exe -m pip install --upgrade pip' command.

pip の新しいバージョンがあるのでバージョンアップしておく。

(.venv) [email protected]: myproject > python -m pip install --upgrade pip

setup.pyと関連ファイル

setup.py はパッケージングに使うスクリプトだけど、もろもろの設定はこのファイルには書かないのが最近の作法らしい。だから中身はこれだけ:

import setuptools


setuptools.setup()

じゃあ、設定はどこに書くのかというと setup.cfg というファイルに書く。書式も ini ファイル風。

[metadata]
name = myproject
version = attr:myproj.__version__
description = My Python project example.
author = takatoh
author_email = [email protected]
license = MIT License
classifier =
    License :: OSI Approved :: MIT License
    Programming Language :: Python
    Programming Language :: Python :: 3

[options]
zip_safe = False
packages = find:
include_package_data = True
entry_points = file:entry_points.cfg
install_requires =
    click

パッケージが実行コマンドを含むので [options] セクションに entry_points = file:entry_points.cfg を指定している。entry_points.cfg ファイルにコマンド名と実際に実行される関数を書くようになっている。その entry_points.cfg はこう:

[console_scripts]
myproj = myproj.command:main

もうひとつ、パッケージに含むファイルについて。packages = find: という記述で Python のスクリプトファイルを含めることができる。だけど場合によってはスクリプトでないファイルを含めたいこともある。img2epub ではテンプレートファイルが必要だった。そういう時には include_package_data = True としておいて、MANIFEST.in にファイルを列挙する。こんな感じ:

include myproj/data/greeting.txt

ファイルは setup.py(あるいは setup.cfg というべき?)からの相対パスで書く。

というわけで、これでパッケージングの準備は完了。設定内容自体は以前の setup.py に書くスタイルと変わらないんだけど、なんかファイルが分散してわかりにくくなったんじゃないかなぁ。

pip install -e .

pip install -e . コマンドは、開発中のパッケージを「開発モード」でインストールしてくれる。

(.venv) [email protected]: myproject > pip install -e .
Obtaining file:///C:/Users/takatoh/Documents/w/myproject
Collecting click
Using cached click-7.1.2-py2.py3-none-any.whl (82 kB)
Installing collected packages: click, myproject
Running setup.py develop for myproject
Successfully installed click-7.1.2 myproject

依存パッケージがあれば一緒にインストールしてくれるのはもちろんだけど、開発中のパッケージのファイルを更新すると、自動的に反映してくれる。

(.venv) [email protected]: myproject > pip list
Package    Version Location
---------- ------- --------------------------------------
click      7.1.2
myproject  0.0.1   c:\users\takatoh\documents\w\myproject
pip        20.2.4
setuptools 49.2.1

myproject だけパスが書いてある。これが開発中のパッケージだ。

※まだ書きかけ。

参考ページ

カテゴリー: Python | コメントする

ひとそろいの画像ファイルからEPUB(電子書籍)ファイルを作る

Qiita の↓の記事を読んで、EPUB って結構簡単(もちろん単純なものなら)なんだな、と思ったので Python でスクリプトを作ってみた。

作ったもの

フォルダに入った画像ファイル一式から EPUB ファイルを生成する。

  • 1ページ1画像のファイル一式
  • とりあえず PNG にだけ対応
  • 画像ファイルはファイル名でソートするので連番でなくても構わない
  • 目次とかそういうのはなし
  • 元データのフォルダ名が EPUB のタイトル、ファイル名になる

ファイル構成は次の通り:

[email protected]: img2epub > tree /f .
フォルダー パスの一覧
ボリューム シリアル番号は 681C-8AA1 です
C:\USERS\TAKATOH\DOCUMENTS\W\IMG2EPUB
│  .gitignore
│  img2epub.py
│
├─data
│      book.opf.template
│      chap1.xhtml.template
│      container.xml
│      nav.xhtml
│
└─sample
        sample-000.png
        sample-001.png
        sample-002.png
        sample-003.png
        sample-004.png
        sample-005.png
        sample-006.png
        sample-007.png

img2epub.py が Python で書いたスクリプト本体。data フォルダ以下のファイルは EPUB を構成するファイルあるいはそのテンプレート。スクリプトは次の通り:

#!/usr/bin/env python
# encoding: utf-8


import sys
import os
import shutil
import subprocess
from datetime import datetime, timezone
import uuid
import glob
from jinja2 import Template, Environment, FileSystemLoader


def main():
    src_dir = sys.argv[1]
    now = datetime.now(timezone.utc)
    tmp_dir_name = "tmp.epub.{time}".format(time=now.strftime("%Y%m%d%H%M%S"))

    make_dirs(tmp_dir_name)
    images = copy_images(src_dir, tmp_dir_name)
    images = [s.replace("\\","/") for s in sorted(images)]
    gen_mimetype(tmp_dir_name)
    copy_container(tmp_dir_name)
    book_opf_context = {
        "title": src_dir,
        "time": now.isoformat(),
        "images": images
    }
    gen_book_opf(tmp_dir_name, book_opf_context)
    copy_nav(tmp_dir_name)
    gen_chap1_xhtml(tmp_dir_name, book_opf_context)
    zip_epub(tmp_dir_name, src_dir)


def make_dirs(tmp_dir_name):
    os.makedirs(os.path.join(tmp_dir_name, "META-INF"))
    os.makedirs(os.path.join(tmp_dir_name, "EPUB"))


def copy_images(src_dir, tmp_dir_name):
    images_dir = os.path.join(tmp_dir_name, "EPUB/images")
    shutil.copytree(src_dir, images_dir)
    return glob.glob("{dir}/*".format(dir=images_dir))


def gen_mimetype(tmp_dir_name):
    with open(os.path.join(tmp_dir_name, "mimetype"), "w") as f:
        f.write("application/epub+zip")


def copy_container(tmp_dir_name):
    shutil.copyfile("data/container.xml", os.path.join(tmp_dir_name, "META-INF/container.xml"))


def gen_book_opf(tmp_dir_name, context):
    env = Environment(loader=FileSystemLoader("data"))
    template = env.get_template("book.opf.template")
    context["images"] = [s.replace("{tmp}/EPUB".format(tmp=tmp_dir_name), ".") for s in context["images"]]
    context["cover"] = context["images"][0]
    context["uuid"] = str(uuid.uuid4())
    with open(os.path.join(tmp_dir_name, "EPUB/book.opf"), "w") as f:
        f.write(template.render(context))


def copy_nav(tmp_dir_name):
    shutil.copyfile("data/nav.xhtml", os.path.join(tmp_dir_name, "EPUB/nav.xhtml"))


def gen_chap1_xhtml(tmp_dir_name, context):
    env = Environment(loader=FileSystemLoader("data"))
    template = env.get_template("chap1.xhtml.template")
    images = [s.replace("{tmp}/EPUB".format(tmp=tmp_dir_name), ".") for s in context["images"]]
    with open(os.path.join(tmp_dir_name, "EPUB/chap1.xhtml"), "w") as f:
        f.write(template.render(images=images))


def zip_epub(tmp_dir_name, title):
    epub_file_name = "../{title}.epub".format(title=title)
    os.chdir(tmp_dir_name)
    subprocess.run(["zip", "-X0", epub_file_name, "mimetype"], stdout=subprocess.DEVNULL)
    subprocess.run(["zip", "-r9", epub_file_name, "*", "-x", "mimetype"], stdout=subprocess.DEVNULL)
    os.chdir("..")



main()

で、sample フォルダ以下が元になる画像ファイル一式。

使い方

スクリプトの引数に画像一式が入っているフォルダを指定するだけ。

[email protected]: img2epub > python img2epub.py sample

そうすると、EPUB ファイル(今回は sample.epub)と、EPUB にまとめる前のファイル一式の入ったフォルダ(同じく tmp.epub.20201027130853)ができる。このフォルダはテンポラリなものなので消しちゃってもいいんだけど、今の段階ではまだ残している。

[email protected]: img2epub > ls


    Directory: C:\Users\takatoh\Documents\w\img2epub

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/10/27    20:07                data
d----          2020/10/27    20:09                sample
d----          2020/10/27    22:08                tmp.epub.20201027130853
-a---          2020/10/27    20:07              9 .gitignore
-a---          2020/10/27    20:07           2832 img2epub.py
-a---          2020/10/27    22:08        8011218 sample.epub

あとは出来上がった sample.epub ファイルを好きな EPUB ビューワで見ればいい。

EPUB のファイルの構成

EPUB のファイルっていうのは、基本的には使用で決められたファイルとコンテンツを zip で一つにまとめて、拡張子を .epub にしただけのファイルだ。単純に zip にしただけではないんだけど、そのへんはこの記事では触れない。冒頭の Qiita の記事か、EPUB 3.2 の仕様を参照のこと。

今回作ったスクリプト img2epub.py では、tmp.epub.* フォルダにその一式が入っている(つまりこれを zip 圧縮して .epub ファイルを作る)。フォルダの中身は次の通り:

[email protected]: img2epub > tree /f tmp.epub.20201027130853
フォルダー パスの一覧
ボリューム シリアル番号は 681C-8AA1 です
C:\USERS\TAKATOH\DOCUMENTS\W\IMG2EPUB\TMP.EPUB.20201027130853
│  mimetype
│
├─EPUB
│  │  book.opf
│  │  chap1.xhtml
│  │  nav.xhtml
│  │
│  └─images
│          sample-000.png
│          sample-001.png
│          sample-002.png
│          sample-003.png
│          sample-004.png
│          sample-005.png
│          sample-006.png
│          sample-007.png
│
└─META-INF
        container.xml

EPUB/images 以下の画像ファイルは、元のデータをコピーしたもの。そのほかのファイルはスクリプトが生成したファイルだ。詳しくは略。

余談

EPUB を構成するファイルの一部(book.opf や chap1.xhtml)を生成するためにテンプレートエンジンを使ってるんだけど、Python には string.Template というテンプレートエンジンが標準でついている。これ、今回調べてて初めて知った。

ところがこの string.Template、単純な値の挿入はできるけど繰り返しや条件分岐の機能がない。今回、条件分岐は使ってないけど繰り返すは必要だったので、結局 Jinja2 を使った。標準添付されてるのはいいけど、変数を値に置き換えるだけしかできないんじゃ、用途は限られるよなぁ。

カテゴリー: misc, Python | コメントする

Poppler for WindowsでPDFをPNGに変換する

PDF をページごとの画像ファイルに変換したくて、はじめは Python でできないか調べてた。そしたら↓のページで pdf2image という(Pythonの)ライブラリを紹介しているのを見つけた。

ところが記事を読んでみるとこう書いてある:

pdf2imageは「Poppler」というフリーのPDFコマンドラインツールを背後で用います。そのため、Popplerをダウンロードしておく必要があります。

それなら Poppler をそのまま使えばいいじゃん。

というわけで、Poppler for Windows をダウンロードした。

バージョンは 0.68.0 (poppler-0.68.0_x86.7z)。7zip なので 7z コマンドをインストールしてから展開した。

展開したファイルを眺めてみると、bin フォルダの中に pdfimages.exe という実行ファイルがある。これが使えそうだ。PATH を通してとりあえずヘルプを見てみた。

[email protected]: tmp > pdfimages -h
pdfimages version 0.68.0
Copyright 2005-2018 The Poppler Developers - http://poppler.freedesktop.org
Copyright 1996-2011 Glyph & Cog, LLC
Usage: pdfimages [options] <PDF-file> <image-root>
  -f <int>       : first page to convert
  -l <int>       : last page to convert
  -png           : change the default output format to PNG
  -tiff          : change the default output format to TIFF
  -j             : write JPEG images as JPEG files
  -jp2           : write JPEG2000 images as JP2 files
  -jbig2         : write JBIG2 images as JBIG2 files
  -ccitt         : write CCITT images as CCITT files
  -all           : equivalent to -png -tiff -j -jp2 -jbig2 -ccitt
  -list          : print list of images instead of saving
  -opw <string>  : owner password (for encrypted files)
  -upw <string>  : user password (for encrypted files)
  -p             : include page numbers in output file names
  -q             : don't print any messages or errors
  -v             : print copyright and version info
  -h             : print usage information
  -help          : print usage information
  --help         : print usage information
  -?             : print usage information

出力は JPEG が欲しかったので -j オプションを指定。<image-root> が何を指すのかよくわからないけどテキトーに。そしたらこうなった。

[email protected]: tmp > pdfimages -j sample.pdf foo
[email protected]: tmp > ls


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/10/24     8:53        5364971 foo-000.ppm
-a---          2020/10/24     8:53        5364971 foo-001.ppm
-a---          2020/10/24     8:53        5364971 foo-002.ppm
-a---          2020/10/24     8:53        5364971 foo-003.ppm
-a---          2020/10/24     8:53        5364971 foo-004.ppm
-a---          2020/10/24     8:53        5364971 foo-005.ppm
-a---          2020/10/24     8:53        5364971 foo-006.ppm
-a---          2020/10/24     8:53        5364971 foo-007.ppm
-a---          2020/03/28     9:12        4082441 sample.pdf

.ppm って!いまどき .ppm ファイルなんて何で見ればいいんだ。なら PNG でいいや。あと、<image-root> は出力ファイルのプレフィックスみたいだな。なので .ppm ファイルは削除してやり直した。

[email protected]: tmp > pdfimages -png sample.pdf sample
[email protected]: tmp > ls


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/03/28     9:12        4082441 sample.pdf
-a---          2020/10/24     9:01        1269290 sample-000.png
-a---          2020/10/24     9:01         446405 sample-001.png
-a---          2020/10/24     9:01         893712 sample-002.png
-a---          2020/10/24     9:01        1258104 sample-003.png
-a---          2020/10/24     9:01        1301072 sample-004.png
-a---          2020/10/24     9:01        1344592 sample-005.png
-a---          2020/10/24     9:01        1157016 sample-006.png
-a---          2020/10/24     9:01         755768 sample-007.png

これで OK。だけど、欲を言えば出力されるファイルをひとつのフォルダに入れたい。それらしいオプションは見当たらないけど、プレフィックスにパスを含めてやればできた。フォルダは先に作っておくこと。

[email protected]: tmp > mkdir out


    Directory: C:\Users\takatoh\Documents\tmp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/10/24    11:29                out

[email protected]: tmp > pdfimages -png sample.pdf out/sample
[email protected]: tmp > ls out


    Directory: C:\Users\takatoh\Documents\tmp\out

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2020/10/24    11:29        1269290 sample-000.png
-a---          2020/10/24    11:29         446405 sample-001.png
-a---          2020/10/24    11:29         893712 sample-002.png
-a---          2020/10/24    11:29        1258104 sample-003.png
-a---          2020/10/24    11:29        1301072 sample-004.png
-a---          2020/10/24    11:29        1344592 sample-005.png
-a---          2020/10/24    11:29        1157016 sample-006.png
-a---          2020/10/24    11:29         755768 sample-007.png

できた。

補足。

-j オプションの説明は write JPEG images as JPEG files となっている。PDFの中身が JPEG なら JPEG で出力するってことのようだ。

カテゴリー: misc, Windows | コメントする