LoginSignup
7
3

More than 1 year has passed since last update.

iPhoneからでもクロネコヤマトの配達状況をAPIで取得したいんじゃ!~SwiftでHTMLを解析~

Last updated at Posted at 2021-12-03

初めに

M1 ProのMacBook Proを買いました。性能には120%の満足なのですが、デザインだけはどうも受け入れ難いです、どうもこんにちはTOSHです。
でもなんやかんやPS5とは違って、MacBookは抽選販売にならなかったので、よかったです😆

さて、近年はコロナの影響もあり、ネットで物を買うことが多くなってきているのではないでしょうか?
そうなってくると、いつ配達されるのかは気になりますよね?そこで、クロネコヤマトの配達状況のWebページなんかを利用すると思います。しかし、RestAPIのような形式で使えるものが欲しいですよね?

では、そういったAPIはないのでしょうか?
業者用のAPIは用意されているようですが、こちらに関しては法人としての登録が必要だったり使用が難しいです。
また、本家クロネコヤマトのアプリが使用してるAPIはどうでしょう?
クロネコヤマトのセキュリティに関することになってしまうので、あまり詳しく解説はしないのですが、配達IDを暗号化してクエリに追加しているような形式になっているため、鍵を知らない一般ユーザーがこのAPIを使用するのもどうやら難しそうです。

荷物お問い合わせシステム

そうなったら、荷物お問い合わせシステムを使用するしかありません。
スクリーンショット 2021-12-03 0.05.05.png

このようなシステムを使用して、おり、送り状番号を入力すると、その商品の配達状況を確認することができます。
では、具体的にはこのサービスはどのようにしてリクエストを送信しているのでしょうか?

"endpoint": "https://toi.kuronekoyamato.co.jp/cgi-bin/tneko"
"methodType": "Post"
"query": ["number00": Int, "number01": Int, "number02": Int, "number03": Int, "number04": Int, "number05": Int, "number06": Int, "number07": Int, "number08": Int, "number09": Int, "number10": Int]

このようにリクエストをすると、配達状況のデータを持ったHTMLが帰ってきます。クエリはnumber00には1を入れてあげ、その後number01~10まで最大10個の伝票番号を同時に問い合わせることができます。
また、クエリを付与する際には、number00=1&number01=伝票番号のような形式に変更してあげる必要があります。
コードで書くとこんな感じです。

extension Dictionary where Key == String, Value == Int {
    func equalEncode() -> String {
        return map { key, value in
            return key + "=" + String(value)
        }
        .joined(separator: "&")
    }
}

レスポンスの処理方法

さて、さっきまでの方法で無事リクエストをすることができるようにはなりましたが、問題はレンポンスの形式です。実際に叩いてみるとわかるのですが、これは、jsonが帰ってくるわけではなく、HTML形式でレスポンスが帰ってきます。
なので、クライアント側でそのHTMLを解析して、使いやすい形に変換してあげる必要があります。
まずはModelの作成です。

public struct Tneko: Codable {
    public var deriveryList: [DeliveryList]

    public struct DeliveryList: Codable {
        public var deliveryID: Int
        public var statusList: [DeliveryStatus]

        public init(deliveryID: Int, statusList: [Tneko.DeliveryList.DeliveryStatus]) {
            self.deliveryID = deliveryID
            self.statusList = statusList
        }

        public struct DeliveryStatus: Codable {
            public var status: String
            public var date: String
            public var time: String
            public var shopName: String

            public init(status: String, date: String, time: String, shopName: String) {
                self.status = status
                self.date = date
                self.time = time
                self.shopName = shopName
            }
        }
    }
}

先ほども述べた通り、JSON形式でレスポンスが帰ってくるわけではないので、Codableに準拠する必要はないのですが、あくまで、Codableで定義できる型以外を定義しないという意味や他のエンドポイントとの統一性、JSON形式でmockを生成した際のことなどを考えてCodableに準拠しておくことをおすすめします。

では、ここからが鬼門です。HTMLをこのような形のModelへと変更する必要があります。
HTMLは容易にattributedStringへと変換できるので、attributedStringへと変換してしまうのが良いでしょう。

let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
    if let attributedString = try? NSAttributedString(data: data!, options[.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
        let tneko = Tneko(
             idList: request.idList(),
             response: attributedString.string
        )
        completion(.success(tneko))
    } else {
        completion(.failure(.decodeErrror("This is not HTML")))
    }
}

このようにして形式を変更しておくと、TnekoのModelではStringからModelを作成すれば良いということになります。

StringからModelへの変更

先ほどの、HTMLをStringに変更したものを一部抜粋するとこのような形式になっています。

配達完了
このお品物はお届けが済んでおります。
お問い合わせはサービスセンターまでお願いいたします。
お受け取り日時・場所変更をする
※このお荷物は対象外です
お届け完了のメール通知を受け取る
※このお荷物は対象外です
    • 商品名:
宅急便

    • お届け予定日時:
-

    1.  荷物受付
10月27日 11:27
ZOZO支店

    2.  発送済み
10月27日 11:27
ZOZO支店

    3.  輸送中
10月28日 01:35
〇〇ベース店

    4.  配達完了
10月28日 11:30
〇〇センター

詳細印刷 
Yahoo!地図からもお荷物の状況が確認できます。
※推奨環境はこちら
▲上部に戻る

上記のStringはすべて一行に収まっているので、\nで区切ってあげて、Stringの配列へと変更すると良いでしょう。
ここからは力技です。HTMLの中からルールを見つけ出し、Modelの要素へと変換していきます。変換するコードのイメージは以下の通りです。

extension Tneko {
    public init(idList: [Int], response: String) {
        self.deriveryList = idList.enumerated().map { initialIndex, id in
            let stringList = response.components(separatedBy: "\n")
            var newStatusList: [Tneko.DeliveryList.DeliveryStatus] = []
            var indexCounter = 0
            stringList.enumerated().forEach { index, str in
                if str.contains("お届け予定日時:") {
                    if initialIndex == indexCounter {
                        var counter = 0
                        let initialStatusGroup = stringList[index + 1].split(separator: "
")
                        if var statusCode = initialStatusGroup[0].split(separator: "\t")[safe: 1]?.description {
                            while statusCode.isValidStatusCode {
                                let newStatusGroup = stringList[index + counter + 1].split(separator: "
")
                                let date = newStatusGroup[1].split(separator: " ")[0].description.replacingOccurrences(of: "月", with: "/").replacingOccurrences(of: "日", with: "")
                                let time = newStatusGroup[1].split(separator: " ")[1].description
                                let shopName = newStatusGroup[2].description
                                let status = Tneko.DeliveryList.DeliveryStatus(
                                    status: statusCode,
                                    date: date,
                                    time: time,
                                    shopName: shopName
                                )
                                newStatusList.append(status)
                                counter += 1
                                statusCode = stringList[index + counter + 1].split(separator: "
")[0].split(separator: "\t")[safe: 1]?.description ?? ""
                            }
                        }
                    }
                    indexCounter += 1
                }
            }
            return DeliveryList(deliveryID: id, statusList: newStatusList)
        }
    }
}

extension String {
    var isValidStatusCode: Bool {
        return self == "荷物受付" || self == "発送済み" || self == "輸送中" || self == "配達中" || self == "配達完了" || self == "持戻(ご不在)" || self == "配達完了(宅配ボックス)"
    }
}

だいぶ、力技の解析にはなってしまっていますが、このようにすると、Modelへと落とし込むことができます。

まとめ

HTMLを力技で解析して、クロネコヤマトのWebページから情報を持ってくる方法を紹介しました!
実際に、これを使って作成したのでよかったら使ってみてください!すべてSwiftUIで作成しております。
https://apps.apple.com/jp/app/%E3%82%AF%E3%83%AD%E3%83%8D%E3%82%B3%E9%85%8D%E9%81%94%E7%8A%B6%E6%B3%81/id1585504785

7
3
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
7
3