42314


Swift 4 で書かれた下記のコードでデータを取得していますが取得がうまくいきません。  
ほかのアドレスではデータの取得が出来ますが、下記のアドレスでは`data=nil`となりエラーになります。

問題点は2点です(原因は1点かもしれませんが)。

  * dataがnilではないときに「データ抽出」部分が走るはずですが、dataにnilが入っており、正常なデータを取得出来ません。  
（`if (data != nil && err ==
nil)`としており、その下ではnilではないことが保証されると思うのですが実際にはnil入っている為、エラーになる）

  * `data=nil`になっており `https://trackings.post.japanpost.jp/m/services/srv/search/?requestNo1=123456789012&search=%8AJ%8En` とブラウザで打ち込んだときと同等の正しいデータが取得出来ない

[code]

            //HTTPリクエスト作成
            let url = URL(string: "https://trackings.post.japanpost.jp/m/services/srv/search/?" +
                "requestNo1=123456789012&search=%8AJ%8En")
            let req = NSMutableURLRequest(url: url!)
            req.httpMethod = "GET"
            var waiting = true
            let task = URLSession.shared.dataTask(with: req as URLRequest , completionHandler: { data, res, err  in
                if (data != nil && err == nil){
                    let text = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
                    DispatchQueue.main.async(execute: {
                        //データ抽出
                        let arr = text!.components(separatedBy: "お問い合わせ番号")
                        if arr.count == 1 {
                            //有効なデータが帰ってこない場合
                            result = "エラー"
                        } else {
                            result = text! as String
                        }
                        waiting = false
                    })
                } else {
                    DispatchQueue.main.async(execute: {
                    })
                }
    
            })
            result = result.uppercased()
            task.resume()
            while (waiting) {
                RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date())
            }
    
            //結果を出力
            print(result)
    
[/code]

ご指導のほど、よろしくお願いいたします。

## 追記

`let arr = text!.components(separatedBy: "お問い合わせ番号")`のところで、`Thread 1: Fatal
error: Unexpectedly found nil while unwrapping an Optional value`となる。

[![このときの状態](https://i.stack.imgur.com/WNX1S.png)](https://i.stack.imgur.com/WNX1S.png)

[![取得データ](https://i.stack.imgur.com/FJWVv.png)](https://i.stack.imgur.com/FJWVv.png)


CC BY-SA 3.0
2018-03-11T00:34:54.353
42308
2018-03-11T03:18:13.937
2020-06-17T08:14:45.997
-1
10845
question
swift ios xcode
URLSession.shared.dataTaskでデータがうまく取得出来ない


ご掲載いただいた`data`の中身はやはりUTF-8ではないようです。並んでいる数値からしておそらくSJIS
(Windows-31J)と思われます。返されているのはHTMLで`charset`を表す`meta`タグもありませんので、レスポンスヘッダーを確認しないといけないのですが、お示しいただいたサイトは`Content-
Type: text/html;
charset=windows-31j`と言うヘッダーを返しているようなので、以下はWindows-31Jとしてコードを書いてあります。

以下のコードをコンパイルするにはクラス定義の外側に以下のようなextensionを定義しておく必要があります。

[code]

    extension String.Encoding {
        static let windows31j = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.dosJapanese.rawValue)))
    }
    
[/code]

コードの本題の部分ですが、呼び出し側がないと完了ハンドラーパターンの使い方がわかりにくいので、適当なViewController内のメソッドとすることを前提に記載しています。

[code]

    class ViewController: UIViewController {
    
        //...
    
        //呼び出し側、何かのボタンが押されたらデータを取得するものとしている
        @IBAction func buttonPressed(_ sender: UIButton) {
            //`parseHtmlData`に自前のクロージャーを渡して呼び出す
            parseHtmlData(completion: {result in
                if let result = result {
                    //結果を出力
                    print(result)
                } else {
                    print("通信エラー")
                }
            })
        }
    
        //呼び出される側、自前の完了ハンドラーをパラメータの最後に含めておく
        func parseHtmlData(completion: @escaping (String?)->Void) {
            //HTTPリクエスト作成
            let url = URL(string: "https://trackings.post.japanpost.jp/m/services/srv/search/?" +
                "requestNo1=123456789012&search=%8AJ%8En")
            var req = URLRequest(url: url!) //可能な限り`NSMutableURLRequest`ではなく`URLRequest`を使う
            req.httpMethod = "GET"
            //waitingなんてフラグは使用しない
            let task = URLSession.shared.dataTask(with: req as URLRequest , completionHandler: { data, res, err  in
                //非nilの値を後で利用するならif-letを使用した方が良い
                if let data = data, err == nil {
                    print(data as NSData, res!.textEncodingName ?? "encoding unknown") //デバッグ用
                    var text: String? = String(data: data, encoding: .utf8) //可能な限り`NSString`ではなく`String`を利用する
                    //これだけだと元の`data`がUTF-8でない場合に`text`がnilになってしまう
                    if text == nil {
                        //UTF-8 でダメだったらSJIS(Windows-31J)を試す
                        text = String(data: data, encoding: .windows31j)
                        //Windows-31Jでもダメならエラーとする
                        if text == nil {
                            DispatchQueue.main.async(execute: {
                                completion(nil)
                            })
                        }
                    }
                    DispatchQueue.main.async(execute: {
                        //データ抽出
                        let arr = text!.components(separatedBy: "お問い合わせ番号")
                        var result: String
                        if arr.count == 1 {
                            //有効なデータが帰ってこない場合
                            result = "エラー"
                        } else {
                            result = text!
                        }
                        //結果は必ず完了ハンドラーの中で使う
                        result = result.uppercased()
                        //完了ハンドラーの中で自前に完了ハンドラーを呼び出す
                        completion(result)
                    })
                } else {
                    //エラーを黙って無視しない
                    if let error = err {
                        print(error)
                    }
                    if data == nil {
                        print("data is nil")
                    }
                    DispatchQueue.main.async(execute: {
                        //何も書かれていなかったが、エラー時にはnilを完了ハンドラーに渡すことにする
                        completion(nil)
                    })
                }
    
            })
            task.resume()
            //フラグ成立まで待ち続けるなんてことはしない
        }
    
    }
    
[/code]

今回の件で一番肝心な部分は、あなたのコードの

[code]

                let text = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
    
[/code]

のところを、

[code]

                var text: String? = String(data: data, encoding: .utf8) //可能な限り`NSString`ではなく`String`を利用する
                //これだけだと元の`data`がUTF-8でない場合に`text`がnilになってしまう
                if text == nil {
                    //UTF-8 でダメだったらSJIS(Windows-31J)を試す
                    text = String(data: data, encoding: .windows31j)
                    //Windows-31Jでもダメならエラーとする
                    if text == nil {
                        DispatchQueue.main.async(execute: {
                            completion(nil)
                        })
                    }
                }
    
[/code]

のように書き換え、「UTF-8のデータじゃなかったらWindows-31Jとして試してみる」ようにした点です。(`String`か`NSString`かは本質的ではありませんが、できるだけ`String`を使った方が良いでしょう。)

「ほかのアドレスではデータの取得が出来ます」と言うご記載があったので、UTF-8かWindows-31Jかだけをチェックしていますが、本当ならレスポンス(あなたのコードで`res`)からエンコーディング情報を取得して、その情報も利用するようにした方が良いでしょう。(古い日本語のサイトにはEUC-
JPなんかを使用している場合もありますし。色々なケースを考えるとコードが複雑になるので今回は無視させてもらいました。例えばcharset=Shift_JISとか言っておいて、Windows-31Jの拡張文字を含む、なんて日本語サイトは山ほどあります。)

あなたのアプリに組み込むにはあれこれ修正しないといけないでしょうが、完了ハンドラーパターンだけは崩さないようにすることを強くお勧めしておきます。


CC BY-SA 3.0
2018-03-11T02:40:31.720
42314
2018-03-11T02:40:31.720
13972
42308
answer