Clojureで文章を形態素解析して、品詞や読みを指定してマッチした文字列を置換するコマンドを作りました。
この記事では作ったコマンドの使い方と仕組みの話をします。
ソースコード
ソースコードは以下。
GitHub Releasesに実行可能ファイルを配置しています。
Linux用とMac用だけ置いてるので、使う方はこちらを使ってください。
開発環境
Key | Value |
---|---|
OS | Ubuntu 18.04 |
Clojure | 1.10.1 |
Leiningen | 2.9.4 |
使い方
以下のように使います。
$ morsed -p 名詞 -s 寿司 吾輩は猫である。名前はまだない
寿司は寿司である。寿司はまだない
# 標準入力からも入力可能
$ morsed -p 名詞 -s 寿司 < README.adoc
複数条件を指定した場合はAND条件となります。
$ morsed -p 名詞 --part2 代名詞 -s 寿司 彼彼女田中
寿司寿司田中
仕組み
まず形態素解析には kuromoji という形態素解析用のJava製のライブラリを使いました。
以下の処理で形態素解析ができます。
(ns morsed.core
(:import [com.atilika.kuromoji.ipadic Token Tokenizer]))
(defn tokens [^String s]
(.tokenize (Tokenizer.) s))
; (tokens "吾輩は猫である")
Clojureは既存のJavaのライブラリも利用可能なので、Javaの膨大な資産を活用できます。
素晴らしいです。
条件のマッチ判定は以下の処理で行っています。
入力文字列を正規表現オブジェクトに変換しています。
変換された正規表現オブジェクトでマッチするかを再帰的にチェックし、一度でもマッチしなければfalseで処理を抜けます。
全部パスした時だけ置換後の文字列で置き換えます。
(ns morsed.core
(:import [com.atilika.kuromoji.ipadic Token Tokenizer])
(:require [clojure.tools.cli :refer [parse-opts]]
[clojure.string :as str]
[cheshire.core :refer [generate-string]])
(:gen-class))
;; 省略 ;;
(defn re-match-str? [^String ptn
^String text]
(if (nil? ptn)
false
(not (nil? (re-matches (re-pattern ptn) text)))))
(defn token-matched? [^Token token
opts
k]
(case k
:reading (re-match-str? (opts k) (.getReading token))
:part (re-match-str? (opts k) (.getPartOfSpeechLevel1 token))
:part2 (re-match-str? (opts k) (.getPartOfSpeechLevel2 token))
:part3 (re-match-str? (opts k) (.getPartOfSpeechLevel3 token))
:part4 (re-match-str? (opts k) (.getPartOfSpeechLevel4 token))
:pronunciation (re-match-str? (opts k) (.getPronunciation token))
:conjugationform (re-match-str? (opts k) (.getConjugationForm token))
:conjugationtype (re-match-str? (opts k) (.getConjugationType token))
:baseform (re-match-str? (opts k) (.getBaseForm token))
:surface (re-match-str? (opts k) (.getSurface token))
false))
(defn token-matched-all? [^Token token
opts]
(loop [k (keys opts)]
(if (empty? k)
true
(if-not (token-matched? token opts (first k))
false
(recur (rest k))))))
opts には置換に指定する文字列をセットしています。
以下のマップです。空文字でないキーのみを判定に使用します。
{:surface ""
:baseform ""
:conjugationform ""
:conjugationtype ""
:part ""
:part2 ""
:part3 ""
:part4 ""
:pronunciation ""
:reading ""}
詳細はソースコードを見ていただければ。
テストコードを省くと1ファイルで136行しかないので読むのは難しくないと思います。
感想
多少コマンドラインオプションを追加してできることを増やした結果現在は136行ほどですが、初期リリース時点では40行ほどで目的を達成できました。
Clojureの表現力の高さと標準ライブラリの豊富さと、Javaの資産を活用できる仕様のおかげで、短期間で目的を達成できました。
ライブラリの作者様にはとても感謝しております。
余談 実行可能ファイルの自動リリース
GitHub Releasesに配布している実行可能ファイルは、各プラットフォーム向けに生成されたNativeImageです。
GraalVMを利用すると各プラットフォーム向けの実行可能ファイルを生成できます。
ClojureはJava系言語なので、実行可能JARやクラスファイル指定での実行の場合はVMの起動に時間がかかります。
コマンドラインから利用する場合は、VMの起動時間がネックになってしまいます。
この問題を解消するために、NativeImageを作成することにしました。
今回GitHub Actionsを使用して、CIからNativeImageを作成してGitHub Releasesにアップロードするようにしています。
name: release
on:
push:
tags:
- 'v*.*.*'
env:
app-name: 'morsed'
release-files: morsed README.* LICENSE
defaults:
run:
shell: bash
jobs:
build:
runs-on: ${{ matrix.runs-on }}
strategy:
matrix:
include:
- runs-on: ubuntu-latest
os: linux
cmd: gu
opts: '--static'
- runs-on: macOS-latest
os: darwin
cmd: gu
opts: ''
# - runs-on: windows-latest
# os: windows
# cmd: gu.cmd
steps:
- uses: actions/checkout@v2
- uses: DeLaGuardo/setup-graalvm@master
with:
graalvm-version: '20.2.0.java11'
- uses: DeLaGuardo/setup-clojure@master
with:
# To use Clojure CLI 1.10.1.561 based on tools.deps
cli: '1.10.1.469'
# leiningen and boot-cli can be installed as well
lein: 2.9.4
# For leiningen and boot you could use 'latest' version
boot: latest
- name: Build native image
run: |
${{ matrix.cmd }} install native-image
lein uberjar
native-image \
-jar target/${{ env.app-name }}.jar \
-H:Name=${{ env.app-name }} \
-H:+ReportExceptionStackTraces \
-J-Dclojure.spec.skip-macros=true \
-J-Dclojure.compiler.direct-linking=true \
"-H:IncludeResources=command.edn" \
"-H:IncludeResources=schema.edn" \
"-H:IncludeResources=config.edn" \
"-H:IncludeResources=version.txt" \
"-H:IncludeResources=docs.adoc" \
'-H:IncludeResources=.*/.*.bin$' \
--initialize-at-build-time \
--report-unsupported-elements-at-runtime \
-H:Log=registerResource: \
--verbose \
--no-fallback \
--no-server \
${{ matrix.opts }} \
"-J-Xmx3g"
- name: Run command
run: |
./${{ env.app-name }}
./${{ env.app-name }} -p 名詞 -s 寿司 吾輩は猫である。
- name: Create artifact
run: |
assets="${{ env.app-name }}_$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]')"
echo "$assets"
mkdir -p "dist/$assets"
cp -r ${{ env.release-files }} "dist/$assets/"
(
cd dist
if [[ "${{ runner.os }}" == Windows ]]; then
7z a "$assets.zip" "$assets"
else
tar czf "$assets.tar.gz" "$assets"
fi
ls -lah *.*
)
- uses: actions/upload-artifact@v2
with:
name: artifact-${{ matrix.os }}
path: |
dist/*.tar.gz
dist/*.zip
create-release:
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Create Release
id: create-release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: |
${{ github.ref }}
draft: false
prerelease: false
- name: Write upload_url to file
run: echo '${{ steps.create-release.outputs.upload_url }}' > upload_url.txt
- uses: actions/upload-artifact@v2
with:
name: create-release
path: upload_url.txt
upload-release:
runs-on: ubuntu-latest
needs: create-release
strategy:
matrix:
include:
- os: linux
asset_name_suffix: linux.tar.gz
asset_content_type: application/gzip
- os: darwin
asset_name_suffix: macos.tar.gz
asset_content_type: application/gzip
# - os: windows-latest
# asset_name_suffix: windows.zip
# asset_content_type: application/zip
steps:
- uses: actions/download-artifact@v2
with:
name: artifact-${{ matrix.os }}
- uses: actions/download-artifact@v2
with:
name: create-release
- id: vars
run: |
echo "::set-output name=upload_url::$(cat upload_url.txt)"
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.vars.outputs.upload_url }}
asset_path: ${{ env.app-name }}_${{ matrix.asset_name_suffix }}
asset_name: ${{ env.app-name }}_${{ matrix.asset_name_suffix }}
asset_content_type: ${{ matrix.asset_content_type }}
ref: https://github.com/jiro4989/morsed/blob/master/.github/workflows/release.yml
以下のコマンドを実行するだけで、実行可能ファイルの生成が完了します。
快適です。
git tag v0.1.0
git push origin --tags
まとめ
- Clojureで文章を形態素解析して、品詞や読みを指定してマッチした文字列を置換するコマンドの実装の説明をしました
- 使っているライブラリ
- ライブラリの使い方
- 文字列マッチロジック
- NativeImageを作成するGitHubActionsワークフローについて説明しました
以上です