WebEngine

だらだらと綴る技術系メモ

【初心者】matchメソッドでさらっと正規表現を勉強する【JavaScript】

f:id:web-engine:20190630100856p:plain

正規表現

正規表現とは、いくつかの文字列を1つの形式で表現するための表現方法です。 この表現方法を利用すれば、たくさんの文章の中からカンタンに特定の文字列を抽出できます。


ここではJavaScript正規表現について見てみます。


しかし、正規表現は、それだけで本1冊分の内容になるほど複雑で多様です。
すべて網羅することは不可能というか、おこがましいので、詳しい解説はほかに譲り、ここでは、初めて正規表現に触れる人がアレルギーを起こさないように注意してまとめました。


1. match()メソッド
2. フラグ
3. 正規表現の検索内容
4. 正規表現で組み合わせてみる
5. RegExpオブジェクト
6. 参考


1. match()メソッド

正規表現を検証する際に、match()を使います。

構文は

文字列オブジェクト.match(正規表現オブジェクト)

正規表現オブジェクトの部分は、/検索内容/と記述します。

結果は配列になって返ってきます。マッチする対象がなければnullを返します。

const test1 = "hello HELLO world hello".match(/hello/)
console.log(test1)
// ["hello", index: 0, input: "hello HELLO world hello", groups: undefined]

const test2 = "hello HELLO world hello".match(/JavaScript/)
console.log(test2)
// null


2. フラグ

対象文字列のすべてを対象にする(/検索内容/g)

match()メソッドの使用例の際には、検索対象の最初のhelloしか認識してくれませんでした。
後半のhelloにもヒットさせるには、フラグを追記します。

const test = "hello HELLO world hello".match(/hello/g)
console.log(test)
// ["hello", "hello"]

結果である配列にヒットした2つのhelloが入りました。
フラグ g は最初のマッチの後に止まることなくすべてのマッチを探します。
フラグの書き方は/検索内容/フラグと2つのスラッシュのあとに加えるだけです。

大文字・小文字の無視(/検索内容/i)

対象文字列には小文字のhello以外に大文字のHELLOが含まれていました。
これをヒットさせたい場合、フラグ i を使います。
フラグは一つのみしか使えないわけではないので、今度は gi を併せて使ってみます。

const test = "hello HELLO world hello".match(/hello/gi)
console.log(test)
// ["hello", "HELLO", "hello"]

大文字のHELLO、後半のhelloにヒットしました。


3. 正規表現の検索内容

正規表現の検索内容の部分には、さまざまな記号や書式を使うことができます。
正規表現」で検索すると、表にまとめられたものがいくつかヒットするので、暇なときに眺めてみるとよいかもしれません。

ここでは、簡単な例のみを紹介します。

任意の1文字(ドット)

const test = "hellow HELLO hell hallo".match(/h.llo/gi)
console.log(test)
//  ["hello", "HELLO", "hallo"]

. (ドット)は、どんな文字でもいい、任意の1文字を示す記号になります。
上記例では、hell以外にヒットします。


いずれかの1文字([a,b…])

const test = "hellow HELLO hullo hallo".match(/h[a,e]llo/gi)
console.log(test)
//  ["hello", "HELLO", "hallo"]

角括弧(ブラケット)内に、いくつかの文字をカンマ区切りで入れると、角括弧内のいずれかの1文字を示すものになります。
[a,e] では、a か e という意味にになります。
u は角括弧内に入っていないため、hullo はヒットしません。


いずれかの1文字([a-b])

const test = "hellow HELLO hullo hallo".match(/h[a-e]llo/gi)
console.log(test)
//  ["hello", "HELLO", "hallo"]

カンマ区切りの場合と異なり、ハイフンを利用した場合は、範囲指定となります。
[a-e] では、a から e の文字が1文字入っていれば、という意味になります。




ほかにもたくさんあるのですが、このへんにしておきます。
とにかく、このような記号、書式を組み合わせることで、正規表現は強い力を発揮します。

4. 正規表現を組み合わせてみる

正規表現の力を示すために、記号と書式を組合せてパターンをつくってみます。
以下の例では、すべて半角数値かチェックしています。

const test1 = "1234567890".match(/^[0-9]+$/) 
console.log(test1) 
// ["1234567890", index: 0, input: "1234567890", groups: undefined]

const test2 = "1234567890".match(/^[0-9]+$/) 
console.log(test2) 
// null

2つ目の例は7が全角なので一致せず、 null が返ってきます。

[0-9] は先ほど解説したとおり、いずれかの1文字を表すものを数値に置き換えただけです。0から9のうちのいずれかの1文字、という意味になります。

^ は、^a とすれば、a で始まる文字列、という意味です。ここでは、^ のあとに[0-9]と続いているので、0から9のうちいずれかで始まる一文字で始まる文字列、ということなります。よって、最初がアルファベットであったり、全角数値で始まることを許しません。

+ は、a+ とすることで1個以上の a という意味になります。
$ は、a$とすることで a で終わる文字列という意味です。


これらを併せて例であるソースコードを考えると

「0~9の数値で始まり、それが1個以上となり、かつ数値で終わる文字列」

という構成の正規表現になっていることがわかります。


ちなみに0から9までを表す[0-9]ですが、\dと置き換えることもできます。

const test1 = "1234567890".match(/^\d+$/) 
console.log(test1) 
// ["1234567890", index: 0, input: "1234567890", groups: undefined]

const test2 = "1234567890".match(/^\d+$/) 
console.log(test2) 
// null

\dは、[0-9]と同じ意味なので、同じ結果が返ってきます。


5. RegExpオブジェクト

検索部分を/検索内容/フラグと書いてきましたが、この書き方は、RegExpオブジェクトを簡略化した記述です。

RegExpオブジェクトを使う場合には、第1引数に検索内容、第2引数にフラグを渡します。

const re = new RegExp("hello", "gi")
const test = "hello HELLO world hello".match(re)
console.log(test)
// ["hello", "HELLO", "hello"]

覚えておくと役に立つかもしれません。

6. 参考


スプレッド構文の使い方【JavaScript】

スプレッド構文(演算子)の使い方

スプレッド構文

スプレーや霧吹きは、高圧の空気などを用いて、液体を霧状に噴出する装置です。
この機構を最初に思いついた人は天才だなあ、と思います。
そういうものはたくさんありますが、自分の身近にあるものは尚更そう感じます。

そんなスプレーのように、配列やオブジェクトの値を拡散させることが可能なのが、スプレッド構文です。

スプレッド演算子、という呼び名も使われているようですが、MDNでは スプレッド構文となっているので、ここではその呼称で統一します。


1. 基本的な使い方
2. Arrayリテラルでの使用例
3. Objectリテラルでの使用例
4. 注意点
5. 参考


1. 基本的な使い方

以下の例では、関数sumの引数に、構文である...をつけて配列numsを渡しています。このように記述することで、配列の値を、sum関数のx、y、zに割り当てることができます。

const sum = (x, y, z) => x + y + z
const nums = [1, 5, 15]
console.log( sum(...nums) )  // 21


また、関数の引数をスプレッド構文にすれば、関数に渡される引数を配列として扱うことができます。

const test = (...nums)  => nums[1]
console.log(test(1, 5, 15))  // 5


2. Arrayリテラルでの使用例

配列のコピー

スプレッド構文によるコピーはシャローコピーとなります。

const ary1 = [1, 2, 3]
// 配列のコピー
const ary2 = ary1  // 参照渡し
const ary3 = [...ary1]  // シャローコピー
// コピー検証
console.log(ary1 === ary2)  // true
console.log(ary1 === ary3)  // false

// 要素を追加した新しい配列を作成
const ary4 = ['hello', ...ary1, 7] // ['hello', 1, 2, 3, 7]


シャローコピーなので、ネストになっている配列とオブジェクトには注意が必要です。
(対象がネストになっていると、ディープコピーではすべての階層について実体をコピーするのに対し、シャローコピーでは最初の1階層のみ実体がコピーされる)

const baseArray = [1, [2, 3], [4, 5]]
let copyArray = [...baseArray]
copyArray[0] = 'aaa'
copyArray[1][0] = 'bbb'
console.log(baseArray, copyArray)  // [ 1, [ 'bbb', 3 ], [ 4, 5 ] ] [ 'aaa', [ 'bbb', 3 ], [ 4, 5 ] ]


配列のマージ

concat()を使わず、簡潔に書けるようになりました。

const ary1 = ["a", 0, "b"]
const ary2 = [1, "hello", 99]
const ary3 = [...ary1, ...ary2]  // ["a", 0, "b", 1, "hello", 99]


3. Objectリテラルでの使用例

コピーとマージの方法、結果は配列と同じです。

オブジェクトのコピー

const obj1 = {name: "tanaka", age: 20}
// オブジェクトのコピー
const obj2 = obj1  // 参照渡し
const obj3 = {...obj1}  // シャローコピー
// コピー検証
console.log(obj1 === obj2)  // true
console.log(obj1 === obj3)  // false

// 要素を追加した新しいオブジェクトを作成
const obj4 = {...obj1, hobby: "guiter"}  // { name: "tanaka", age: 20, hobby: "guiter" }

配列と同じく、ネストされているオブジェクトには注意。

const baseObject = {
  name: {
    firstname: "Taro",
    familyname: "Tanaka"
  },
  age: 20
}
let copyObject = {...baseObject}
copyObject.name.firstname = "Jiro"
copyObject.age = 35
console.log(baseObject, copyObject)
// {age: 20, name: { familyname: "Tanaka", firstname: "Jiro" } }
// {age: 35, name: { familyname: "Tanaka", firstname: "Jiro" } }


オブジェクトのマージ

const human = {name: "tanaka", age: 20}
const subinfo = {hobby: "guiter", job: "enginner"}
const tanaka = {...human, ...subinfo}  // {name: "tanaka", age: 20, hobby: "guiter", job: "enginner"}


4. 注意点

オブジェクト内に配列は展開できるが、配列にオブジェクトは展開できない

以下のようにオブジェクトを配列内に展開しようとするとエラーになります。

const obj = {name: "tanaka", age: 20}
const ary = [1, ...obj, 2]  // TypeError: obj is not iterable

ただし、配列をオブジェクトに展開することは可能です。

const ary = ["Takana play guiter.", "Human data"]
const obj = {name: "tanaka",  age: 20, ...ary}  // {0: "Takana play guiter.", 1: "Human data", name: "tanaka", age: 20}


null, undefined

nullやundefinedをスプレッド構文で扱おうとすると、配列はエラーになるが、オブジェクトの場合はエラーになりません。

// 配列
[...null]  // Uncaught TypeError: object null is not iterable
[...undefined]  // Uncaught TypeError: undefined is not iterable
// オブジェクト
{...null}  // {}
{...undefined}  // {}


IE未対応

言わずもがな。


5. 参考


引っ掛けの多いタプル【Python】

f:id:web-engine:20190615135810p:plain

環境

すべてコマンドライン上で確認

タプルってなに?

タップルではありません。タプルです。


タプルってなに?

「コレクション」タイプのデータ型の一種です。

アクセスの仕方はリストとだいたい同じですが、リストと異なり、生成したあとに変更ができないオブジェクトになります。
主として、この変更不可能性を利用して、変更を許したくない変数を定義したりするのに使います。

使い方はこんな感じ。

tp = ('apple', 'orange', 'grape')
type(tp)  # <class 'tuple'>

# アクセス方法
tp[0]  # 'apple'
tp[1:]  # ('orange', 'grape')

# タプルの生成後に変更が不可能
tp[1] = 'banana' # TypeError: 'tuple' object does not support item assignment


Pythonのタプルには、大きく分けて2つ引っかかるところがあると思います。

1. 「変更できない」の厳密な意味
2. 要素が1つだけのタプルを生成するとき

順に見ていきます。

引っ掛け1.「変更できない」の厳密な意味

注意点として、「変更ができない」とは、厳密にはオブジェクト id を変えずに、要素を追加・変更・削除することができないという意味です。

この性質をイミュータブル( immutable )と呼びます。

どういうことかと言うと、以下の例はタプルでも可能だということです。

  1. タプルを参照している変数への再代入
  2. タプル内のミュータブルな要素の変更


1. タプルを参照している変数への再代入

tp = ('apple', 'orange', 'grape')
tp = ('apple', 'orange', 'banana')

変数がタプルを参照している場合でも、そこに別のオブジェクトを代入し中身を置き換えることは可能です。

ですが、これは Python には定数(再代入ができないもの)がないというルールに起因するものなので、理解の範疇といえるでしょう。


混乱するのは+=演算を利用した場合です。

tp = ('apple', 'orange', 'grape')
tp += ('banana', 'grapefruit')
print(tp)  # ('apple', 'orange', 'grape', 'banana', 'grapefruit')

なぜこのソースコードも通るのでしょうか。

実は、+=を使っている行では、既存のタプルが変更されているのではありません。新しいタプルが生成されていて、オブジェクトのidが変わっています。tpに再代入されているのは新しいオブジェクトなのです。

tp = ('apple', 'orange', 'grape')
id(tp)  # 2252232
tp += ('banana', 'grapefruit')
id(tp)  # 2732848


リストの場合は、オブジェクトのidが維持されます。

lt = ['apple', 'orange', 'grape']
id(lt)  # 2274480
lt += ['banana', 'grapefruit']
id(lt)  # 2274480


2. タプル内のミュータブルな要素の変更

ミュータブルというのは、イミュータブルの逆で、要は変更可能という意味です。

sites = ({'name': 'WebEngine', 'url': 'https://example.com'}, {'name': 'hoge', 'url': 'https://hoge.co.jp'})
sites[0]['url'] = 'http://web-engine.hatenadiary.com'
print(sites)  #  ({'name': 'WebEngine', 'url': 'http://web-engine.hatenadiary.com'}, {'name': 'hoge', 'url': 'https://hoge.co.jp'})

このソースコードは通ります。
タプル内の要素である辞書(dict型)がミュータブルだからです。

Pythonでは、タプル内の要素そのものが変更可能であれば、その要素自体の編集は問題ないのです。

引っ掛け2.要素が1つだけのタプルを生成するとき

要素が1つだけのタプルを生成しようとして()内に1つだけオブジェクトを書くと、()は無視されて処理され、タプルとは見なされません。

tp = ('apple')
print(tp)  # 'apple'
type(tp)  # <class 'str'>

要素が1つだけのタプルを生成するには、カンマが必要になります。

tp = ('apple',)
print(tp)  # ('apple', )
type(tp)  # <class 'tuple'>

+=等で要素を1つ加えるときなどには、カンマを忘れないように注意する必要があります。

tp = ('apple', 'orange', 'grape')
tp += ('banana')  # TypeError: can only concatenate tuple (not "str") to tuple
tp += ('banana',)  # OK


どうして要素が1つのタプルにカンマが必要なのかといいますと、タプルは()に囲まれた値なのではなく、カンマで区切られた値だからです。

驚くことに、()が省略されていてもタプルとして処理されます。

tp = 'apple','orange','grape'
type(tp)  # <class 'tuple'>


ただし、空のタプルをつくる場合は素直に書きます。
カンマだけ書くとエラーになります。

tp = ()
type(tp)  # <class 'tuple'>
tp = ,  # error


まとめ

Pythonはリストや辞書型の利便性に目が行きがちで、タプルが拗ねている節がある。


参考