読者です 読者をやめる 読者になる 読者になる

SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

HttpWebRequestで取得したデータをcharsetに応じてデコードする (C#)(F#)


先にも書いた通り、HttpWebRequestで取得したデータをstringに変換する際、HttpWebResponsのCharacterSetプロパティで示されるエンコード形式は正確でない事があります。

下記のサンプルでは、HttpWebRequestで取得したデータをbyteで受け、一旦ASCIIでstringに変換しヘッダの中にcharsetの指定が有るかどうかを検索しています。
検索の結果、エンコードの形式が判明した場合は、byteから改めて適切なエンコードでstringへ変換
します。

C#サンプル

using System;
using System.Net;
using System.IO;
using System.Text;

namespace Examlpe {
    class Program {
        static void Main(string[] args) {

            //HTTPリクエスト
            var req = HttpWebRequest.Create("http://www.sapporoworks.ne.jp/spw/");
            req.Timeout = 15000;//タイムアウト(15秒)
            //HTPレスポンス
            var res = (HttpWebResponse)req.GetResponse();

            //byte [] で読み込む
            var buf = ReadBytes(res.GetResponseStream());
            //デフォルトをASCIIとする
            var encoding = Encoding.ASCII;
            
            //ASCIIでstringに変換する
            string str =encoding.GetString(buf);

            //1行ごとMETAヘッダにcharesetの指定がないかを検索する
            foreach (var s in str.Split('n')) {
                var l = s.ToUpper();
                if (l.IndexOf("<META") != -1 && l.IndexOf("CHARSET")!=-1) {
                    //指定があった場合は、エンコードの種類を取得する
                    var charset = GetCharset(l);
                    encoding = Encoding.GetEncoding(charset);
                    //取得したエンコードでstringへの変換をやり直す
                    str = encoding.GetString(buf);
                    break;
                }
            }
            Console.WriteLine(str);

            Console.WriteLine();
            Console.WriteLine("何かのキーを押してください。");
            Console.ReadKey();
        }
        
        //charset=の後ろを取得する
        static string GetCharset(string str) {
            //"="の検索開始
            int start = str.IndexOf("CHARSET");
            if (start == -1)
                return "";//失敗
           
            for(int i=start;;i++){
                if(str[i]=='='){
                    start = i+1;
                    break;
                }
            }
            int end = 0;
            for (int i = start + 1; ; i++) {
                if(str[i]=='"' || str[i]=='>' || str[i]==' ' || str[i]==';'){
                    end=i;
                    break;
                }
            }
            if (end == 0)
                return "";//失敗
            return str.Substring(start,end-start);
        }
        
        //Streamからbyte[]で読み込む
        static byte[] ReadBytes(Stream stream) {
            var buf = new byte[0];
            var tmp = new byte[1024];
            while (true) {
                var len = stream.Read(tmp, 0, 1024);
                if (len <= 0)
                    break;
                buf = AppendBuffer(buf, tmp , len);
            }
            return buf;
        }
        //byte[]への追加
        static byte[] AppendBuffer(byte[] buf, byte[] tmp ,int len) {
            var res = new byte[buf.Length + len];
            Buffer.BlockCopy(buf, 0, res, 0, buf.Length);
            Buffer.BlockCopy(tmp, 0, res, buf.Length, len);
            return res;
        }
    }
}

F#サンプル
もっと、シンプルに書けないものだろうか・・・・

open System
open System.Net
open System.IO
open System.Text
open System.Linq

////charset=の後ろのエンコードの種類を取得する
let GetCharset (str:string) = 
    //charsetの後の最初の'='の位置をスタート位置(s)として取得
    let s = str.IndexOf('=',str.IndexOf("CHARSET")) + 1
    //スタート位置以降で、最初に'"'、'>'、' '若しくは';'が現れた位置を終了位置(e)として取得する
    let e = 
        [s..str.Length-1]
        |>Seq.tryFind(fun n -> str.[n] = '"' || str.[n] = '>'|| str.[n] = ' '|| str.[n] = ';')
    if e <> None then 
        str.Substring(s,e.Value-s) //スタート位置〜終了位置の部分文字列を取得
    else
        "" //失敗

//Streamからbyte[]で読み込む
let rec ReadBytes (stream:Stream,buf) = 
    let tmp = Array.create 1024 0uy
    match stream.Read(tmp, 0, 1024) with
    |n when n>0 -> ReadBytes ( stream , Array.append buf tmp.[0..n-1] )
    |_ -> buf
   

//byte[] をstringに変換する
let rec GetString (encoding:Encoding,buf:byte[]) = 
    let str = encoding.GetString(buf)
    //ASCIIでデコードした場合は、METAヘッダを確認する
    if encoding = Encoding.ASCII then
        let s = //<charsetのある行を取得する
            str.Split('n') 
            |> Seq.map(fun s->s.ToUpper())
            |> Seq.tryFind(fun s->s.IndexOf("<META") <> -1 && s.IndexOf("CHARSET") <> -1)
        match s with
        |None -> str
        |_ ->
            match GetCharset(s.Value) with //エンコードの種類を取得
            |charset when charset <> "" -> GetString(Encoding.GetEncoding(charset),buf)
            |_ -> str
    else
        str

//HTTPリクエスト
let req = HttpWebRequest.Create("http://www.sapporoworks.ne.jp/spw/")
req.Timeout <- 15000//タイムアウト(15秒)
//HTPレスポンス
let res = req.GetResponse():?>HttpWebResponse
//Streamからbyte[]で読み込む
let buf = ReadBytes(res.GetResponseStream(),[||])
//byte[] をstringに変換する
let str = GetString(Encoding.ASCII,buf)

printfn "%s" str
printfn ""
printfn "何かのキーを押してください。"
Console.ReadKey() |> ignore

2011.11.24追記
mclh46氏による記事
http://d.hatena.ne.jp/mclh46/20111123/1322015736