我打算使用http-conduit
通过HTTP/HTTPS获取大量数据。为了有效地做到这一点,我想使用Accept-Encoding: deflate,gzip
标头来允许服务器(如果支持的话)以压缩的方式传输数据。
然而,我想从中获取的一些服务器似乎错误地使用Content-Encoding: gzip
标头进行响应,而没有返回gzip数据。
因此,我需要处理以下情况:
- 服务器不支持压缩-->返回纯响应正文
- 服务器返回gzipped/deflated内容-->返回解压缩的响应体
- 服务器表示(在响应标头中返回gzip内容,但gzip解码失败-->返回纯响应正文
在第三种情况下,可以(在这个特定的情况下)安全地假设,明文、未压缩的数据看起来不像gzip数据,所以响应标头说它是gzipped&;un-zip失败完全等同于数据未压缩。
如何使用http-conduit
执行此操作?
注意:这个问题故意不显示研究成果,因为它已经在问答中立即得到了回答;A型方式。
为了使这个答案更加简洁,我们将使用代码&我以前的一些帖子中的概念:
simpleHttpWithManager
- 从这里进行容错gzip/delate解码
为了避免重复,我将首先解释基本步骤,然后提供一个完整的示例。
首先,我们将处理发送标头的问题。注意,如果http-types
包含hContentEncoding
,则hAcceptEncoding
不是预定义的。除此之外,这是一项琐碎的任务。
发送请求后,我们需要检查是否有Content-Encoding
。如果没有,我们将假设为未压缩的明文,否则我们需要检查它是gzip
还是deflate
。在这种情况下,它到底是哪一个并不重要,因为[zlib
]支持通过标头进行自动检测。
对于这个相当简单的示例,我们只是假设,如果服务器返回既不存在也不存在gzip
或deflate
的Content-Encoding
值,则响应不会被压缩。由于我们不允许(通过Accept-Encoding
)像sdch
这样的其他压缩,服务器这样做将违反HTTP标准。
如果我们检测到压缩编码,我们会尝试解压缩并返回它。如果失败或数据根本没有压缩,我们会返回纯响应体。
下面是一个例子:
{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Conduit
import Network.Connection
import Codec.Compression.Zlib.Internal
import Data.Maybe
import Data.Either
import Network.HTTP.Types
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB
myurl :: String
myurl = "http://stackoverflow.com"
hAcceptEncoding :: HeaderName
hAcceptEncoding = "Accept-Encoding"
-- | The Accept-Encoding HTTP header value for allowing gzip or deflated responses
gzipDeflateEncoding :: ByteString
gzipDeflateEncoding = "gzip,deflate"
-- HTTP header list that allows gzipped/deflated response
compressionEnabledHeaders :: RequestHeaders
compressionEnabledHeaders = [(hAcceptEncoding, gzipDeflateEncoding)]
-- | Give an encoding string and a HTTP response object,
-- Checks if the Content-Encoding header value of the response object
-- is equal to the given encoding. Returns false if no ContentEncoding
-- header exists in the given response, or if the value does not match
-- the encoding parameter.
hasResponseEncoding :: ByteString -> Response b -> Bool
hasResponseEncoding encoding response =
let responseEncoding = lookup hContentEncoding headers
headers = responseHeaders response
in maybe False (== encoding) responseEncoding
-- | Convert the custom error format from zlib to a Either
decompressStreamToEither :: DecompressStream -> Either String LB.ByteString
decompressStreamToEither (StreamError _ errmsg) = Left errmsg
decompressStreamToEither stream@(StreamChunk _ _) = Right $ fromDecompressStream stream
decompressStreamToEither StreamEnd = Right $ ""
-- | Decompress with explicit error handling
safeDecompress :: LB.ByteString -> Either String LB.ByteString
safeDecompress bstr = decompressStreamToEither $ decompressWithErrors gzipOrZlibFormat defaultDecompressParams bstr
-- | Decompress gzip, if it fails, return uncompressed String
decompressIfPossible :: LB.ByteString -> LB.ByteString
decompressIfPossible bstr =
let conv (Left a) = bstr
conv (Right a) = a
in (conv . safeDecompress) bstr
-- | Tolerantly decompress response body. As some HTTP servers set the header incorrectly,
-- just return the plain response text if the compression fails
decompressResponseBody :: Response LB.ByteString -> LB.ByteString
decompressResponseBody res
| hasResponseEncoding "gzip" res = decompressIfPossible $ responseBody res
| hasResponseEncoding "deflate" res = decompressIfPossible $ responseBody res
| otherwise = responseBody res
-- | Download like with simpleHttp, but using an existing manager for the task
-- and automatically requesting & handling gzipped data
simpleHttpWithAutoGzip :: Manager -> String -> IO LB.ByteString
simpleHttpWithAutoGzip manager url = do req <- parseUrl url
let req' = req {requestHeaders = compressionEnabledHeaders}
fmap decompressResponseBody $ httpLbs req' manager
-- Example usage
main :: IO ()
main = do manager <- newManager conduitManagerSettings -- Create a simple manager
content <- simpleHttpWithAutoGzip manager "http://stackoverflow.com"
-- Print the uncompressed content
print $ content