【问题标题】:Making looped HTTP requests发出循环的 HTTP 请求
【发布时间】:2020-09-28 10:03:08
【问题描述】:

我正在编写一个连接在线翻译 API 的翻译应用程序。由于 API 的大小限制,我编写了我的程序,一次发送一个句子,然后将翻译连接在一起。我已经循环了

let serialQueue = DispatchQueue(label: "translationQueue")
        
        for line in lines {
          serialQueue.async {
            print("line is: " + line)
            
            var jpText = String(line)
            
            if jpText.isEmpty {
              jpText = "\n"
            }
            
            let escapedStr = jpText.addingPercentEncoding(withAllowedCharacters: (NSCharacterSet.urlQueryAllowed))
            let urlStr:String = ("https://api.mymemory.translated.net/get?q="+escapedStr!+"&langpair="+langStr!)
            let url = URL(string: urlStr)
            
            // Creating Http Request
            let request = NSURLRequest(url: url!)
            
            
            // If empty, don't feed to translator.
            if escapedStr!.isEmpty {
              //translatedLines.append("\n")
              self.enTextView.text = translatedLines
            }
            
            else {
              let configuration = URLSessionConfiguration.default
              configuration.waitsForConnectivity = true
              let defaultSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
              var dataTask: URLSessionDataTask?
              
              let group = DispatchGroup()
              group.enter()
              
              dataTask?.cancel()
              
              dataTask = defaultSession.dataTask(with: request as URLRequest) { [weak self] data, response, error in
                
                if let error = error {
                  // self?.errorMessage += "DataTask error: " + error.localizedDescription + "\n"
                  print("DataTask error: " + error.localizedDescription + "\n")
                  
                } else if
                  let data = data,
                  let response = response as? HTTPURLResponse,
                  response.statusCode == 200 {
                  let jsonDict: NSDictionary!=((try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)) as! NSDictionary)
                  
                  // if(jsonDict.value(forKey: "responseStatus") as! NSNumber == 200){
                  let responseData: NSDictionary = jsonDict.object(forKey: "responseData") as! NSDictionary
                  
                  group.leave()
                  
                  var translatedString = String()
                  translatedString = responseData.object(forKey: "translatedText") as! String
                  
                  let data = translatedString.data(using: .utf8)
                  
                  let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
                    .documentType: NSAttributedString.DocumentType.html,
                    .characterEncoding: String.Encoding.utf8.rawValue
                  ]
                  
                  guard let attributedString = try? NSAttributedString(data: data!, options: options, documentAttributes: .none) else {
                    return
                  }
                  
                  let decodedString = attributedString.string
                  print("decoded: " + decodedString)
                  
                  translatedLines.append(decodedString)
                  translatedLines.append("\n")
                  
                  
                  DispatchQueue.main.async {
                    self?.enTextView.text = translatedLines
                  }
                }
              }
              
              dataTask?.resume()
              group.wait()
            }
          }
        }

但是翻译输出的顺序是随机的。我大致了解正在发送并发请求。但是我可以在我的 for 循环中做些什么来确保整个发送/接收在移动到下一次迭代之前发生?

【问题讨论】:

  • 序列化异步。最简单的方法是调度组加等待加通知。我个人会使用 Combine Framework,但这更难。
  • 请在回复中详细说明。

标签: json swift http concurrency urlsession


【解决方案1】:

我会使用以下技术:

  1. 为翻译结果预分配一个数组。结果可能是成功(带有关联的翻译文本)或失败(带有关联的错误类型)。 Swift 的 Result 类型非常适合这个用例。
  2. 为每个数据任务分配一个索引(本质上是在第 1 步创建的数组中的一个索引)。
  3. 同时执行所有数据任务,使用DispatchGroup协调它们。
  4. 无论成功还是失败,每个任务都会将其结果写入数组。
  5. 当所有数据任务完成后,调度组将通知主队列并执行我们的完成块,我们可以在其中更新 UI。

这是一个基于您的快速操场代码,它使用我上面描述的技术:

let lines = ["one", "two", "three", "four"]

enum TranslationError: Error {
    case neverRequested
    case countNotBuildUrl
    case dataTaskError(localizedDescription: String)
    case responseDidNotContainData
    case parsingError
}

let dispatchGroup = DispatchGroup()
var translationResults = [Result<String, TranslationError>](repeating: .failure(.neverRequested), count: lines.count)
for i in 0..<lines.count {
    dispatchGroup.enter()
    let line = lines[i]
    var urlComponents = URLComponents(string: "https://api.mymemory.translated.net/get")!
    urlComponents.queryItems = [
        URLQueryItem(name: "q", value: line),
        URLQueryItem(name: "langpair", value: "en|fr")
    ]
    guard let url = urlComponents.url else {
        translationResults[i] = .failure(.countNotBuildUrl)
        dispatchGroup.leave()
        continue
    }

    let dataTask = URLSession.shared.dataTask(with: url) { data, _, error in
        print("Data task #\(i) finished.")
        defer {
            dispatchGroup.leave()
        }
        if let error = error {
            DispatchQueue.main.async {
                translationResults[i] = .failure(.dataTaskError(localizedDescription: error.localizedDescription))
            }
            return
        }
        guard let data = data else {
            DispatchQueue.main.async {
                translationResults[i] = .failure(.responseDidNotContainData)
            }
            return
        }
        guard let jsonDict = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary,
            let responseData = jsonDict.value(forKey: "responseData") as? NSDictionary,
            let translatedLine = responseData.value(forKey: "translatedText") as? String else
        {
            DispatchQueue.main.async {
                translationResults[i] = .failure(.parsingError)
            }
            return
        }
        DispatchQueue.main.async {
            translationResults[i] = .success(translatedLine)
        }
    }
    print("Starting data task #\(i)...")
    dataTask.resume()
}
dispatchGroup.notify(queue: .main, execute: {
    // Handle errors and update the UI
    print(translationResults)
})

一些注意事项:

  1. 重要的是所有数据任务完成块都更新同一队列上的translationResults 数组,这就是我每次调用DispatchQueue.main.async 的原因。不这样做可能会导致奇怪的崩溃,因为数组在 Swift 中不是线程安全的。
  2. 这种技术不允许在翻译进入时逐行更新 UI,但可以添加类似的内容。
  3. 在真正的应用程序中,我会重构此代码并将其拆分为几个职责更集中的方法。但是为了更容易理解这个想法,我决定不这样做。
  4. 我的代码中的错误处理也是非常基本的,只是为了展示想法并保持展示,它不包括我在实际应用中执行的一些其他检查。
  5. 我很确定有更好、更优雅的方法来实现相同的结果,所以我期待着看看是否有人提出了更好的实现。

【讨论】:

  • 感谢您的想法。我更新了我的代码并为 DispatchQueue 添加了几行代码。现在一切都按顺序进行,但是由于每个翻译都逐行进行,因此速度有点慢。有没有办法让它更快地完成每次迭代?我尝试将 .wait() 更改为 group.wait(timeout: .now() + 1) 1 秒,但随后一些翻译行再次混乱。
  • 是的,如果你有很多行文本,它会很慢,因为 DispatchGroup 直到最后一个数据任务完成后才会完成。如果您要处理数百或数千行,我建议将这些行分组。每个批次都有自己的调度组,并会在完成时更新 UI,这样用户将看到逐渐出现的翻译。您需要注意的另一件事是正确的错误处理。如果可能的话,我会使用 Combine 或其他一些响应式框架(RxSwift 或 ReactiveSwift)来完成这项任务,这是一个完美的用例。
猜你喜欢
  • 2016-08-24
  • 2014-02-14
  • 2013-04-10
  • 2020-02-18
  • 2018-03-23
  • 1970-01-01
  • 2020-05-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多