ウェブ会議でホワイトボード/PDF に手書き/Debian で使うタブレット

しばらく前(2023年に入ったころ)から、いわゆるウェブ会議を使って、リモートで家庭教師のようなこと(本当は違うのだが少しフェイクを入れるのでここではそういうことにしておく)をやっている。

こちらの環境は Debian で、そもそもプロプライエタリな道具はほぼ存在しないので、いろいろと探し回ることになった。

必要な道具は、まずウェブ会議の仕組みにホワイトボード機能となる。はじめは Jitsi を使っていたのだが、そのホワイトボード機能にバグのようなもの(数十分ほど書いたり消したりしているとハングアップしたようにそれ以上の書いたり消したりができなくなる)が出現して、Whereby を使うようになった。この切り替えはもう3か月ほど前なので、現在(2023年9月末)ではもう解決しているかもしれない。

Whereby は2人(1対1)で使う分には時間無制限なので、ここでの目的では困らない。Whereby ではホワイトボードとして外部サービスの Miro の画面を共有するようになっており、この使い方では Miro にアカウントを作る必要もない。

また、ホワイトボードに PDF を背景画像のように表示させてその上に書き込むように表示させる(家庭教師的に言えば、練習問題のプリントを表示させておいて解説を書き込む)ような使い方は、Jitsi のホワイトボードではできず、バグの有無に関わらず Whereby (Miro) のほうが優れている。

やや話はずれるが、ウェブ会議とは無関係に、既存のPDFに手書きで書き込むようなアプリケーションを探してみたら、Xournal++ というものが見つかった。これはこれでたいへん優れた道具だ。

さて、そのように手書きで書き込む際、マウス(と言っても自分はトラックボールだが)ではなかなか上手く書くことができないので、ペンタブレットを用意することにした。価格が手頃で Linux での実績がありそうな XPPen の Deco Fun XS を購入。お絵描きには小さすぎるかもしれないが、ここでの目的にはちょうどいい。

表面は思いの外ツルツルで、ペンが滑りすぎると感じるので、紙(たとえば便箋)をタブレットの作業エリアに貼り付けた。これで書き味がまさに紙に書いている感じになり、すこぶるよくなった。

XPPen には Linux 用のドライバが用意されている。これがなくても機器を接続するだけで十分使えるのだが、これによってさらに便利に使うことができる。たとえば、タブレットでの作業エリアを画面上の一部領域に設定できたり、ペンのボタンに機能を割り当てたりできるようになる。

ここでは「180度回転」と設定し、タブレットそのものを上下逆さまに置くことにした。このタブレットには作業エリア以外に幅 2cm ほどの部分があり、それを手前に持ってきてちょうどパームレスト(文字を書くので実際には小指の脇か)のように使うことで、より安定して字を書けるようになった。

【以下は、2023年9月末現在の自分の環境(Debian testing)での問題である。】

ところが、ここでは Debian の testing バージョン(安定バージョンではなく、ソフトウェア(パッケージ)が少しずつ新しいものに置き換わっていく)を使用しているため、ある時の Debian の更新以降この XP-Pen 提供のドライバ(この時点で XPPen-pentablet-3.2.3.230215-1.x86_64.deb)が動かなくなってしまった。提供されているドライバは当然にも 対応は Debian 安定版としか謳っていないので文句をいう筋合いではない。

XP-Pen 提供のドライバは実際には /usr/lib/pentablet/pentablet.sh というシェルスクリプト

#!/bin/sh
appname=`basename $0 | sed s,\.sh$,,`
dirname=`dirname $0`
tmp="${dirname#?}"
if [ "${dirname%$tmp}" != "/" ]; then
dirname=$PWD/$dirname
fi
LD_LIBRARY_PATH=$dirname/lib
export LD_LIBRARY_PATH
$dirname/$appname "$@"

を通じて、同じディレクトリの lib/ 以下のライブラリを使いつつ、同じディレクトリの pentablet というバイナリが起動されている。OSバージョンで困らないようにわざわざ同梱されている lib/ 以下のライブラリのどれかが逆に悪さして、このドライバが動かなくなっていた。このシェルスクリプトを飛ばして(LD_LIBRARY_PATHを設定しないようにして)直接 /usr/lib/pentablet/pentablet を起動すると、問題なく使えるようになった。

$ ldd /usr/lib/pentablet/pentablet

	linux-vdso.so.1 (0x00007fff87dc2000)
	libusb-1.0.so.0 => /lib/x86_64-linux-gnu/libusb-1.0.so.0 (0x00007f4e0b29d000)
	libX11.so.6 => /lib/x86_64-linux-gnu/libX11.so.6 (0x00007f4e0b15b000)
	libXtst.so.6 => /lib/x86_64-linux-gnu/libXtst.so.6 (0x00007f4e0b153000)
	libXi.so.6 => /lib/x86_64-linux-gnu/libXi.so.6 (0x00007f4e0b13f000)
	libXrandr.so.2 => /lib/x86_64-linux-gnu/libXrandr.so.2 (0x00007f4e0b132000)
	libXinerama.so.1 => /lib/x86_64-linux-gnu/libXinerama.so.1 (0x00007f4e0b12b000)
	libQt5X11Extras.so.5 => /lib/x86_64-linux-gnu/libQt5X11Extras.so.5 (0x00007f4e0b124000)
	libQt5Widgets.so.5 => /lib/x86_64-linux-gnu/libQt5Widgets.so.5 (0x00007f4e0aa00000)
	libQt5Gui.so.5 => /lib/x86_64-linux-gnu/libQt5Gui.so.5 (0x00007f4e0a200000)
	libQt5Xml.so.5 => /lib/x86_64-linux-gnu/libQt5Xml.so.5 (0x00007f4e0b0e0000)
	libQt5Network.so.5 => /lib/x86_64-linux-gnu/libQt5Network.so.5 (0x00007f4e0a055000)
	libQt5Core.so.5 => /lib/x86_64-linux-gnu/libQt5Core.so.5 (0x00007f4e09a00000)
	libGL.so.1 => /lib/x86_64-linux-gnu/libGL.so.1 (0x00007f4e0a979000)
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4e0b0d9000)
	libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4e09600000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4e09f76000)
	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4e0b0b5000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4e0941e000)
	libudev.so.1 => /lib/x86_64-linux-gnu/libudev.so.1 (0x00007f4e0a947000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f4e0b2d2000)
	libxcb.so.1 => /lib/x86_64-linux-gnu/libxcb.so.1 (0x00007f4e0a91d000)
	libXext.so.6 => /lib/x86_64-linux-gnu/libXext.so.6 (0x00007f4e0a908000)
	libXrender.so.1 => /lib/x86_64-linux-gnu/libXrender.so.1 (0x00007f4e0a8fb000)
	libpng16.so.16 => /lib/x86_64-linux-gnu/libpng16.so.16 (0x00007f4e099ca000)
	libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f4e099ab000)
	libharfbuzz.so.0 => /lib/x86_64-linux-gnu/libharfbuzz.so.0 (0x00007f4e09884000)
	libmd4c.so.0 => /lib/x86_64-linux-gnu/libmd4c.so.0 (0x00007f4e0a8e9000)
	libgssapi_krb5.so.2 => /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 (0x00007f4e093cb000)
	libdouble-conversion.so.3 => /lib/x86_64-linux-gnu/libdouble-conversion.so.3 (0x00007f4e0986f000)
	libicui18n.so.72 => /lib/x86_64-linux-gnu/libicui18n.so.72 (0x00007f4e09000000)
	libicuuc.so.72 => /lib/x86_64-linux-gnu/libicuuc.so.72 (0x00007f4e08e02000)
	libpcre2-16.so.0 => /lib/x86_64-linux-gnu/libpcre2-16.so.0 (0x00007f4e0933d000)
	libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007f4e08d41000)
	libglib-2.0.so.0 => /lib/x86_64-linux-gnu/libglib-2.0.so.0 (0x00007f4e08bfb000)
	libGLdispatch.so.0 => /lib/x86_64-linux-gnu/libGLdispatch.so.0 (0x00007f4e08b42000)
	libGLX.so.0 => /lib/x86_64-linux-gnu/libGLX.so.0 (0x00007f4e08b0e000)
	libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x00007f4e09f6a000)
	libXau.so.6 => /lib/x86_64-linux-gnu/libXau.so.6 (0x00007f4e0b0a8000)
	libXdmcp.so.6 => /lib/x86_64-linux-gnu/libXdmcp.so.6 (0x00007f4e08800000)
	libfreetype.so.6 => /lib/x86_64-linux-gnu/libfreetype.so.6 (0x00007f4e08a42000)
	libgraphite2.so.3 => /lib/x86_64-linux-gnu/libgraphite2.so.3 (0x00007f4e08a16000)
	libkrb5.so.3 => /lib/x86_64-linux-gnu/libkrb5.so.3 (0x00007f4e08724000)
	libk5crypto.so.3 => /lib/x86_64-linux-gnu/libk5crypto.so.3 (0x00007f4e086f7000)
	libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2 (0x00007f4e09869000)
	libkrb5support.so.0 => /lib/x86_64-linux-gnu/libkrb5support.so.0 (0x00007f4e0985b000)
	libicudata.so.72 => /lib/x86_64-linux-gnu/libicudata.so.72 (0x00007f4e06800000)
	libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f4e0865c000)
	libbsd.so.0 => /lib/x86_64-linux-gnu/libbsd.so.0 (0x00007f4e08647000)
	libbz2.so.1.0 => /lib/x86_64-linux-gnu/libbz2.so.1.0 (0x00007f4e0932a000)
	libbrotlidec.so.1 => /lib/x86_64-linux-gnu/libbrotlidec.so.1 (0x00007f4e08a09000)
	libkeyutils.so.1 => /lib/x86_64-linux-gnu/libkeyutils.so.1 (0x00007f4e08640000)
	libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f4e0862f000)
	libmd.so.0 => /lib/x86_64-linux-gnu/libmd.so.0 (0x00007f4e08620000)
	libbrotlicommon.so.1 => /lib/x86_64-linux-gnu/libbrotlicommon.so.1 (0x00007f4e085fd000)

Markdown でビジネス文書を作成する

まとめ

  • Pandoc’s Markdown という一方言には::: という汎用Divに対応する記法がある。
  • {markdown, latex} → html は pandoc のみでできる。別途 css を用意するといい。
  • {html, latex} → markdown は pandoc のみでできる。
  • {html, markdown} → latex はフィルタが必要。別途 sty を用意するといい。

ビジネス文書

しばらく前に「公文書を Markdown で」という話題があった。図入りの面倒くさいものや白書のような普通の書籍ほどの長大なものや法律の条文のような特殊なものは別として、圧倒的に多いであろう1枚ペラ程度の通達の類の文書には、 Markdown はとても向いていると思う。簡単なものほど Word のような高機能文書作成ソフトを使わずに Markdown で済ませたい。

この様式[1]は、役所が作成する公文書[2]から会社、さらには小学校や PTA、町内会までよく浸透している。

しかし Markdown では、この類の文書の様式に欠かせない右寄せ(右揃え)ができないのが最大のネックだ。意味見た目の分離という観点から言えば右寄せ(右揃え)は見た目なのだが、ここではもうそう呼ぶ ことにする。

Markdown

機能を絞り込んで簡潔であることが Markdown の特長なのだから、あれもこれも盛り込んでいけば最初から HTML を使えばいいとなって元も子もない。とは言え不満はなんとかしたい。Markdown で書きながらも最終的には HTML にすることを想定して直接 HTMLタグ (<div style="text-align:right">)を書いてしまうという手段もあるのだが、せっかくの Markdown なのだし他の形式への変換も考えるとなるべくそういうことはやりたくない。

ここは Markdown 記法に HTML の汎用タグ <div><span> を表せるものがあれば万能なのになあ……と思っていたら、Pandoc’s Markdown という一方言にはこれが存在していることに今さらながら気がついた。

Pandoc の Markdown 拡張

Pandoc’s Markdown という一方言には::: という汎用の Div に対応する記法がある。ここでは触れないが、汎用の Span に対応する []{} 記法もある。

この記法は Pandoc で考案されたようだ[3]。他の方言ではあまりサポートされていないようだ。HackMD (CodiMD, HedgeDoc) では ::: はやや違った意味にされ、記法も違い限定された語だけしか使えないようだ。

汎用タグを使いすぎれば意味見た目の分離が台なしになることは解っているのだが、最終手段として便利であることは間違いない。

たとえばファイル名 input.md に、Pandoc’s Markdown で書く。

::: {.myaddress}
○△□町会\
会長 ▼▲ ■◆
:::

Markdown から HTML へ

pandoc の標準機能で、特に何もしなくてもいい。コマンドラインで

pandoc --from markdown input.md -to html -o output.html

これにより作成される output.html は次のようになる。

<div class="myaddress">
<p>○△□町会<br />
会長 ▼▲ ■◆</p>
</div>

つまり、Markdown で書いた {.myaddress} がクラス名になる。このクラス名に対応する CSS は別途用意しておく。たとえばファイル名 myaddress.css に

.myaddress {
        text-align: right;
        text-indent: 0pt;
}

と書いておき、コマンドラインで

pandoc --from markdown input.md --to html --output output.html --css myaddress.css --standalone

として利用する。スタイルシートへのリンクがドキュメントヘッダに含まれるため、–standalone オプションも同時に付ける必要がある。

Markdown から LaTeX へ

出力形式を LaTeX とすると、::: は除去されてしまう。つまりコマンドラインで

pandoc --from markdown input.md --to latex --output output.tex

とすると、作成されるファイル output.tex は次のようになる。

○△□町会\\
会長 ▼▲ ■◆

pandoc の作者によるフィルタの例 latexdivs.py を使う[4]と、出力を

\begin{myaddress}

○△□町会\\
会長 ▼▲ ■◆

\end{myaddress}

のようにできる。これを実際に LaTeX で処理するには、 myaddress 環境を別途定義しておく必要がある。たとえば myaddress.sty に

% myaddress
\newenvironment{myaddress}
{\begin{flushright}}
  {\end{flushright}}

と書いておき、コマンドラインで --include-in-header で読み込ませる。

pandoc --from markdown input.md --to latex --output output.tex --include-in-header myaddress.sty --standalone

これにより作成される出力ファイルの内容は

\documentclass (略)
  (略)
% myaddress
\newenvironment{myaddress}
{\begin{flushright}}
  {\end{flushright}}
  (略)
\begin{document}

\begin{myaddress}

○△□町会\\
会長 ▼▲ ■◆

\end{myaddress}

\end{document}

のようになる。

形式の相互変換

以上、markdown → {html, latex} の例を示した。

自分のよく使う markdown, html, latex の相互変換についてまとめると、

  • {markdown, latex} → html は pandoc のみでできる。別途 css を用意するといい。
  • {html, latex} → markdown は pandoc のみでできる。
  • {html, markdown} → latex はフィルタが必要。別途 sty を用意するといい。

となる。

具体的な例

変換された html で利用する CSS ファイル
business.css
latex 出力のための pandoc フィルタ
business_md.py
latex 出力が利用する sty ファイル
business.sty
入力ファイル (markdown)
business-sample.md
pandoc --from markdown --to html --css business.css --standalone --output business-sample.html business-sample.md での出力ファイル (html)
business-sample.html
pandoc --from markdown --to latex --filter=./business_md.py --include-in-header business.sty --standalone --output business-sample.tex business-sample.md[5]での出力ファイル (latex)
business-sample.tex
pandoc --from markdown --to pdf --filter=./business_md.py --include-in-header=./business.sty --output business-sample.pdf business-sample.md[6]での出力ファイル (pdf)
business-sample.pdf
  1. 英文レターの形式を調べると、アメリカ式「フル・ブロック・スタイル」とイギリス式「フル・インデント・スタイル」が見つかる。日本のいわゆるビジネス文書はこのイギリス式「フル・インデント・ス タイル」にとてもよく似ている。特に電磁的文書ならアメリカ式がとても楽だし合理的だと思うのだが、なんと言っても理屈ではない習慣だからそう簡単に廃れることはないだろう。
  2. 検索すると多くの自治体の規程が見つかる。たとえば墨田区佐伯市など。
  3. Syntax for divs
  4. サンプルのフィルタ latexdivs.py をそのまま使うには、入力ファイルのクラス名を書くところに .latex を加えておく必要がある。つまり input.md は ::: {.latex .myaddress} のように書く。
  5. PDF に変換させるには、オプションはこれだけでは実は足りず、ここの例では --pdf-engine=lualatex -V documentclass=bxjsarticle -V classoption=pandoc -V classoption=jafont=auto -V indent=1zw -V pagestyle=empty を加えている。
  6. 前註に同じ。

CSS で見出し(h1, h2, …)に連番を付ける

検索すると似たようなタイトルでたくさんの記事が見つかります。今さら……な話題ですが、それらを参考に試行錯誤して、次のような CSS になりました。

よく見かける例だと、それぞれの見出しでひとつ下の階層のカウンターだけをリセットしているのが多いのですが、それだと h2 の次に(h3 がなくて) h4 が来るような場合にうまくいきません。そういう文書が悪いとも言えますが。

Chrome と Firefox で異なる結果になったりして、結局こうなりました。counter-set を紹介している記事はほとんど見かけません。見つけたのはこの質問と回答くらいで、回答者が仕様を説明してくれているのはとても参考になりました。仕様の EXAMPLE 22 は Firefox では正しく再現されません。

【2022年4月18日追記】現時点での Safari や iOS 上のブラウザー(Webkit)では counter-set が実装されていないため、下記ではうまくいかないようです。【追記終わり】【2023年12月12日追記】Safari や iOS 上のブラウザー(Webkit)に counter-set が実装されたようです。現時点でまだ実際に動作を確認していませんが。【追記終わり】

padding で字下げをしています。番号の書式は「公用文作成の考え方」を参考にしました。\a0 (non-breaking space)をいくつか入れているのは、連番をカタカナとしたので、見出しそのものの内容がカタカナで始まっている場合にある程度あいだが開いていないと混乱するからです。

body {
    counter-reset: chapter section subsection subsubsection paragraph subparagraph;
}

h1 {
    counter-set: section subsection subsubsection paragraph subparagraph;
}
h2 {
    counter-set: subsection subsubsection paragraph subparagraph;
    padding-left:1em;
}
h3 {
    counter-set: subsubsection paragraph subparagraph;
    padding-left:2em;
}
h4 {
    counter-set: paragraph subparagraph;
    padding-left:4em;
}
h5 {
    counter-set: subparagraph;
}

h1::before {
    counter-increment: chapter;
    content: counter(chapter, upper-roman) "\a0\a0\a0\a0";
}
h2::before {
    counter-increment: section;
    content: counter(section, decimal) "\a0\a0\a0\a0";
}
h3::before {
    counter-increment: subsection;
    content: "(" counter(subsection, decimal) ")\a0\a0\a0";
}
h4::before {
    counter-increment: subsubsection;
    content: counter(subsubsection, katakana) "\a0\a0\a0\a0";
}
h5::before {
    counter-increment: paragraph;
    content: "(" counter(paragraph, katakana) ")\a0\a0\a0";
}
h6::before {
    counter-increment: subparagraph;
    content: counter(subparagraph, upper-alpha) "\a0\a0\a0\a0";
}

きっかけは Markdown でした。最近、Firefox なら拡張機能 Markdown Viewer Webext 、Chrome なら拡張機能 Markdown Preview Plus などを使って、手元で Markdown で書いたものをブラウザーで眺めることがしばしばあります。その際に、この文書は見出しに連番がほしいよなあなどと思ったのです。

ここに挙げた2つの拡張機能は CSS を追加できるので、そこにこれを書いてもいいですが、そうすると Markdown なら全て連番付き見出しになってしまいます。拡張機能 Stylus で、好きなときに入れたり切ったりすることにしました。もちろん Markdown のプレビューだけではなく、あらゆるサイトを見る際に適用できます。

2001–2010年ころの日本語システムフォント

これまでにも何度か書いているように、私自身は個人的には「日本語のある程度の長さのまとまった文章には、ゴシック体より明朝体のほうが向いている」と考えています。Web ページにおいても、です。

一方で、「Web ページはゴシック体」という意見が数多く見られます。おそらく、単純に「明朝派」か「ゴシック派」かで言えば、「ゴシック派」のほうがかなり多数のような印象があります。

システムフォントの歴史

いろいろ思い出すために、2000年頃以降のシステムフォント—大きなシェアを占めていた Windows と Mac にデフォルトで装備されているフォント—について、ざっと調べてみました。

Windows

2001XPMS明朝ゴシック 2.30 MS明朝 2.31
2006Vistaメイリオ 5.00 MS明朝ゴシック 5.00 MS明朝 5.00 (JIS X 0213:2004)
20138.1游ゴシック 游明朝

Mac

MacOS 9.2.2までOsaka 平成明朝 リュウミンライト-KL
2001OS X 10.0ヒラギノ角ゴ Pro W4・ヒラギノ明朝 Pro W3
200710.5ヒラギノ角ゴ ProN W4・ヒラギノ明朝 ProN W3
201310.9游ゴシック体 游明朝体 M

フリーフォント

私自身はこの時代より前から今日に至るまでずっと Linux (Debian) を常用していて、それにはデフォルトとか標準という考えがなく好みのフォントを使います。もう記憶が確かでない部分もあるのですが、主流だったと思えるものを拾い出してみました。

1998-1999渡邊フォント
2000-2003Kochi
2003-2004Sazanami
2007IPAゴシック・IPA明朝 (単体配布)
2010IPAexゴシック・IPAex明朝
2010Takaoフォント
2014源ノ角ゴシック / Noto Sans CJK JP
2017源ノ明朝 / Noto Serif CJK JP

参考:ブラウザーの歴史

1992mozaic
1994Netscape Navigator
1996NN3, IE3
1997NN4
2001IE6
2003Safari (10.3から。それ以前(10.2)の標準ブラウザはIEforMac)
2004Firefox 0.8
2006IE7
2008Chrome

漠然とですが、

  • 2001–2010年ころ、Web ページにとって明朝体は、技術的に「使い物にならな」かった
  • そのため、その頃とそれ以降、日本語の Web ページは圧倒的にゴシック体を主体としたものが多い
  • その環境で育った人たちは、もう「Web ページはゴシック体」が当たり前であり、技術的な問題が既に解決されても、むしろ明朝体だと違和感がある

のようなことではなかろうかと考えています[1]

2001年のMacOS Xにヒラギノというのは本当に画期的だったとは思うのですが、何しろシェアが違いすぎ、それにあぐらをかいた Windows のために「暗黒の10年」だったと言っても過言ではありません。ブラウザーの IE6 天下と軌を一にしています。

話はややずれますが、ハードウェアとしてのディスプレイが CRT から LCD になっていったのものこの頃でした。私が切り換えたのはだいぶ遅めの2005年ころでしたが、CRT ではいい具合にボケていた文字の輪郭が LCD だとくっきりしすぎて、文字として美しくなくなったのを覚えています。それからアンチエイリアスとかヒンティングなどを意識することになりました。

  1. 翻って考えると、私の「日本語のある程度の長さのまとまった文章には、ゴシック体より明朝体のほうが向いている」という考えも、WWW 以前の、印刷物に接する時間が長かった(印刷物は言うまでもなく、本文は明朝系であることが圧倒的に多い)影響が強いのかもしれません。

Emacs 風キーバインド

先日、久しぶりに勉強会のようなところへ出かけてみました。そこで会の主要なテーマとは直接関係のない Mac のキーバインドの話を聞きました。私は Mac にはほとんど触れたことがないので知りませんでしたが、おやこれは「Emacs風キーバインド」ではないかと思いました。起源はもう忘れられているかもしれません。

さて、そこで Debian をデスクトップで使っているという自分の環境を思い返してみました。エディターは主に Emacs なので、いろいろなところを「Emacs風キーバインド」にしたいと思いながら、ウィンドウマネージャーを変更したりブラウザーがバージョンアップして拡張機能が効かなくなっていたり、と、いつの間にか不便な状態になっていて、まあそのうち、と思いながら過ごしていたのです。

これを機会に、ちょっと探してみました。ほとんど自分用メモですが、Mac から Linux に乗り換えたい人への情報になるかもしれません。

Xfce4

以前 Gnome を使っていた頃はどこかで設定していたのですが、数年前に Xfce4 に切り替えてから、どうすればいいのか調べてもおらず、そのままになっていました。

「設定」→「設定エディター」で、「チャネル」の一番下の xsettings を開き、Gtk のグループの中のプロパティ KeyThemeName の値に Emacs と入力します。

これで、Xfce4 上で開く多くのアプリケーションで、C-aC-eC-k などが有効になりました。ただし C-y で取り出せるバッファが C-k と連動していないところが Emacs や次の bash と違う挙動で、ときどき混乱してしまいます。

Bash

ターミナル内のシェル は bash です。これは元々「Emacs風キーバインド」のようで、入力行で C-aC-eC-k が使えて、C-k したものを C-y で貼り付けることができます。C-pC-n で履歴をたどれるなど、かなり多くのキーバインドが使えます

Firefox

以前は、拡張機能でそういうのがあったのですが、ブラウザー本体のバージョンアップで使えなくなっていました。

上述の Xfce4 の設定で、いくらかのキーが使えるようになりました。しかし、上下に動かしたいときについ C-pC-n と打って「印刷」「新しいウィンドウを開く」になってしまったり、ページ内検索のつもりでつい C-s と打って「ファイルに保存」になってしまうことがしょっちゅうなので、この辺りを補ってくれるものはないかなあと思っています。

GhostText

Firefox の入力欄 (textarea) で、好きな外部エディターを使えます。以前は別の拡張機能を使っていたのですが、やはりブラウザー本体のバージョンアップで使えなくなってしまい、また別の拡張機能も WordPress の新しい入力画面 Gutenberg との相性が悪そうなので、現在は GhostText を使っています[1]

Emacs 側でも準備が必要で、atomic-chrome パッケージをインストールして、設定しておきます。

  1. GhostText という拡張機能は Firefox にも Chrome にもあり、元々 Emacs 以外のエディターを使うものだったので、それらのどの組み合わせでも使えます。

ニュースのスクレイピングでタイピング練習

ひらがな数文字を打ち返すだけのタイピング練習は案の定すぐに飽きてしまったので、何か別のネタを考えなくてはならなくなりました。飽きないためには膨大かまたは頻繁に更新される元データがあればいい、青空文庫かな、でも小学生に向いているものがどれほどあるかしらん、頻繁に更新されるといえばニュース、でもこれまた小学生向きではなさそう……と思ったら実にぴったりのものがありました。NHK NENS WEB EASY です。ひとつの記事で50字ほどの文が10ほど。意味もわかりやすくて量もちょうどいい。かなり手間をかけて作られているようです。

さて、これをなんとか持ってきてタイピング練習の材料にしようと思ったのですが、何しろ本業でも何でもないので情報を集めるところからスタートでした。今回やっていることは実は「スクレイピング」という程のこともないのですが、そのとっかかりということで、せっかくなのでここに記録しておきます。

環境の準備

NHK NENS WEB EASY のページの肝心な部分は JavaScript で生成されているようで、Python で単純に requests.get(url) とやっても、ブラウザで見ている HTML ソースとは別のものしか得ることができません。そこでまず、ブラウザが実際に表示するページを取得できるようにします。

Debian パッケージ chromium-driver をインストールします。

sudo apt-get install chromium-driver

これを Python から使うためにライブラリ Selenium をインストールします。

pip install selenium

取得した HTML から必要な箇所を切り出すのには BeautifulSoup4 を使います[1]

pip install beautifulsoup4

でインストールします。

ニュースサイトの構造

ニュースサイトによくあることですが、各記事の URL は数字の羅列のような名前で、先頭ページではそれが日々更新されます。

ブラウザのデベロッパーツールで NHK NENS WEB EASY の先頭ページ https://www3.nhk.or.jp/news/easy/ の構造を見てみます。

<div class="top-news-list__pickup news-list-item" id="js-news-pickup">
  ...
  <h1 class="news-list-item__title is-pickup">
  <a href="./k10011463631000/k10011463631000.html"><em class="title"><ruby>日本<rt>にっぽん</rt></ruby>の<ruby>二酸化炭素<rt>にさんかたんそ</rt></ruby>の<ruby>濃度<rt>のうど</rt></ruby>が<ruby>今<rt>いま</rt></ruby>まででいちばん<ruby>高<rt>たか</rt></ruby>くなる</em><time class="time">6月5日 11時30分</time></a>
  </h1>
</div>

最初に大きく取り上げられている記事は <div id="js-news-pickup"> で、その中の <h1> の中の <a> から記事個別ページの URL が得られます。

その記事個別ページを同様にブラウザのデベロッパーツールで見てみると、記事本文は <div id="#js-article-body"> にあることがわかります。これを切り出してくればいい訳です。

スクレイピング

参考にした (というか、ほぼそのままコピーさせてもらった) コードは「Python Webスクレイピング テクニック集」の「JavaScriptによる描画に対応する」です。

ソース中のコメント「ブラウザを起動する」の箇所で、ブラウザのパスを指定する必要がありました。Debian のパッケージを使っている場合

driver = webdriver.Chrome(executable_path='/usr/bin/chromedriver', chrome_options=options)

です。

切り出し

1回めの

        # ブラウザでアクセスする
        siteurl = "https://www3.nhk.or.jp/news/easy/"
        driver.get(siteurl)

        ...

        # BeautifulSoupで扱えるようにパースします
        soup = BeautifulSoup(html, "html.parser")

        # id で特定の要素を切り出す
        href = soup.select_one("#js-news-pickup h1 a").get('href')

で記事個別ニュースの URL が得られるので、2回めは

        driver.get(newsurl)
        html = driver.page_source.encode('utf-8')
        soup = BeautifulSoup(html, "html.parser")
        # ルビを削除
        for s in soup(['rt']):
            s.decompose()

        ...

        # ニュースの本文
        text = soup.select_one("#js-article-body").text

で、記事本文を切り出します。

ルビを削除

ニュースの本文は

...<p><span class="colorC"><ruby>気象庁<rt>きしょうちょう</rt></ruby></span>は「もっと<a href="javascript:void(0)" class="dicWin" id="id-0000"><ruby><span class="under">二酸化炭素</span><rt>にさんかたんそ</rt></ruby></a>を<ruby>出<rt>だ</rt></ruby>さないようにしなければなりません」と<ruby>話<rt>はな</rt></ruby>しています。</p>...

のようになっています。BeautifulSoup の .text で単純にタグを削除するだけでは

気象庁きしょうちょうは「もっと二酸化炭素にさんかたんそを出ださないようにしなければなりません」と話はなしています。

と、ルビが本文に混じってしまい、まともな文になりません。事前に rt 要素を削除しておく必要があります。

この作業をやってみて、rt がほかとは異質なタグ(要素)であることを実感しました。これについてはまた別の記事に書こうと思います。

1文ずつに分解

記事を「。」で区切り、リストにします。「。」自身も含めたいので split が使えません。NHK のニュース記事で全体の最後に「。」がないことはまさかないだろうと仮定して、

        lines = re.findall(".*?。", text)

とします。あとは Errnot がこれを1文ずつ表示するようにするだけです。これでこの bot を相手に XMPP のチャットでオウム返しにタイピングの練習をすることができるようになりました。

それにしても、いろいろ寄せ集めるだけでこれだけできるのですから、便利な世の中になったものだとつくづく思いました。

  1. Python にはじめからある html.parser でもある程度できます。また Selenium にも同様の機能があるようです。

XMPP チャットと bot でタイピング練習

スウちゃん (仮名、4年生) がパソコンでやりたいことのひとつが「字を打てるようになる」こと。

学校でローマ字も習ったし、パソコンには以前からちょっとは触っているので、人差し指打法でたどたどしくは打てます。せっかくだから「正しい」タイピング技法 (タッチタイピング) を習得させてやりたいと思っています。父である私はそれをやらずに我流のまま来てしまいました。ある程度までは速く打てるものの指が定まっていないため間違いが多く、打ち直しのため結局は遅いことになってしまっています。きれいにすらすらと打てる人は本当に間違いも少ないので、スウちゃんには最初からその方向でいってほしいと思っているのです。

子どもでもできそうな練習アプリやサイトを見て回りました。Windows 向けはまず却下。ウェブ版のほとんどは Flash ですが、いまどきこれで大丈夫なのでしょうか。Debian の Firefox に Flash を入れて動かすのが面倒なので、これもできるだけ避けたいところです。

いずれにしても、練習の順番に疑問を感じます。たいていのものはまずホームポジション fj からです。しかしこの2字とも日本語での出現頻度はかなり小さいはずです。jjj fff など日本語でもないから、つまらないことこの上なしです。もっと「日本語のローマ字入力のためのタイピング練習」というのがないものでしょうか。

ちょっと検索してみて、こんな意見を見つけました。「これぞ最速!ブラインドタッチ(タッチタイピング)の効率的な練習法」です。

  1. 指を基本となるホームポジションに置く
  2. パソコンのキーボードを見ながらローマ字のAIUEOを入力する
  3. キーボードを見ずに、パソコン画面を見ながらAIUEOを何度も入力する
  4. 子音を入れつつ練習する
  5. ……

そうだよなあ。この順番のほうがうんと納得がいきます。練習ソフトでこの順番になっているものは意外と見つかりません。

というわけで、既存の練習ソフトに頼らないことにしました。

前回、チャット (XMPP) のクライアント使えるようにしたので、チャットの相手側に私がいて「あお」「いおえ」……などと打ってやり、それをオウム返しに打つことにしました。日本語にない単語になってしまいますが、この際しばらく我慢してもらうことにして。

ついでに、日本語でのキーの出現頻度を検索して見つけた「ローマ字頻度表」によると、母音、n の後は t, k, s, … らしいので、この順に1字づつ (ひらがなでいえば5音づつ) 増やしていくことにしましょう。

さて、ずっと付き合うのは私もたいへんなので、Errbot が相手をするようにプラグインを書いてみました。超いい加減ですがメインはこんな感じ。

    @re_botcmd(pattern=r"^(.*)$", prefixed=False, flags=re.IGNORECASE)
    def question(self, msg, match):
        """文字列を表示する。それを入力してみてください。"""
        if not self.active:
            return None

        s = [[] for i in range(self.levelmax+1)]
        s[0] = 'あいうえおん' * 15
        s[1] = s[0] + 'かきくけこたちつてと' * 10
        s[2] = s[1] + 'さしすせそやゆよ' * 10
        s[3] = s[2] + 'なにぬねのはひふへほ' * 8
        s[4] = s[3] + 'らりるれろ' * 8
        s[5] = s[4] + 'まみむめもわを' * 6
        s[6] = s[5] + '、。ー' * 5
        s[7] = s[6] + 'がぎぐげござじずぜぞ' * 5
        s[8] = s[7] + 'だでどばびぶべぼ'  * 3
        s[9] = s[8] + 'ぢづ'
        s[10]= s[9] + 'ぱぴぷぺぽ' * 2

        reward1 = ['😀','😃','😉','🙂','❤','💗','💞','🎀','👌','✌','👏']
        reward2 = [' OK! ',' いいね! ',' ばっちり! ',' うまい! ',' じょうず! ']

        if match.group(0) == self.get('word'):
            yield(random.choice(reward1) + random.choice(reward2) + random.choice(reward1))

        self['word'] = ''.join(random.choices(s[self.level], k=self.length))
        yield(self['word'])

UI は XMPP クライアントだし応答は Errbot にお任せなので、書くのは実質ほんのわずかです。文字どおり子供だましで、そう遠からず飽きてしまいそうですが。