【Flutter】メッセージのURL認識を実装しよう

8 min read読了の目安(約5100字

みなさんこんにちは!つまようじ職人(@moment_1102)といいます。

現在はとあるスタートアップにて、FlutterとFirebaseを用いてモバイルアプリ開発を日々試行錯誤しております。

今回記事にしているのは、チャットの機能を実装する際にメッセージについて、

・メッセージにURLを含んでいる際のリンクの認識
・特に、複数のURLを含んでいる際の実装

について考える機会があるかと思います(私自身、1人で悩んでおりました、、、)

ということで、今回は私自身が自力で実装したのですが、それに関する実装を公開し、もし有識者がみていただけるなら教えていただきたい、と思いまして投稿にいたりました。

実際、これ以上の実装があるのかよくわかっていないのですが、アルゴリズムとしては自身の実装に自信はあったので、ご参考になれば幸いです。

逆に、何か知見等ありましたら、ご連絡いただけると助かります。

URLを含んだ文字列の分割

例えば、URLを含んだ文字列というのは様々なパターンが存在すると思います。

その中でも、以下のような例を考えていきたいと思います。

例)
final List<String> exampleMessages = 
[
 '①https://yahoo.co.jp',
 '②ここから検索できるよ! https://google.co.jp',
 '③意見をhttps://twitter.comで見れるから、https://google.co.jpで検索してね!'
];

こんな風な①、②、③で考えた時に、下に行くほど実装の難易度は上がっていくと思います。

簡単に分類させていただくと、

①そのままURLが入っている
②文字列とURLが混在している
③文字列とURLが複数混在している

というパターンに分類できます。

その際に、③でちゃんと分割ができるようになれば、①と②のようなパターンも分割できるようになるということを今回の実装でやっていきますので、ここから説明していきます。

分割の実装

実際、これはFlutterのようなdart以外の言語でも実装できるものだとは思いますが、文字列の分割作業について考えていきます。

上記の③を例にとって考えていきます。

まずこの作業で行う実装としては、Stringの文字列をList<String>として分割した配列にしていくことを目標にします。

’意見をhttps://twitter.comで見れるから、https://google.co.jpで検索してね!'
=> ['意見を', 'https://twitter.com', 'で見れるから、', 'https://google.co.jp', 'で検索してね!' ]

という風になればいいわけですね。

つまり、こういったメソッドを考えていけば良いわけです。

	// 分割するメソッド
	List<String> getSplittedMessage(String message){
	};
	

それでは、こちらのメソッドを埋めていきたいと思います。

①メッセージからURL抽出

まずは、このメッセージの中からURLを抽出し、それを一つの配列として作成します。

イメージとしましては、

 ’意見をhttps://twitter.comで見れるから、https://google.co.jpで検索してね!'

=> ['https://twitter.com', 'https://google.co.jp' ]

という風になればよいわけです。ここに関する記事はこちらを参考にさせていただきました。
Flutterでテキスト内のURLをチェックする方法

まずは、RegExpクラス(URLなどの判定に用いるクラス)のメソッドを用いて、URLを抽出するメソッドを書きます。

List<String> getSplittedMessage(String message){

  // RegExpを定義
  final RegExp urlRegExp = RegExp(
        r'((https?:www\.)|(https?:\/\/)|(www\.))[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9]{1,6}(\/[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)?');
				
	// ここで、Iterable型でURLの配列を取得
  final Iterable<RegExpMatch> urlMatches =
        urlRegExp.allMatches(message);
	};

これによって、urlMatchesというIterable型の配列が出来上がります。

こちらには、先ほどの例でいくなら、urlMatches = ['https://twitter.com', 'https://google.co.jp']という形で代入され、URLの抽出それ自体はこの時点で完了します

②メッセージの分割

ここから、アルゴリズム的に取り組んでいきます。

最初に代入されたmessageと、先ほどのurlMatchesを用いて分割を行っていきます。

こちらに関しては、そのままコードを載せてもわかりにくいので、まず分割する手順について説明していきます。

元の文章
'意見をhttps://twitter.comで見れるから、https://google.co.jpで検索してね!'

①空の配列と、文字列を用意
 List<String> splittedMessage = <String>[];
 String textEnd = "";

② 1つ目のURL(twitter)で、messageを分割(かならず2つに分かれる)
 ['意見を', 'で見れるから、https://google.co.jpで検索してね!']
 
③ ②の配列で、twitterのURLまでの文章とURLを、splittedMessageに入れる
 splittedMessage = ['意見を', 'https://twitter.com'];
 
④ ②の配列で、残ったメッセージをtextEndに代入
 textEnd = 'で見れるから、https://google.co.jpで検索してね!';
 
⑤ 2つめのURL(google)で、textEndを分割
 ['で見れるから、', 'で検索してね!'];
 
⑥ ⑤の配列で、googleのURLまでの文章とURLを、splittedMessageに入れる
 splittedMessage = ['意見を', 'https://twitter.com', 'で見れるから、', 'https://google.co.jp'];
	
⑦ ⑤の配列で、残ったメッセージをtextEndに代入
 textEnd =  'で検索してね!';

⑧ textEndにはもうURLが含まれていないので、そのままsplittedMessageに入れる
 splittedMessage = ['意見を', 'https://twitter.com', 'で見れるから、', 'https://google.co.jp', 'で検索してね'];

これで、分割された配列splittedMessageが完成

こうやってみると少しくどいのですが、これはURLの個数分だけの繰り返し処理をしております。

そして、当てはまったURLより前の文章を配列に収めていき、残った文末をさらに分割して配列に収めていくというイメージになります。

実装としてはこんなイメージとなります。

List<String> getSplittedMessage(String message) {
    final RegExp urlRegExp = RegExp(
        r'((https?:www\.)|(https?:\/\/)|(www\.))[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9]{1,6}(\/[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)?');
    final Iterable<RegExpMatch> urlMatches =
        urlRegExp.allMatches(message);

    // ①
    final List<String> splittedMessage = <String>[];
    String textEnd = ''; // 文末(文末の分割を繰り返していく)
    int count = -1; // Iterableのためcount

    // messageInLineの分割
    for (RegExpMatch urlMatch in urlMatches) {
      count++;
			// URL取得
      final String url = message.substring(urlMatch.start, urlMatch.end);
      List<String> splittedText;
			
      if (count == 0) {
        // ②
        splittedText = messageInLine.split(url);
      } else {
        // ⑤
        splittedText = messageEnd.split(url);
      }   
      // ③, ⑥ url以前の文字列を分割して、splitTextsに収める
      if (splittedText[0] != url) {
        splittedMessage.add(splittedText[0]);
      }
      splittedMessage.add(url);
      // ④, ⑦
      messageEnd = splittedText.last;
    }
    // ⑧
    if (messageEnd != '') {
      splittedMessage.add(messageEnd);
    }
    return splittedMessage;
  }

ただ、これは気をつけなければいけない例外などがあったり、そもそもの要件などをしっかり定義しておく必要があります。

例えば、このコードの通りにいくと、

・URLが文字列として連続してmessageにある場合
 ex) https://google.co.jphttps://twitter.com

・同じURLが存在している場合(現実的にやるひとはいなさそうだが、、、)
 ex) https://twitter.comだよ!聞いてる?https://twitter.comだって

のような場合ではおそらくうまく分割はできないと思います。

そういった際に、これらのようなメッセージをはじいたりする処理が別途必要となってきます。

まとめ

処理として書くとものすごく泥臭いことをしています。パッケージとかライブラリがあるならそういったものを使う必要がありますが、なかったのでこのような実装となりました。

参考にしていただければ幸いですが、追記で他にも情報などありましたらTwitterなどでメッセージいただければ幸いです。