为什么 Haskell 无法处理来自特定网站的字符?

Why can Haskell not handle characters from a specific website?

我在想能不能写一个Haskell程序来点播一些小说的更新,我举的网站是this。我在显示它的内容时遇到了问题(在 mac el capitan 上)。简单代码如下:

import Network.HTTP

openURL :: String -> IO String
openURL = (>>= getResponseBody) . simpleHTTP . getRequest

display :: String -> IO ()
display = (>>= putStrLn) . openURL

然后我在ghci上运行display "http://www.piaotian.net/html/7/7430/"的时候出现了一些奇怪的字符;第一行看起来像这样:

<title>×ß½øÐÞÏÉ×îÐÂÕ½Ú,×ß½øÐÞÏÉÎÞµ¯´°È«ÎÄÔĶÁ_Æ®ÌìÎÄѧ</title>
<meta http-equiv="Content-Type" content="text/html; charset=gbk" />
<meta name="keywords" content="×ß½øÐÞÏÉ,×ß½øÐÞÏÉ×îÐÂÕ½Ú,×ß½øÐÞÏÉÎÞµ¯´° Æ®ÌìÎÄѧ" />
<meta name="description" content="Æ®ÌìÎÄѧÍøÌṩ×ß½øÐÞÏÉ×îÐÂÕ½ÚÃâ·ÑÔĶÁ£¬Ç뽫×ß½øÐÞÏÉÕ½ÚĿ¼¼ÓÈëÊղط½±ãÏ´ÎÔĶÁ,Æ®ÌìÎÄѧС˵ÔĶÁÍø¾¡Á¦ÔÚµÚһʱ¼ä¸üÐÂС˵×ß½øÐÞÏÉ£¬Èç·¢ÏÖδ¼°Ê±¸üУ¬ÇëÁªÏµÎÒÃÇ¡£" />
<meta name="copyright" content="×ß½øÐÞÏÉ°æȨÊôÓÚ×÷ÕßÎáµÀ³¤²»¹Â" />
<meta name="author" content="ÎáµÀ³¤²»¹Â" />
<link rel="stylesheet" href="/scripts/read/list.css" type="text/css" media="all" />
<script type="text/javascript">

我也试过下载成文件如下:

import Network.HTTP

openURL :: String -> IO String
openURL = (>>= getResponseBody) . simpleHTTP . getRequest

downloading :: String -> IO ()
downloading = (>>= writeFile fileName) . openURL

但是下载文件后,就像照片中一样:

如果我通过 python 下载页面(例如使用 urllib),字符显示正常。还有,如果我写一个中文html然后解析,那好像就没有问题了。因此,问题似乎出在网站上。但是,我看不出网站上的字符和我写的有什么区别。

非常感谢任何有关这背后原因的帮助。

P.S.
python代码如下:

import urllib

urllib.urlretrieve('http://www.piaotian.net/html/7/7430/', theFic)

theFic = file_path

文件一切正常。

我很确定,如果您将 Network.HTTPString 类型一起使用,它会使用您的系统编码将字节转换为字符,这通常是错误的。

这只是我不喜欢的几个原因之一 Network.HTTP

您的选择:

  1. 使用Bytestring界面。由于某种原因,它更尴尬。它还需要您手动将字节解码为字符。大多数站点都会在响应 headers 中为您提供编码,但有时它们会撒谎。真是一团糟。

  2. 使用不同的 http 抓取库。我不认为任何消除处理说谎编码的混乱,但它们至少不会让不正确使用系统编码变得更加尴尬。我会调查 wreq or http-client

此程序产生与 curl 命令相同的输出:

curl "http://www.piaotian.net/html/7/7430/"

测试:

stack program > out.html
open out.html

(如果不使用 stack,只需安装 wreqlens 软件包并使用 runhaskell 执行。)

#!/usr/bin/env stack
-- stack --resolver lts-6.0 --install-ghc runghc --package wreq --package lens --package bytestring
{-# LANGUAGE OverloadedStrings #-}

import Network.Wreq
import qualified Data.ByteString.Lazy.Char8 as LBS
import Control.Lens

main = do
  r <- get "http://www.piaotian.net/html/7/7430/"
  LBS.putStr (r ^. responseBody)

如果您只想下载文件,例如稍后再看,你只需要使用 ByteString 接口。最好为此使用 http-client(如果您有一些镜头知识,则使用 wreq)。然后你可以在浏览器中打开它,它将看到它是一个 gbk 文件。到目前为止,您只是将原始字节作为惰性字节串传输。如果我理解它,那就是 python 所做的一切。编码在这里不是问题;浏览器正在处理它们。

但是如果你想查看 ghci 中的字符,例如,主要问题是默认情况下没有任何东西可以像浏览器那样处理 gbk 编码。为此,您需要 text-icu 之类的东西和底层 C 库。下面的程序使用 http-client 库和 text-icu - 我认为这些是解决这个问题的标准,尽管你可以使用功能较弱的 encoding 库来解决尽可能多的问题到目前为止我们已经看到了。它似乎工作正常:

import Network.HTTP.Client                     -- http-client
import Network.HTTP.Types.Status (statusCode)
import qualified Data.Text.Encoding as T       -- text
import qualified Data.Text.IO as T
import qualified Data.Text as T
import qualified Data.Text.ICU.Convert as ICU  -- text-icu
import qualified Data.Text.ICU as ICU
import qualified Data.ByteString.Lazy as BL

main :: IO ()
main = do
  manager <- newManager defaultManagerSettings
  request <- parseRequest "http://www.piaotian.net/html/7/7430/"
  response <- httpLbs request manager
  gbk <- ICU.open "gbk" Nothing
  let txt :: T.Text
      txt = ICU.toUnicode gbk $ BL.toStrict $ responseBody response
  T.putStrLn txt

这里 txt 是一个 Text 值,即基本上就是 'code points'。最后一位 T.putStrLn txt 将使用系统编码将文本呈现给您。您还可以使用 Data.Text.Encoding 中的函数或 text-icu 中更复杂的 material 中的函数显式处理编码。例如,如果您想以 utf8 编码保存文本,您可以使用 T.encodeUtf8

所以在我的 ghci 中输出看起来像这样

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" " http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>走进修仙最新章节,走进修仙无弹窗全文阅读_飘天文学</title>
<meta http-equiv="Content-Type" content="text/html; charset=gbk" />

...

这样看起来对吗?在我的 ghci 中,我看到的是通过 utf8 传输的,因为这是我的系统编码,但请注意,该文件当然是说它是一个 gbk 文件。那么,如果您想进行一些 Text 转换,然后将其保存为 html 文件——那么您当然需要确保文件中提到的字符集与您使用的编码相匹配将您写入文件的字节串。

你当然也可以通过将最后三行替换为

来将其变成 Haskell String 的形状
let str :: String
    str = T.unpack $ ICU.toUnicode gbk $ BL.toStrict $ responseBody response
putStrLn str

这是使用 encoding 包的更新答案 将 GBK 编码的内容转换为 Unicode。

#!/usr/bin/env stack
{- stack
  --resolver lts-6.0 --install-ghc runghc
  --package wreq --package lens --package encoding --package binary
-}

{-# LANGUAGE OverloadedStrings #-}

import Network.Wreq
import qualified Data.ByteString.Lazy.Char8 as LBS
import Control.Lens
import qualified Data.Encoding as E
import qualified Data.Encoding.GB18030 as E
import Data.Binary.Get

main = do
  r <- get "http://www.piaotian.net/html/7/7430/"
  let body = r ^. responseBody :: LBS.ByteString
      foo = runGet (E.decode E.GB18030) body 
  putStrLn foo

既然你说只对链接感兴趣,那么就没有必要把GBK编码转成Unicode了。

这是一个打印出文档中所有链接如“123456.html”的版本:

#!/usr/bin/env stack
{- stack
  --resolver lts-6.0 --install-ghc runghc
  --package wreq --package lens
  --package tagsoup
-}

{-# LANGUAGE OverloadedStrings #-}

import Network.Wreq
import qualified Data.ByteString.Lazy.Char8 as LBS
import Control.Lens
import Text.HTML.TagSoup
import Data.Char
import Control.Monad

-- match \d+\.html
isNumberHtml lbs = (LBS.dropWhile isDigit lbs) == ".html"

wanted t = isTagOpenName "a" t && isNumberHtml (fromAttrib "href" t)

main = do
  r <- get "http://www.piaotian.net/html/7/7430/"
  let body = r ^. responseBody :: LBS.ByteString
      tags = parseTags body
      links = filter wanted tags
      hrefs = map (fromAttrib "href") links
  forM_ hrefs LBS.putStrLn