Go言語で文字列の正規化を行う 〜「Pairs」投稿監視システムの裏側・Part1〜

恋愛・婚活マッチングサービス「Pairs」では、400万人を超える全てのユーザーが安心・安全にサービスを利用できるように、投稿監視システムに力を入れて取り組んでいます。

 

その投稿監視システムの一部として NG ワードフィルタリングを用いて、不正であったり悪質な文字列を事前に検知する仕組みを導入しています。

文字列の正規化処理

NG ワードフィルタリングでは投稿されてきた文字列をあるルールを元に前処理(正規化処理)した文字列をフィルタリングすることによって精度を高めることが可能です。
今回は簡単な文字列の正規化処理として、ひらがなとカタカナの相互変換と、全角と半角の相互変換を紹介させていただきます。

ひらがな/カタカナ変換

ひらがなとカタカナを相互に変換する方法は unicode.SpecialCase が持つ ToUpper と ToLower 関数を利用して変換します。

unicode.SpecialCase

unicode.SpecialCase は unicode.CaseRange 型のスライスをとるようになっています。なので、CaseRange を各自で定義することになります。

type SpecialCase []CaseRange

type CaseRange struct {
    Lo    uint32
    Hi    uint32
    Delta d
}

この CaseRange 型は Unicode の下界値と上界値をそれぞれ Lo と Hi に指定された範囲の Unicode 値を Delta 分スライドさせることができます。また、この Delta にある d の指定は src/unicode/letter.go に書いてあります。

// Indices into the Delta arrays inside CaseRanges for case mapping.
const (
	UpperCase = iota
	LowerCase
	TitleCase
	MaxCase
)

type d [MaxCase]rune // to make the CaseRanges text shorter

上記の const と type から Upper, Lower, Title の順に Delta 値を設定することになります。

CaseRange の設定値

さて、実際にひらがなとカタカナを変換するために CaseRange を設定します。
今回はひらがなを ToUpper でカタカナへ変換し、カタカナを ToLower でひらがなに変換するように設定します。

const (
	hiraganaLo = 0x3041 // ぁ
	hiraganaHi = 0x3096 // ゖ

	katakanaLo = 0x30a1 // ァ
	katakanaHi = 0x30f6 // ヶ
)

var (
	jaCase = unicode.SpecialCase{
		unicode.CaseRange{
			// Hiragana To Katakana
			Lo: hiraganaLo,
			Hi: hiraganaHi,
			Delta: [unicode.MaxCase]rune{
				katakanaLo - hiraganaLo, // UpperCase
				0,
				0,
			},
		},
		unicode.CaseRange{
			// Katakana To Hiragana
			Lo: katakanaLo,
			Hi: katakanaHi,
			Delta: [unicode.MaxCase]rune{
				0,
				hiraganaLo - katakanaLo, // LowerCase
				0,
			},
		},
	}
)

Unicode の参考ソースとして、src/unicode/tables.go にある _Hiragana と _Katakana の Range が参考になりますが、変換対象だけに範囲を狭めています。

var _Hiragana = &RangeTable{
	R16: []Range16{
		{0x3041, 0x3096, 1},
		{0x309d, 0x309f, 1},
	},
	R32: []Range32{
		{0x1b001, 0x1b001, 1},
		{0x1f200, 0x1f200, 1},
	},
}

var _Katakana = &RangeTable{
	R16: []Range16{
		{0x30a1, 0x30fa, 1},
		{0x30fd, 0x30ff, 1},
		{0x31f0, 0x31ff, 1},
		{0x32d0, 0x32fe, 1},
		{0x3300, 0x3357, 1},
		{0xff66, 0xff6f, 1},
		{0xff71, 0xff9d, 1},
	},
	R32: []Range32{
		{0x1b000, 0x1b000, 1},
	},
}

ただし、上の変数は unicode.In(r rune, ranges ...*RangeTable) bool で文字がひらがなかカタカナかを判別するのに使用します。

ToUpper, ToLower

HiraganaToKatakana と KatakanaToHiragana という安直な名前を付けて変換を行います。
実装としては、文字に対してひらがな/カタカナであれば ToUpper/ToLower を実行しているだけです。

// HiraganaToKatakana converts string.
func HiraganaToKatakana(str string) string {
	src := []rune(str)
	dst := make([]rune, len(src))
	for i, r := range src {
		switch {
		case unicode.In(r, unicode.Hiragana):
			dst[i] = jaCase.ToUpper(r)

		default:
			dst[i] = r

		}
	}
	return string(dst)
}

// KatakanaToHiragana converts string.
func KatakanaToHiragana(str string) string {
	src := []rune(str)
	dst := make([]rune, len(src))
	for i, r := range src {
		switch {
		case unicode.In(r, unicode.Katakana):
			dst[i] = jaCase.ToLower(r)

		default:
			dst[i] = r

		}
	}
	return string(dst)
}

わざわざ In でそれぞれの文字形式を見ている理由としては、アルファベットが ToUpper と ToLower に反応してしまうためです。

全角/半角変換

さて、次に全角と半角の相互変換ですが、こちらは /golang と strings.Replacer を併用して実装します。

import "golang.org/x/text/width"

var (
	halfToFullReplacer = strings.NewReplacer(
		"゙", "゛",
		"゚", "゜",
	)

	fullToHalfReplacer = strings.NewReplacer(
		"ガ", "ガ",
		"ギ", "ギ",
		"グ", "グ",
		"ゲ", "ゲ",
		"ゴ", "ゴ",
		"ザ", "ザ",
		"ジ", "ジ",
		"ズ", "ズ",
		"ゼ", "ゼ",
		"ゾ", "ゾ",
		"ダ", "ダ",
		"ヂ", "ヂ",
		"ヅ", "ヅ",
		"デ", "デ",
		"ド", "ド",
		"バ", "バ",
		"パ", "パ",
		"ビ", "ビ",
		"ピ", "ピ",
		"ブ", "ブ",
		"プ", "プ",
		"ベ", "ベ",
		"ペ", "ペ",
		"ボ", "ボ",
		"ポ", "ポ",
		"ヮ", "ワ",
		"ヰ", "イ",
		"ヱ", "エ",
		"ヴ", "ヴ",
	)
)

// HalfWidthToFullWidth converts string.
func HalfWidthToFullWidth(str string) string {
	return width.Widen.String(halfToFullReplacer.Replace(str))
}

// FullWidthToHalfWidth converts string.
func FullWidthToHalfWidth(str string) string {
	return width.Narrow.String(fullToHalfReplacer.Replace(str))
}

text/width では半角カナ周りが上手く変換されないため、先に Replacer を文字列を処理し、そのあとに text/width で全角と半角の変換処理を施しています。

おわりに

Pairsの投稿監視システムでは、文字列を通す正規化処理を他にも実施しているので、Part2ではもう少し深い文字列の前処理をご紹介したいと思います。

今回のソースコードはこちらにプッシュしています。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

エウレカ流Swift Style Guideを公開しました

10 Tips when moving from Objective-C to Swift