LoginSignup
1
0

More than 1 year has passed since last update.

Swift で Link Header をパースする

Last updated at Posted at 2022-03-14

はじめに

今回、追加読み込みが可能な 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"

先程取得した文字列には上記のようなフォーマットで値が格納されています。これをパースするのが今回の本題です。
さっそく以下の手順でパースしていきます。

  1. <url>; rel="rel" を1セットとしてグループ化
  2. グループ毎にパターンマッチング
  3. マッチング位置を元に 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 に関してはかなりシンプルにパースできたと思います。

最後まで読んでいただき、ありがとうございます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0