はじめに
今回、追加読み込みが可能な API の「次に読み込むものがあるか」をネイティブアプリ側に伝える手段として Link Header を利用しました。その際に必要になるパース処理の一例をご紹介します。
フォーマットなどは以下を参考にしてみてください。
Get Link Header
let response: HTTPURLResponse
let linkHeader = response.allHeaderFields["Link"] as? String
ヘッダ情報は HTTPURLResponse から取得可能です。ここでは以下の3点に注意した実装を行う必要があります。
- Link Header が含まれていない場合もある
- リンクは複数である可能性がある
- URL が Foundation.URL に準拠している保証はない
Let's parse
<url>; rel="rel", <url>; rel="rel"
先程取得した文字列には上記のようなフォーマットで値が格納されています。これをパースするのが今回の本題です。
さっそく以下の手順でパースしていきます。
-
<url>; rel="rel"
を1セットとしてグループ化 - グループ毎にパターンマッチング
- マッチング位置を元に url, rel を文字列で抽出
1. グループ化
// ["<url>; rel=\"rel\"", "<url>; rel=\"rel\""]
let links = linkHeader.components(separatedBy: ",").map {
$0.trimmingCharacters(in: .whitespaces)
}
グループの切れ目がカンマなので components を利用して分割します。これだけでも良いですが、カンマ後の半角スペースが余分に含まれてしまうので trimmingCharacters を利用して削除しています。
2. パターンマッチング
links.map { link in
guard let regex = try? NSRegularExpression(pattern: "<(.*)>; rel=\"(.*)\"") else {
return nil
}
...
}
文字列抽出の方法はいくつかあると思いますが、今回は NSRegularExpression で正規表現を用いた抽出を行ってみます。
正規表現の書き方に関しては割愛しますが、取得したい2箇所が ()
で囲まれていることを意識すると、理解できるかと思います。
追記:2022/03/15
今回は url と rel のみが存在する場合のみで正規表現をご紹介しています。しかし、Link Header はそんなに単純なものではなく、title などの属性も使われる場合があります。
NSRegularExpression(pattern: "<(.*?)>(?:[^\"]|\"([^\"]*\")*;\\s*rel=\"(.*?)\"")
また、属性が2つ以上の場合、<url>; title=""; rel=""
と <url>; rel=""; title=""
の2パターンが考えられます。
どちらとも ; rel=""
であることに着目すると上記のように書き換えられます。
さらに別の属性も取得することを考えると…
3. 文字列で抽出
links.map { link in
...
let matches = regex.matches(in: link, range: NSRange(location: .zero, length: link.count))
guard let firstMatche = matches.first,
firstMatche.numberOfRanges >= 2
else {
return nil
}
}
matches 関数を利用して文字列からパターンにマッチする箇所を抽出します。
今回は map 関数内でパターンマッチングを行うため、matches.first
として1つしか取れないことを前提とした処理としています。そして、取得したい箇所は2箇所なので安全のために firstMatche.numberOfRanges >= 2
と制御を入れています。
カンマで分けずに文字列をそのままパターンマッチングさせれば、このような制御処理は不要なのですが、正規表現をシンプルに書きたかったのでこの方法を採用しました。
enum Relation: String {
case next
case none
}
links.map { link in
...
let string: (Int) -> String = { at in
NSString(string: link).substring(with: firstMatche.range(at: at))
}
let url = string(1)
let rel = Relation(rawValue: string(2)) ?? .none
}
最後に仕上げです。
抽出したい箇所の Range をもとに実際の文字列を取得します。
さいごに
いかがだったでしょうか。
フレームワークに頼らずに文字列をパースすると聞くと難しいイメージがありますが、Link Header に関してはかなりシンプルにパースできたと思います。
最後まで読んでいただき、ありがとうございます。