Lua#11 Luaの部分文字列取得と文字列パターンマッチング

Last modified: 2021-07-30

ここでは文字列内の特定範囲を取り出す操作と、文字列のパターンマッチングについてを扱う。

解説

文字列内の特定範囲を取り出す

文字列のメンバ関数sub()を用いると、その何文字目から何文字目までかを指定してその範囲が取り出せる。

最初の引数が開始位置、2番目の引数が終了位置(省略時は末尾まで)となる。

-- 文字列を用意
> text = 'abcdefghij'

-- 半角英数の2文字目から4文字目までを取り出す
> text:sub(2, 4)
bcd

-- 半角英数の7文字目から末尾までを取り出す
> text:sub(7)
ghij

開始位置を末尾(右)側から数えて指定することも可能で、末尾から数えた開始位置を絶対値にとる負の値を指定する。

-- 末尾の半角英数3文字を取り出す
> text:sub(-3)
hij

文字列内に文字列が含まれるかをテストする

文字列のメンバ関数find()を用いると、引数に指定したパターンに合致する最初の位置を数値で返し、合致しなければnilが返る。

パターンの指定には記法(日本語訳)がある。

パターン文字列を用いず、指定した文字列そのものを含むかどうかを調べるには、その文字列の後ろに

  • 何文字目から調べるか(既定値は1)
  • 指定文字列そのものを含むかを調べるための指定true

の2つの引数を追加する。

-- 文字列を用意
> text = 'Lua cool lua LUA kur lua'

-- 文字列 "lua" が最初に見つかった範囲を得る
> text:find('lua', 1, true)
10      12

-- 見つからない場合は nil が返る
> text:find('@', 1, true)
nil

-- 戻り値を変数に入れて値を表示する
> i_start, i_end = text:find('LUA', 1, true)
> print(('i_start=%d, i_end=%d'):format(i_start, i_end))
i_start=14, i_end=16

最短と最長のマッチング

パターン指定において.-.+はどちらも任意の複数文字を示すが、一致する箇所が最短になるようにしたいか最長になるようにしたいかで使い分ける。

  • .-: 最短
  • .+: 最長
-- "lu ...(最短)... a" のパターンで調べる
> i_start, i_end = text:find('lu.-a')
> print(('%d, %d: %s'):format(i_start, i_end, text:sub(i_start, i_end)))
10, 12: lua

-- "lu ...(最長)... a" のパターンで調べる
> i_start, i_end = text:find('lu.+a')
> print(('%d, %d: %s'):format(i_start, i_end, text:sub(i_start, i_end)))
10, 24: lua LUA kur lua

パターンに一致する各部分を取り出す

文字列のメンバ関数match()を用いると、引数に指定したパターンの丸括弧内が一致した部分の文字列を戻り値として得ることができる。

戻り値の数とmatch()内の丸括弧の数は同じである必要がある。

-- 文字列を用意
> text = 'My Name <name@example.com>'

-- 名前部分と不等号内を変数に入れる
> name, email = text:match('(.-) +<(.+)>')

-- 各変数の中身を表示
> print(('name: "%s"\nemail: "%s"'):format(name, email))
name: "My Name"
email: "name@example.com"

コード例

以上の内容を踏まえた文字列処理の例を示す。

[任意]ファイル名:011_substrings-and-matching.lua エンコーディング:UTF-8
#! /usr/bin/lua
-- -*- coding: utf-8 -*-

--[[--  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --
動作確認バージョン: 5.4, JIT2.1
--  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  --]]

-----
----- 文字列の部分文字列やマッチングの処理
-----

-- 部分文字列の処理
do
    -- 扱う文字列
    local datetime = '2021/01/23 12:34:56'

    -- 部分文字列を得るsub()には
    -- (半角英数で)何文字目から何文字目まで取り出すかを指定する
    local year = datetime:sub(1, 4)
    local month = datetime:sub(6, 7)
    local day = datetime:sub(9, 10)
    -- 2つ目の引数を省略すると末尾までが取り出せる
    local time = datetime:sub(12)
    -- 末尾から数えた開始位置を絶対値にとる負の値を1つ指定すると
    -- 末尾から指定文字数が取り出せる
    -- local time = datetime:sub(-8)
    print(('year:%s month:%s day:%s time:%s'):format(year, month, day, time))
end

print(('-'):rep(80))  -- 出力の区切り線

-- 文字列のパターンマッチング
do
    -- 扱う文字列
    local local_path = '/dir1/dir2/file.lua'

    -- 最後の "/" の前後をそれぞれの変数に入れる例
    -- match()内の丸括弧の数と戻り値(文の "=" の左側)の変数の数は
    -- 同じである必要がある
    local dirname, basename = local_path:match('(.+)/(.+)')
    print(
        ('%s --> dirname:%s, basename:%s'):format(
            local_path, dirname, basename))

    -- 最初に ".lua" に合致する位置を返す
    -- find()の2番目の引数は何文字目から探すか(省略時は1)
    -- 3番目の引数が false または省略されると単純な文字列を探すのではなく
    -- 指定したパターン文字列に基づいて探すようになる
    local i = local_path:find('.lua', 1, true)  -- 16文字目に ".lua" の先頭
    print(('("%s"):find(".lua", 1, true) --> %d'):format(local_path, i))

    -- 合致するものがない場合は nil が返る
    if local_path:find('.LUA', 1, true) == nil then
        io.stderr:write(('No ".LUA" in "%s"\n'):format(local_path))
    end
end

print(('-'):rep(80))

do
    -- 扱う文字列
    local urls = {
        'https://www.example.com/dir1/dir2/file.ext',
        'http://www.example.net/dir1/',
        'http://www.example.net/dir1',
        'http://www.example.net',
    }

    for _, url in ipairs(urls) do
        -- 最初の "://" の左側と右側の文字列をそれぞれの変数に入れる
        -- このプログラムでは "://" を含んでいるものとみなしているが
        -- 場合によっては異常終了を避ける目的でチェックする必要がある
        local scheme, host_and_path = url:match('(.-)://(.+)')

        -- これら2つの変数はこの後の分岐内で中身を入れるが
        -- 取り出しは分岐の終わった後で行うため、ここで local を書いておく
        -- (分岐内で local を記述するとその分岐内でしかその値が読み込めない)
        local host
        local path
        -- "www.example.net" のようにホスト名部分のみで
        -- "/" 以降を含まない形の場合は別の形で処理するので
        -- "/" を含む場合と含まない場合とで分岐
        if host_and_path:find('/', 1, true) then
            -- 最初の "/" の左側と右側で分ける
            -- "/" はパス名部分に含まれるようにしたいので2つ目の丸括弧の中へ記述
            host, path = host_and_path:match('(.-)(/.+)')
        else
            -- "/" 以降を含まない形はホスト名とみなす
            -- その際のパス名は "/" となる
            host = host_and_path
            path = '/'
        end

        -- それぞれを表示
        print(('URL: %s'):format(url))
        print(('  Scheme: %s'):format(scheme))
        print(('  Host: %s'):format(host))
        print(('  Path: %s'):format(path))
    end
end

-- 終了

下は実行結果。

year:2021 month:01 day:23 time:12:34:56
--------------------------------------------------------------------------------
/dir1/dir2/file.lua --> dirname:/dir1/dir2, basename:file.lua
("/dir1/dir2/file.lua"):find(".lua", 1, true) --> 16
--------------------------------------------------------------------------------
URL: https://www.example.com/dir1/dir2/file.ext
  Scheme: https
  Host: www.example.com
  Path: /dir1/dir2/file.ext
URL: http://www.example.net/dir1/
  Scheme: http
  Host: www.example.net
  Path: /dir1/
URL: http://www.example.net/dir1
  Scheme: http
  Host: www.example.net
  Path: /dir1
URL: http://www.example.net
  Scheme: http
  Host: www.example.net
  Path: /
  • 使用したバージョン
    • Lua 5.4.2
    • LuaJIT 2.1.0 Beta 3