2012年7月25日 星期三

利用StreamWriter 資料流I/O 寫成檔案

資料庫是web程式設計上的重點之一,但有時候一些小資料,或是不需統計、不需長期存放的資料,也許就不需要用到資料庫,底下這個範例,是一個把報表寫成文字檔的範例,和我之前寫的將GridView裡資料匯出成Excel檔可以相互比較,這篇是把文字寫成txt檔案。

image

程式的demo頁(AspNet49.aspx)我就不放上了,畢竟streamwriter都是會動用到server的資源的
dnowba 的免費空間可能經不起大家的考驗
僅提供程式碼

因為比較基礎,所以也就乾脆把從無到有的歷程寫下來,我想這也是一般程式設計的歷程,整個精神就是「先求有再求好」,最後達到使用者挑不出骨頭的最高境界…

廢話不多說,我設計分別用四個Button來做歷程分界…每一歷程就多講一些。

Me.Button1.Text = "將文字寫入txt檔案"
Me.Button2.Text = "將文字寫入doc檔案,寫入前檢查檔名是否存在"
Me.Button3.Text = "依日期將文字寫入個別的檔案"
Me.Button4.Text = "依日期將文字寫入個別的檔案,並跳出下載視窗"

就請大家慢慢看吧…

    Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        Me.Button1.Text = "將文字寫入txt檔案"
        Me.Button2.Text = "將文字寫入doc檔案,寫入前檢查檔名是否存在"
        Me.Button3.Text = "依日期將文字寫入個別的檔案"
        Me.Button4.Text = "依日期將文字寫入個別的檔案,並跳出下載視窗"
    End Sub

    Protected Sub Button1_Click(sender As Object, e As System.EventArgs) Handles Button1.Click
        ' 這個範例在示範怎麼將文字寫入TXT檔案,先用hard-coded的方式來簡化程式碼
        ' 最基本的StreamWriter方法(path as sting) 中的path參數,需要提供「實體路徑」
        ' 而path要寫上檔案名稱,也建議寫上副檔名(實際上不寫系統也有辦法辨識他是text)

        Dim sw As New System.IO.StreamWriter("C:\Users\Administrator\Documents\Visual Studio 2010\WebSites\Dnowba\AspNet49.txt")
        sw.WriteLine("這個demo測試將文字寫入現有的txt檔案")
        sw.WriteLine("寫入的方式除了.write還有.writeline")
        sw.WriteLine("和console.write/writeline,stringbuilder.write/writeline一樣")
        sw.WriteLine("writeline就是自動換行的意思啦!!!")
        sw.Write("===寫入日期:")
        sw.WriteLine(DateTime.Now & "===")
        sw.WriteLine("*****************************************")
        ' 不關閉資料流的話,會一直佔用,出現「由於另一個處理序正在使用檔案,所以無法存取該檔案」的錯誤訊息
        ' 養成close和dispose的習慣,不管是連結資料庫或是存取檔案,都一定會用上應用程式佔掉資源
        sw.Close()
        sw.Dispose()
        Me.Label1.Text = "<script>window.alert('已寫入檔案')</script>"

        ' 副檔名的部分,如果副檔名要用doc之類的也是沒差。(前提是你要有相應的應用程式來讀取檔案)
        ' 像這邊設定doc,也會因為編碼問題發生錯誤
        ' 所以最好的方法就是寫好編碼

        ' 實體路徑的部分,在本機上測試階段,你要把檔案放在哪個目錄底下都沒問題
        ' 但若是已發佈的網站,在另一個browser端下,就會因為「安全性」發生問題
        ' 一來是路徑設定方式,browser可不能檢視server站台以外的目錄
        ' 二來是會阻止一般user在server端建立檔案, 所以還要設定好使用者權限,不然會出現「拒絕存取路徑 」的錯誤
        ' 所以最好的方法就是寫在自已這個應用程式的目錄底下

        ' 上面的範例,每觸發一次事件,指定的檔案就會被初始化 (原來的內容會被覆蓋)
        ' 如果希望每次執行時是寫入既有檔案中,並附加在原有文字後,請參考下一個方法
    End Sub

    Protected Sub Button2_Click(sender As Object, e As System.EventArgs) Handles Button2.Click
        ' 這個範例示範寫入既有檔案中,並附加在原有文字後
        ' 同時解決寫成doc檔時編碼的問題
        ' 同時解決寫入檔案安全性的問題

        ' 路徑用Server.MapPath方法,把WEB伺服器上虛擬路徑轉成實體路徑,並傳回結果
        ' 可以用 HttpContext.Current.Response.Write(Server.MapPath(""))來檢查一下是否正確
        ' 另外用StreamWriter的其他參數來達成寫入檔案並附加在原有文字的目標
        ' 其中編碼的參數用了System.Text.Encoding.GetEncoding方法,把big5的字碼頁(字符編碼的識別號)傳回,如果你知道big5的字符頁是950,直接寫950也行)
        'Dim sw As New System.IO.StreamWriter(Server.MapPath("AspNet49.doc"), True, System.Text.Encoding.GetEncoding("Big5"))
        Dim sw As New System.IO.StreamWriter(Server.MapPath("AspNet49.doc"), True, System.Text.Encoding.GetEncoding(950))
        sw.WriteLine("這個demo測試將文字寫入doc檔案,寫入前檢查檔名是否存在")
        sw.WriteLine("如果檔案不存在,就生成新檔案;如果存在就附加文字在原檔案下")
        sw.WriteLine("同時解決寫成doc檔時編碼的問題")
        sw.WriteLine("同時解決寫入檔案安全性的問題")
        sw.Write("===寫入日期:")
        sw.WriteLine(DateTime.Now & "===")
        sw.WriteLine("*****************************************")
        sw.Close()
        sw.Dispose()

        ' 這個範例在寫入時修改文字編碼,但並不能解決問題(目前無解)。
        ' 如果每次生成的內容很大的話,也許可以不要用附加文字在原檔案的方法
        ' 如果希望每次的檔案寫入都能被保留,可以用小技巧,在檔名前加上日期時間達到類似UID的效果
        ' 如果還覺得不保險,希望每次寫入檔案前可以先檢查一下,請參考下一個範例。
    End Sub

    Protected Sub Button3_Click(sender As Object, e As System.EventArgs) Handles Button3.Click
        ' 這個範例示範依日期將文字寫入個別的檔案
        ' 檔案名稱加上日期做為識別
        ' 如果是同一天的檔案,就寫在同一個txt裡
        ' 如果不是同一天的檔案,就寫到另一個txt裡
        ' 並先檢查檔案是否存在

        ' 有關日期的字串格式,這邊本來想用 DateTime.Today.ToShortDateString()或是FormatDateTime(Now, DateFormat.ShortDate)之類的方式
        ' 後來因為時間日期有culture文化特性的關係,使用上會很複雜,就不用了
        ' 有關文化特性的問題,例如底下我故意設定日期的格式是用"-"來做分隔符號,但結果還是輸出時還是用"/"
        Dim filewithdate As String = "AspNet49_" & Now.Year & "_" & Now.Month & "_" & Now.Day & ".txt"

        If System.IO.File.Exists(Server.MapPath(filewithdate)) Then
            Response.Write("今天已經產生過報表,會將結果寫附加在原檔案" & filewithdate & " 下")
            Dim sw As New System.IO.StreamWriter(Server.MapPath(filewithdate), True)
            sw.WriteLine("這個範例示範依日期將文字寫入個別的檔案")
            sw.WriteLine("因為是同一天的檔案,就寫在同一個txt裡")
            sw.WriteLine("===寫入日期:" & String.Format("{0:MM-dd-yyyy}", DateTime.Now) & "===")
            sw.WriteLine("*****************************************")
            sw.Close()
            sw.Dispose()
            ' 下面用了一個「Return」,因為條件式為true,
            ' 所以執行到這段就會「返回」,程序不會往下走,也就不會執行下面的動作了
            Return
        End If

        ' 是不是常常忘了寫sw.Close()、sw.Dispose()來關閉資料流啊,Framework 2.0 提供了一個方法:
        ' 使用Using來宣告變數,那麼在程式執行到End Using的時候就會自動的把該變數做資源回收
        ' 另外這裡也用另一種方式來寫入檔案,不用new了,因為等號=後面已經提供了參考了
        Using sw As System.IO.StreamWriter = System.IO.File.CreateText(Server.MapPath(filewithdate))
            sw.WriteLine("這個範例示範依日期將文字寫入個別的檔案")
            sw.WriteLine("不是同一天的檔案,就寫到另一個txt裡")
            sw.WriteLine("===寫入日期:" & String.Format("{0:yyyy-M-d}", DateTime.Today) & "===")
            sw.WriteLine("*****************************************")
        End Using
        ' streamwriter 的動作,都是針對server端的檔案,無法把檔案「直接」寫到client端
        ' 像這類的檔案操作,在webform設計上,都比較偏向於產生報表之類的工作
        ' 不過使用者的需求是永遠也無法滿足的…
        ' 如果使用者是在遠端的話,那麼可能希望除了server端有檔案以外
        ' 還可以在在遠端下載到這個檔案
        ' 那麼我們就要學習一下 之前寫的範例:將GridView裡資料匯出成Excel檔
        ' http://dnowba.blogspot.tw/2012/07/gridviewexcel.html

    End Sub

    Protected Sub Button4_Click(sender As Object, e As System.EventArgs) Handles Button4.Click
        Dim filewithdate As String = "AspNet49_" & Now.Year & "_" & Now.Month & "_" & Now.Day & ".txt"

        ' 這裡的判別式只有在提示文字,因為StreamWriter本身的參數就帶有判別,請細細品味
        If System.IO.File.Exists(Server.MapPath(filewithdate)) Then
            Me.Label1.Text = "今天已經產生過報表,會將結果寫附加在原檔案" & filewithdate & " 下"
        End If
        Using sw As New System.IO.StreamWriter(Server.MapPath(filewithdate), True)
            sw.WriteLine("這個範例示範依日期將文字寫入個別的檔案,並跳出下載視窗")
            sw.WriteLine("如果是同一天的檔案,就寫在同一個txt裡,如果不是就寫在不同的txt")
            sw.WriteLine("===寫入日期:" & String.Format("{0:yyyy_M_d}", DateTime.Now) & "===")
            sw.WriteLine("*****************************************")
        End Using

        '底下是利用範例得來的精華
        'Response.Redirect("~/" & filewithdate)
        Me.Label1.Text = Server.MapPath(filewithdate)
        DownloadFile(Page, filewithdate, Server.MapPath(filewithdate))

        ' 不過這樣子設計,除了今天的報表外,舊的報表就看不到了
        ' 所以再完整一點的作法,就是把所有的報表檔案給顯示在頁面上供查詢
    End Sub
    Shared Sub DownloadFile(ByVal WebForm As System.Web.UI.Page, ByVal FileNameWhenUserDownload As String, ByVal FilePath As String)
        WebForm.Response.ClearHeaders()
        WebForm.Response.Clear()
        WebForm.Response.Expires = 0
        WebForm.Response.Buffer = True
        WebForm.Response.AddHeader("Accept-Language", "zh-tw")
        WebForm.Response.AddHeader("content-disposition", "attachment; filename=" & Chr(34) & System.Web.HttpUtility.UrlEncode(FileNameWhenUserDownload, System.Text.Encoding.UTF8) & Chr(34))
        WebForm.Response.ContentType = "Application/octet-stream"
        WebForm.Response.BinaryWrite(System.IO.File.ReadAllBytes(FilePath))
        WebForm.Response.End()
    End Sub


程式補充說明

行13中,我們寫的實體路徑是在本機上的,不是Browser端的

streamwriter所有的動作都是用到web server端的資源,寫都是寫到本機上,所以這種寫法比較適合內部作業 (老闆要求要看某報表,或是自已要紀錄某程式設計的偵錯歷程) 。若是提供給一般使用者的話,寫這樣的路徑就會出現「Could not find a part of the path」或是「拒絕存取路徑」 的錯誤(如下二張圖)。
image

image

寫這樣的路徑不是不行,但你要先克服三個問題:
一、一般發行web站台我們都用虛擬目錄來隱藏web站台在server上的實際位置。如果你要用這種路徑寫法,你的路徑又會曝露在使用者眼下,那麼可能有安全疑慮。
二、使用者要在你的server生檔案,該目錄是要提供給「網路使用者」寫的權限。這個安全危機又更大了。
三、前面二個危機夠大了。第三個問題就是如果你的web站台不是自已架設的,而是用第三方提供的空間…你能查得到自已確切的目錄嗎?

行23、24中,寫入sw.Close()、sw.Dispose()目的是要把資源關閉,避免佔用

如果不寫的話,就會發生「由於另一個處理序正在使用檔案」的錯誤(如下圖)
一個處理序就是一個處理序,sever上不會分辨是不是由不同的Browser端的請求,所以就會出現資源佔用的情形。image

一個執行緒的資源佔用的話,不只寫沒辦法寫,若是文字檔的話連讀都會出現「無法開啟檔案,因為其他的處理序正在使用它」的錯誤(如下圖)。
image

額外一提,如果不寫sw.Close()、sw.Dispose()至於會不會一段時間自動釋放,這個我記得以前有看過相關的內容,答案是肯定的,畢竟現在是多執行緒的pc時代,「當資源不夠用的時候,會釋放拖很久沒使用的執行緒」所以通常到那個時候,你的server也被「佔著茅坑不拉屎」的資源拖垮的差不多了。

行51中,用streamwriter方法把資料流寫入成doc檔案的方法會有問題

如下圖,在第一次使用streamwriter方法把資料流塞到doc裡的時候,開啟會出現「檔案轉換」的視窗,要我們選擇要使用的編碼方式,下圖可以看到目前的選項是Unicode(UTF-8),目前ASP.NET的webform預設都是以Unicode(UTF-8)來作字符集,所以用streamwriter方法導出來的資料流自然編碼也是UTF-8。
所以一開始我是想會跳出檔案轉換的視窗的原因是不是因為我們不是用系統或軟體的預設編碼方式?如果用預設值的話應該不會出錯吧。
image

但是「windows的預設值是big5,word的預設編碼是utf-8…」這個應該沒有錯,感覺就算改了編碼也沒有用,但抱著死馬當活馬醫的精神,還是試了下去
所以我就改成如程式碼行51,用big5方式編碼,結果如預料,一樣跳出視窗
image

至於為什麼,從ANSI和Unicode、UTF-8和UTF-16、BOM這個BOM的經驗、還有將GridView裡資料匯出成Excel檔也出現原生格式的問題,二個文章相互參考之下
我相信word的原生格式裡,在表頭上應該也會放上一串識別編碼的字串,而我們輸出的資料流並沒有,所以不管你怎麼編碼,編成ANSI、Unicode…等等都沒有用,編碼對了,問題是在開啟時沒有明確的提供識別(是不是這樣我沒有研究,純推論)。

所以資料流寫成doc一定會有這個問題,那麼有沒有「使用word開啟純文字檔,不要出現檔案轉換視窗?」的解決方法呢…
我在將GridView裡資料匯出成Excel檔也有提到「關於這類的錯誤訊息」,都可以透過改機碼(regedit)的方式來處理。請參考當您開啟文字檔案時檔案編碼方式轉換] 對話方塊便會出現

但是你要怎麼要求使用者改機碼,而且照這樣的方式改完很麻煩,變成word強制會以某種編碼方式開啟檔案,萬一編碼不同的文件,開起來就是一堆?????的問號。這個方式並不好…追根究柢就是根本不需要用word開的東西。額外一提,我曾經看過一個網頁設計公司寫給客戶的Q&A…很有趣

客戶Q:每次開報表都會出現「文字轉換」視窗
公司A:請按「確定」後即可開啟
DNOWBA:廢話

不過使用者的需求有時候就是很「直覺」,單純的文字不用TXT就硬要用WORD開,對一些技術夠的人來說這當然這個是小菜一碟,但是我們這種還在跌跌撞撞中成長的人來說,還是要委婉點說無法克服,除非你不想作生意就寫上面這種理直氣壯型的。

還好沒人要求用WORD 2007的格式開,不然原生格式是XML的docx,就算你把副檔名硬改,開啟就給你個紅牌,直接出現「無法開啟 Office Open XML 檔案…檔案已損毀…」,WORD連鳥都不鳥你。
image

行78的檔案名稱設定方式,是用日期+檔名的方式呈現,因為日期的culture文化特性會產生許多變數,所以作罷

用本來想用 DateTime.Today.ToShortDateString()或是FormatDateTime(Now, DateFormat.ShortDate)之類的方式來作字串連結,但是會有文化特性的關係,所以就用簡單的字串來作。

關於日期的文化特性,又是一個蠻大的議題,這邊就簡單帶過。
我在行85、100、123的地方分別寫了三種不同的格式
使用方法我想在MSDN裡有詳細的說明
http://msdn.microsoft.com/en-us/library/8kb3ddd4(VS.110,classic).aspx
http://msdn.microsoft.com/zh-tw/library/system.globalization.datetimeformatinfo.aspx

行97、行120 中把資料流寫入檔案的方法不同,一個沒有NEW 、一個有NEW…到底什麼時候要用NEW

先比較一下差別

行97:Using sw As System.IO.StreamWriter = System.IO.File.CreateText(Server.MapPath(filewithdate))

行120:Using sw As New System.IO.StreamWriter(Server.MapPath(filewithdate), True)

什麼時候要NEW,什麼時候又不用,以前我只記一個要訣,就是要「實體化」而且「繼承原物件的屬性、方法」,然後反正你用NEW的時候程式出錯,你就不要NEW…

目前還在讀MSDN有關的文章後。

實值型別和參考型別
http://msdn.microsoft.com/zh-tw/library/t63sy5hs(VS.110).aspx
物件存留期:物件的建立和終結 (Visual Basic) 中提到了建構函式
http://msdn.microsoft.com/zh-tw/library/hks5e2k6.aspx

還在理解中…所以不枉下斷論…

2 則留言:

  1. 謝謝你, 因為我也正在學, 看到你的文章真的是獲益良多! 希望你繼續寫下去

    回覆刪除
    回覆
    1. 感謝您的回覆鼓勵,寫東西的目的在於自已整理思緒、便於以後查詢,若能讓你有所收獲,我也會很開心。

      刪除

Related Posts Plugin for WordPress, Blogger...
// Dnow Function