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