透過 WebClient.DownloadFile() 或 DownloadData() 下載檔案對 .NET 老鳥而言是雕蟲小技(參考:CODE-使用C#程式從網站下載檔案 ),但此種寫法檔名需自行指定。若下載對象非靜態檔案,伺服器端程式會透過 Content-Disposition Response Header 傳回檔名供客戶端參考,WebClient 是否能由 Response Header 自動取得檔名呢?

答案是可以! 程式範例如下:

static string DownloadFile(string url, string saveFolder)
{
    using (var wc = new WebClient())
    {
        using (var stream = wc.OpenRead(url))
        {
            //若伺服器未提供檔名,預設以下載時間產生檔名
            var fn = DateTime.Now.ToString("yyyyMMddHHmmss") + ".data";
            var cd = wc.ResponseHeaders["content-disposition"];
            if (!string.IsNullOrEmpty(cd))
            {
                Match m = Regex.Match(cd, "filename[*]=(?<es>[^;]+)");
                if (m.Success)
                {
                    fn = DecodeRF5987(m.Groups["es"].Value);
                }
                else
                {
                    m = Regex.Match(cd, "filename=[\"]*(?<f>[^\";]+)[\"]*");
                    if (m.Success)
                    {
                        fn = m.Groups["f"].Value;
                        //如伺服器會傳回UrlEncode()格式檔名,視需要加入
                        if (fn.Contains("%")) fn = Uri.UnescapeDataString(fn);
                    }
                }
            }
            using (var file = File.Create(Path.Combine(saveFolder, fn)))
            {
                stream.CopyTo(file);
            }
            return fn;
        }
    }
}
//REF: https://github.com/grumpydev/RFC5987-Decoder/blob/master/RFC5987/RFC5987.cs
private static IEnumerable<byte> GetDecodedBytes(string encData)
{
    var encChars = encData.ToCharArray();
    for (int i = 0; i < encChars.Length; i++)
    {
        if (encChars[i] == '%')
        {
            var hexString = new string(encChars, i + 1, 2);

            i += 2;

            int characterValue;
            if (int.TryParse(hexString, NumberStyles.HexNumber,
                CultureInfo.InvariantCulture, out characterValue))
            {
                yield return (byte)characterValue;
            }
        }
        else
        {
            yield return (byte)encChars[i];
        }
    }
}
static string DecodeRF5987(string encStr)
{
    Match m = Regex.Match(encStr, "^(?<e>.+)'(?<l>.*)'(?<d>[^;]+)$");
    if (m.Success)
    {
        //TODO: 此處未包含伺服器傳回資料有誤之容錯處理
        var enc = Encoding.GetEncoding(m.Groups["e"].Value);
        return enc.GetString(GetDecodedBytes(m.Groups["d"].Value).ToArray());
    }
    return encStr;
}

2018-09-17 更新:Content-Disposition Header 可能包含多值(且會帶有雙引號),原本 Regex.Split() 寫法錯抓機率甚高,例如:Content-Disposition: attachement; filename="…"; name="fieldName"; ,更進一步,伺服器端還可能依據 RFC5987,以 filename*=UTF-8''%c2%a3%20and%20rates.pdf 編碼規則提供 Unicode 檔名,故調整以 Regex.Match 取 filename* 或 filename,並加入 RFC5987 解碼。(註: 程式碼未經廣泛驗證,大家如發現有誤請不吝指正)
感謝網友 Slash 提醒。

Demostrating how to use WebClient to download file and get filename from response header.


Comments

# by Slash

雖然與你分享這段小程式的目的不同,但還是要提醒Regex.Split這邊會有RFC5987的問題。 或者應該是說,Content-Disposition可鬆散、多值的表示,故簡單比對套用在無法受控的對方伺服器,儲存到非預期檔案名稱的機率很高。

# by Jeffrey

to Slash, 感謝提醒,程式已修改強化。

Post a comment