無組件上傳一直是困擾大家的一個問題。其實原理很簡單,核心就是分析字符串。但是,實際操作時,卻困難重重。其中的關(guān)鍵問題還是大家往往對原理的剖析不夠深入,或者是因為過程過于繁瑣,導(dǎo)致bug不斷。一直以來,都想做一個完善的例子,只不過想想就頭痛,加上沒時間(借口,呵呵 ),所以沒有付諸行動。
今天就咬咬牙,給大家提供一個完整的無組件上傳的例子。因為本人耐性不好,所以咱們一點一點來,分幾天完成。未來的幾天,我會天天更新這個文檔,這個過程也是大家學(xué)習(xí)和提高的過程。
(完整的源碼和示例,可以在這里找到:http://www.2yup.com/asp/attach/A0000006.zip)
============================================================== 第一天:認(rèn)識我們的解剖對象——數(shù)據(jù)
上傳文件時,首先要知道我們得到的是什么。下面是一個上傳文件的表單,我們就從他開始。 <form action="doupload.asp" method=post enctype="multipart/form-data"> file1說明:<input type=text name=file1_desc> file1<input type=file name=file1><br> file2說明:<input type=text name=file2_desc> file2<input type=file name=file2><br> <input type=submit name=upload value=upload> </form>
表單中enctype="multipart/form-data"的意思,是設(shè)置表單的MIME編碼。默認(rèn)情況,這個編碼格式是application/x-www-form-urlencoded,不能用于文件上傳;只有使用了multipart/form-data,才能完整的傳遞文件數(shù)據(jù),進行下面的操作(有興趣的朋友,可以自己試試看兩者的異同。方法很簡單,就是把這一句去掉),F(xiàn)在,我們在表單中分別填入數(shù)據(jù): file1的說明 D:\我的 圖片\BACK046.GIF file2的說明 D:\我的 圖片\BACK293.GIF
這里用了中英文、空格混排。目的是讓例子更有一般性。我選的這兩個圖片分別是54和62字節(jié)。大圖片的原理完全一樣,不過小圖片做例子更合適些,原理容易展現(xiàn)。 為了看到我們得到的數(shù)據(jù),在doupload.asp里,有這幾行代碼: <% formsize=request.totalbytes formdata=request.binaryread(formsize) response.BinaryWrite(formdata) %>
很簡單,作用就是打出來傳過來的所有數(shù)據(jù)。如果不熟悉,你可以先研究一下request和response對象的這兩個方法。
提交表單,我們在ie里面查看html源,得到: -----------------------------7d22131090458 Content-Disposition: form-data; name="file1_desc"
file1μ??μ?÷ -----------------------------7d22131090458 Content-Disposition: form-data; name="file1"; filename="D:\?òμ? í???\BACK046.GIF" Content-Type: image/gif
GIF89a‘ì?f?f3?ì???ì!ù,@?.á?o ;
-----------------------------7d22131090458 Content-Disposition: form-data; name="file2_desc"
file2μ??μ?÷ -----------------------------7d22131090458 Content-Disposition: form-data; name="file2"; filename="D:\?òμ? í???\BACK293.GIF" Content-Type: image/gif
GIF89a(‘???YYYììì!ù,(@L€?j(·"j?N(34ˉ; -----------------------------7d22131090458 Content-Disposition: form-data; name="upload"
upload -----------------------------7d22131090458--
不用懷疑,這就是你從上一個“簡單”表單傳過來的東西,F(xiàn)在想想看,怎么對付這一堆東西?是不是看上去有規(guī)律,又不知道從何下手?明天,咱們就分析一下這堆“圖片”,看看怎么分離出我們要的內(nèi)容。
============================================================== 第二天:分拆初步
睡了個好覺,大家腦子清醒多了吧?今天中午吃的火鍋,阿森納vs.鐵哥也沒看完,現(xiàn)在一腦子大油。。。 OK,咱們繼續(xù)研究這個枯燥的問題。首先,要找出規(guī)律?瓷先ニ坪鹾芎唵,就是用 -----------------------------7d22131090458 做分隔,這樣,每一個文本單元里,都是 Content-Disposition: form-data; name="表單域的名字";
表單域的內(nèi)容
而每一個文件單元里,都是 Content-Disposition: form-data; name="表單域的名字"; filename="文件全路徑" Content-Type: 文件類型
文件的二進制內(nèi)容
那么,是不是直接用 split(formdata,"-----------------------------7d22131090458") 就可以得到各個單元了呢?答案是否定的。首先,formdata不是字符串而是二進制串,不能用split的方法;其次,這里的7d22131090458并不固定,每次都會有變化,并不適合做分隔符。所以,應(yīng)該用一個更保險的辦法。想到?jīng)]?很簡單——就用formdata的第一行做分隔符。只要用instrb函數(shù)得到換行符的位置,然后用leftb或midb函數(shù)截取數(shù)據(jù)就行了。我們動手試試: <% ' 二進制的回車<return> bncrlf=chrB(13) & chrB(10)
' 得到formdata formsize=request.totalbytes formdata=request.binaryread(formsize)
' 得到分隔符 divider=leftB(formdata,clng(instrb(formdata,bncrlf))-1)
' 看看對不對? response.BinaryWrite(divider) %>
運行。。。成功了!得到了需要的divider。注意,這里的字符串函數(shù)都是針對二進制數(shù)據(jù)操作的,所以,用的是他們的二進制版,加了“b”(binary的首字母)——instrb,leftb(以后可能還出現(xiàn)rightb,midb,lenb。。等等)。畢竟,formdata是用“binaryread()”得到的嘛。好了,有的分隔符,就可以得到數(shù)據(jù)了。我們從簡單的開始,先拿第一個單元出來看看,目標(biāo)是得到表單域名稱和數(shù)據(jù)。 <% ' 這是回車<return> bncrlf=chrB(13) & chrB(10)
' 得到數(shù)據(jù) formsize=request.totalbytes formdata=request.binaryread(formsize)
' 得到divider,分隔符 divider=leftB(formdata,clng(instrb(formdata,bncrlf))-1)
' 起始位置 startpos = instrb(formdata,divider)+lenb(divider)+lenb(bncrlf) ' 終止位置,從起始位置開始到下一個divider endpos = instrb(startpos, formdata, divider)-lenb(bncrlf) part1 = midb(formdata, startpos, endpos-startpos) response.BinaryWrite(part1) %>
這一段有注釋,相信大家沒問題。如果對這些函數(shù)不了解,可以到http://www.2yup.com/asp/referrence/index.asp下載msdn參考看看vbscript的函數(shù)用法,對提高水平有很大幫助。 這時候得到的結(jié)果可以通過查看生成的html源的方式看到: Content-Disposition: form-data; name="file1_desc"
file1的說明
好了,離成功又進一步! 下來只要分別讀取part1里name="和第一個“雙引號+回車”之間的內(nèi)容就可以得到表單域的名稱;讀取連續(xù)兩個回車之后的內(nèi)容就可以得到表單域的值了。下面一段順理成章: <% ' 這就是name=" const_nameis=chrb(110)&chrb(97)&chrb(109)&chrb(101)&chrb(61)&chrb(34) ' 這是回車<return> bncrlf=chrB(13) & chrB(10)
' 得到數(shù)據(jù) formsize=request.totalbytes formdata=request.binaryread(formsize)
' 得到divider,分隔符 divider=leftB(formdata,clng(instrb(formdata,bncrlf))-1)
' 起始位置 startpos = instrb(formdata,divider)+lenb(divider)+lenb(bncrlf) ' 終止位置,從起始位置開始到下一個divider endpos = instrb(startpos, formdata, divider)-lenb(bncrlf) part1 = midb(formdata, startpos, endpos-startpos)
' 得到表單域名稱,就是<input type=sometype name=somename>里的somename。 fldname = midb(part1,_ instrb(part1, const_nameis)+lenb(const_nameis),_ instrb(part1, bncrlf)-instrb(part1,const_nameis)-lenb(const_nameis)-1) ' 得到表單域的值 fldvalue = midb(part1,_ instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf),_ lenb(part1)-instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf))
' 檢查一下?可以每次打開一個注釋,分別檢查。 'response.binarywrite(fldname) 'response.binarywrite(fldvalue) %>
執(zhí)行一下?呵呵,沒問題啦,分別打開注釋,會在IE里看到“file1_desc”和“file1的說明”。 當(dāng)然,這是得到文本單元的方法,不過看看上邊的原始數(shù)據(jù)就知道,得到文件單元方法可以說是基本相同,只不過: 1。需要額外得到filename=""里的值,也就是文件全路徑; 2。需要額外得到Content-Type: 后邊的值,也就是文件的類型。 這個工作就是體力勞動了,相信大家沒問題。現(xiàn)在更大的精力應(yīng)該放在:怎么得到所有的段落內(nèi)容?想來應(yīng)該是某種形式的循環(huán),但是,具體怎么做?還有,怎么樣組織得到的東西,才不顯得凌亂?
呵呵,不早了,這個就是咱今天晚上要做的夢了。明天來,咱就一起解決這個問題。。。。
============================================================== 第三天:得到所有的文本單元
wake up!繼續(xù)啦~~~~~ 昨天,我們已經(jīng)找到了得到一個單元的信息的辦法,不過,還沒有到實用階段。畢竟,想實用,還至少要: 對于文本單元,能按名稱檢索的內(nèi)容; 對于文件單元,能按名稱得到文件的具體內(nèi)容、類型、全路徑、以及大小等信息。 今天,我們就首先著手解決文本單元的問題。
得到內(nèi)容也許不難,可是,怎么組織才能使這個過程井井有條,才能符合我們的一般習(xí)慣?我們可以從現(xiàn)有的知識里找答案。 大家都知道,asp有一個內(nèi)置對象request,他的功能是得到用戶請求的相關(guān)信息,包括表單域的值。粗看上去,他的form集合的用法和我們要實現(xiàn)的得到文本單元內(nèi)容的功能是很近似的,我們來看看request.form的幾個應(yīng)用的例子:
得到表單域的值 - request.form("表單域名稱")或request.form("表單域在<form></form>里的序號") 得到同名表單域的各個元素 - request.form("表單域名稱")(i)或request.form("表單域在<form></form>里的序號")(i) 得到同名表單域的個數(shù) - request.form("表單域名稱").Count或request.form("表單域在<form></form>里的序號").Count
如果我們能夠用ourRequest.form("name"),ourRequest.form(index),ourRequest.form("name").count,ourRequest.form(index).count這樣的方式,或是與之相近的方式,不就可以很好的和request對象對應(yīng)起來么?而且,因為對request對象本身的熟悉,也會降低使用我們自己方法的時候的門檻,相對于寫一堆getValue(name)函數(shù)這樣的方法,更不容易出錯,擴展性更好更靈活,可讀性也好得多。那么,我們就看看如果要實現(xiàn)自己的request對象,都有哪些工作要做。
首先,ourRequest應(yīng)該是一個對象,有自己的屬性和方法。只有這樣,才可能和現(xiàn)有的request對象做呼應(yīng)。在vbs5里面,已經(jīng)可以通過Class關(guān)鍵字,來實現(xiàn)自己的類了,所以,可行性上是沒有問題的,只要我們自己定義一個類,然后實例化他,就可以得到我們所需的對象; 其次,因為ourRequest.form可以用名稱和序號檢索,所以,應(yīng)該提供比較豐富的訪問方式; 第三,在表單里有多個域名稱相同的時候(比如多個checkbox),應(yīng)該能夠得到其中的各個元素,并且可以得到總個數(shù)。所以,ourRequest.form()得到的,應(yīng)該也是一個可以檢索的對象,而且有Count屬性。
最終,結(jié)合vbscript的語言特點,兼顧開發(fā)效率,我們決定實現(xiàn)這樣的幾個類: A。UploadRequest 這個類和request對象是對應(yīng)的 屬性: RawData 得到原始數(shù)據(jù),方便檢查[只讀] Forms 得到一個有count屬性的計數(shù)器, 可以用outRequest.Forms.Count的方式,得到文本表單域的的個數(shù)[只讀] Form(index) 可以用數(shù)字或文本檢索文本表單域,做用類似request.form。 他返回一個FormElement型的對象 B。FormElement 可以把它看成單個表單域的化身。通過這個類,可以得到詳細(xì)的表單域信息,比如name,value等等。如果有多個value(比如checkbox的情況),還可以選擇用序號索引 屬性: Value 得到表單域的值。如果有多個(比如checkbox), 返回所有的,用逗號分隔[默認(rèn)] Name 得到表單域的名稱 Item(index) 用數(shù)字索引多個值中的某一個 Count 得到對應(yīng)一個name,所擁有的value的個數(shù)。主要用于checkbox[只讀] C。Counter 一個輔助類,就是為了實現(xiàn)outRequest.Forms.Count功能。這里寫的并不好,不過考慮大家的理解方便,先暫時這樣。 屬性: Count 得到Count 方法: setCount 設(shè)置Count
下面,我們就來看看這幾個類的實現(xiàn): <% Class FormElement
' m_開頭,表示類成員變量。 Private m_dicItems
Private Sub Class_Initialize() Set m_dicItems = Server.CreateObject("Scripting.Dictionary") End Sub
' count是咱們這個類的一個只讀屬性 Public Property Get Count() Count = m_dicItems.Count End Property
' Value是一個默認(rèn)屬性。目的是得到值 Public Default Property Get Value() Value = Item("") End Property
' Name是得到文本域名稱。就是<input name=xxx>里的xxx Public Property Get Name() Keys = m_dicItems.Keys Name = Keys(0) Name = left(Name,instrrev(Name,"_")-1) End Property
' Item屬性用來得到重名表單域(比如checkbox)的某一個值 Public Property Get Item(index) If isNumeric(index) Then '是數(shù)字,合法! If index > m_dicItems.Count Then err.raise 1,"IndexOutOfBound", "表單元素子集索引越界" End If Itms = m_dicItems.Items Item = Itms(index) ElseIf index = "" Then '沒給值?那就返回所有的!逗號分隔 Itms = m_dicItems.Items For i = 0 to m_dicItems.Count-1 If i = 0 Then Item = Itms(0) Else Item = Item & "," & Itms(i) End If Next Else '給個一個不是數(shù)字的東東?出錯! err.raise 2,"IllegalArgument", "非法的表單元素子集索引" End If End Property
Public Sub Add(key, item) m_dicItems.Add key, item End Sub
End Class
Class UploadRequest
Private m_dicForms Private m_bFormdata
Private Sub Class_Initialize() Set m_dicForms = Server.CreateObject("Scripting.Dictionary") Call fill() End Sub
' 有了這個,就可以檢查原始數(shù)據(jù)了 Public Property Get RawData() RawData = m_bFormdata End Property
' 這一段丑陋的代碼是為了實現(xiàn)outRequest.Forms.Count這個功能。 Public Property Get Forms() Set Forms = New Counter Forms.setCount(m_dicForms.Count) End Property
Public Property Get Form(index) If isNumeric(index) Then '是數(shù)字?用數(shù)字來檢索 If index > m_dicForms.Count Then err.raise 1,"IndexOutOfBound", "表單元素索引越界" End If Items = m_dicForms.Items Set Form = Items(index) ElseIf VarType(index) = 8 Then '字符串?也行! If m_dicForms.Exists(index) Then '存在,就返回值 Set Form = m_dicForms.Item(index) Else '不存在,就給個空值——request對象就是這么做的。 Exit Property End If Else '給了一個不是數(shù)字也不是字符串的東東?出錯! err.raise 2,"IllegalArgument", "非法的表單元素索引" End If End Property
Private Sub fill ' 得到數(shù)據(jù) m_bFormdata=request.binaryread(request.totalbytes) ' 調(diào)用這個函數(shù)實現(xiàn)遞歸循環(huán),讀取文本單元 Call fillEveryFirstPart(m_bFormdata) End Sub
Private Sub fillEveryFirstPart(data) ' 這就是name=" const_nameis=chrb(110)&chrb(97)&chrb(109)&chrb(101)&chrb(61)&chrb(34) ' 這就是filename=" const_filenameis=chrb(102)&chrb(105)&chrb(108)&chrb(101)&_ chrb(110)&chrb(97)&chrb(109)&chrb(101)&chrb(61)&chrb(34) ' 這是回車<return> bncrlf=chrb(13) & chrb(10) ' 得到divider,分隔符 divider=leftb(data,instrb(data,bncrlf)-1) ' 起始位置 startpos = instrb(data,divider)+lenb(divider)+lenb(bncrlf) ' 終止位置,從起始位置開始到下一個divider endpos = instrb(startpos, data, divider)-lenb(bncrlf) If endpos < 1 Then '沒有下一個了!結(jié)束! Exit Sub End If part1 = midb(data, startpos, endpos-startpos) ' 得到part1的第一行 firstline = midb(part1, 1, instrb(part1, bncrlf)-1)
'沒有filename=",有name=",說明是一個文本單元(這里有一個BUG,自己研究一下?當(dāng)作業(yè)吧) If Not instrb(firstline, const_filenameis) > 0_ And instrb(firstline, const_nameis) > 0 Then ' 得到表單域名稱,就是<input type=sometype name=somename>里的somename。 fldname = B2S(midb(part1,_ instrb(part1, const_nameis)+lenb(const_nameis),_ instrb(part1, bncrlf)_ -instrb(part1, const_nameis)-lenb(const_nameis)-1)) ' 得到表單域的值 fldvalue = B2S(midb(part1,_ instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf),_ lenb(part1)-instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf))) If m_dicForms.Exists(fldname) Then Set fElement = m_dicForms.Item(fldname) m_dicForms.Remove fldname Else Set fElement = new FormElement End If
fElement.Add fldname&"_"&fElement.Count, fldvalue m_dicForms.Add fldname, fElement
End If
' 截取剩下的部分,遞歸調(diào)用這個函數(shù),來得到下一個part1。 Call fillEveryFirstPart(rightb(data, lenb(data)-endpos-1)) End Sub
' 這是一個公用函數(shù),作用是二進制和字符串的轉(zhuǎn)換 Private Function B2S(bstr) If not IsNull(bstr) Then for i = 0 to lenb(bstr) - 1 bchr = midb(bstr,i+1,1) If ascb(bchr) > 127 Then '遇到了雙字節(jié),就得兩個字符一起處理 temp = temp & chr(ascw(midb(bstr, i+2, 1) & bchr)) i = i+1 Else temp = temp & chr(ascb(bchr)) End If next End If B2S = temp End Function
End Class
' 這是一個輔助類,為了實現(xiàn)outRequest.Forms.Count功能。 Class Counter Private m_icnt
' count是咱們這個類的一個只讀屬性 Public Property Get Count() Count = m_icnt End Property
Public Function setCount(cnt) m_icnt = cnt End Function End Class %>
<% '下面是測試碼 set outRequest = new UploadRequest %>
<%=outRequest.Form(0).Name%>:<%=outRequest.Form("file1_desc")%><br> <%=outRequest.Form(1).Name%>:<%=outRequest.Form("file2_desc")%><br> <%=outRequest.Form(2).Name%>:<%=outRequest.Form(2).Count%><br> <%=outRequest.Form(3).Name%>:<%=outRequest.Form(3)%><hr>
一共有<%=outRequest.Forms.Count%>個文本單元
這里的注釋很詳細(xì),而且,每一個類的屬性和方法都很少,所以相信基礎(chǔ)好的朋友讀懂是沒有問題的。對應(yīng)的,我們的測試表單也改成了: <form action="doupload.asp" method=post enctype="multipart/form-data"> file1說明:<input type=text name=file1_desc> file1<input type=file name=file1><br> file2說明:<input type=text name=file2_desc> file2<input type=file name=file2><br> <input type=checkbox name=chk value=a>a <input type=checkbox name=chk value=b>b <input type=checkbox name=chk value=c>c <input type=checkbox name=chk value=d>d <input type=checkbox name=chk value=e>e<hr> <input type=submit name=upload value=upload> </form>
注意,這里的每一個文本表單域都要填上,因為測試碼給得很特殊,讀了0,1,2,3各個項目的值,測試了各個屬性。不過,現(xiàn)實情況下,因為事先知道表單域的名稱;即使不知道,也可以用outRequest.Forms.Count來循環(huán)讀取,所以是沒問題的,不容易出錯。
現(xiàn)在,試試看!怎么樣?成功了吧 呵呵,中英文都沒有問題,用法也很簡單,很清晰。現(xiàn)在,我們就可以說基本上解決了文本域的讀取問題。
-------------------------------------------------------- 今天這一段是很有挑戰(zhàn)性的。我寫了兩個多小時。對于尚處于初級的朋友,可能會覺得有些吃力。其實,關(guān)鍵在于深刻的理解類的概念,如果這一點沒有問題,那么,理解這些代碼就不在話下了。對了,今天的代碼里有一個比較明顯的BUG(我故意放的,當(dāng)然,肯定還有不少不明顯的BUG ),有興趣的朋友可以當(dāng)做作業(yè)來檢驗一下自己的水平。 因為今天要掌握的內(nèi)容比較多,所以,明天暫停一天,給大家一個消化的機會(我也順便偷個懶)。。。如果有疑問,請用下面的“我要提問”連接提出。 現(xiàn)在輕松啦,可以上床虎虎了。。。
============================================================== 第四天:休息,休息一下
今天大家可要好好消化一下昨天的東西啦。。正好,我也歇歇。對了,有不明白的,點下面的“我要提問”連接提出,我會在論壇里解答。畢竟這里的“我要評論”顯示效果差一些,也不能查詢。謝謝大家合作 ^ ^
|