最新消息

[公告2014/05/30] 如有需要將部落格中,任何一篇文章的程式碼使用在商業用途,請與我聯繫。

[公告2015/04/26] Line版的 iInfo程式與投資應用 群組已上線想加入的朋友們,請先查看 "入群須知" 再與我聯繫 Line : aminwhite5168,加入請告知身分與回答 "入群須知" 的問題。

[公告2018/04/22] 台北 Python + Excel VBA 金融資訊爬蟲課程,課程如網頁內容 金融資訊爬蟲班:台北班 Python 金融資訊爬蟲、EXCEL VBA 金融資訊爬蟲

[公告2019/01/08] 請注意:我再次重申,部落格文章的程式碼,是要提供各位參考與學習,一旦網頁改版請自行修改,別要求東要求西要我主動修改,你們用我寫東西賺錢了、交差了,請問有分我一杯羹嗎?既然賺錢沒分我,請問有什麼理由要求我修改,如果沒能力改,就花錢來找我上課。

[公告2019/12/01] 若各位有 Excel VBA 案子開發需求,歡迎與我聯繫,可接案處理。

[公告2020/05/22] 頁面載入速度慢,起因為部分JS來源(alexgorbatchev.com)失效導致頁面載入變慢,目前已做調整,請多見諒。

2017年1月8日 星期日

LINE Notify 入門到進階應用(6) --- 傳送本機圖片Line Notify

前面幾篇文章介紹Line Notify傳送文字與網路圖片方法都算是小兒科,藉由傳送本機圖片到 Line Notify 的機會來介紹POST傳送方法的食物操作 (是實務操作才對) 的進階方法,這對於網路工程師來說應該屬於小兒科了,這一篇原本筆者不打算公開,因為對一般使用者真的很進階,但幾經考量後,還是將它寫成文章。
如果寫的內容有誤,請不吝嗇的給於小弟一些指導,對於一般使用者,在看完以下內容後跟不上的朋友,請多花時間了解。
HTTP/1.1協議  中規定,HTTP request 由三個部分組成:狀態行、請求頭、消息主體。
<method> <request-uri> <http-version>

<request-header>

<message-body>
詳見 HTTP/1.1協議 第五章內容。

協議規定POST提交的數據放在消息主體中傳輸,消息主體有多種編碼方式,請求頭中的字段Content-type標明了消息主體使用的編碼方式,常見的媒體格式類型如下:
  1. text/html:HTML格式
  2. text/plain:純文本格式
  3. text/xml:XML格式
  4. image/gif:gif圖片格式
  5. image/jpeg:jpg圖片格式
  6. image/png:png圖片格式
  7. application/xhtml+xml:XHTML格式
  8. application/xml:XML數據格式
  9. application/atom+xml:Atom XML聚合格式
  10. application/json:JSON數據格式
  11. application/pdf:pdf格式
  12. application/msword:Word文檔格式
  13. application/octet-stream:二進制流數據(如常見的文件下載)
  14. application/x-www-form-urlencoded:預設encType,form表單數據被編碼為格式發送到伺服器(表單預設的傳送數據的格式)
  15. multipart/form-data:在表單中進行檔上傳時,就需要使用該格式
那表單傳送時,內容型態是由誰決定呢?
答案是「enctype」,它規定了表單數據編碼的內容類型,引用 HTML 4.01規範的Form章節 一段文字:
enctype = content-type [CI]
This attribute specifies the content type used to submit the form to the server (when the value of method is "post"). The default value for this attribute is "application/x-www-form-urlencoded". The value "multipart/form-data" should be used in combination with the INPUT element, type="file".
以上enctype其實就是Content-type,它也是Form中的屬性之一,用來表式傳送參數到伺服器時,消息主體所使用的編碼類型。
<form enctype="value">

筆者將4個常用的Content-type類型補充說明:

類型 描述
application/x-www-form-urlencoded 在發送前編碼所有字元(預設)。
multipart/form-data 不對字元編碼。,在使用包含檔上傳控制項的表單時,必須使用該值。
application/json AJAX中預設JSON數據格式。
text/plain 空格轉換為 "+" 加號,但不對特殊字元編碼。

application/x-www-form-urlencoded說明:
以台灣證券交易所的個股日收盤價及月平均價網頁為例。

查詢股票代碼的表單原始程式碼。

透過 Fiddler 查看傳送的HTTP request內容。

從表單原始程式碼中,並未見到「enctype」屬性設定,在POST傳送參數時,Content-type以「application/x-www-form-urlencoded」作為預設,將消息主體以URL編碼方式轉換。

multipart/form-data 說明:
在Imgur免費圖片空間上傳一張圖片為例。

透過 Fiddler 查看傳送的HTTP request內容。


在HTTP request內容,會看到在 「multipart/form-data」後面用boundary字樣緊接一個字串,該字串是隨機產生,也可指定字串內容,主要是用來區分消息主體內容不重複。每部分以「 --boundary」開始,再接文字內容或檔案內容,如果傳輸的是文件,還要包含文件名和文件類型資訊。消息主體最後以「--boundary--」標示結束。關於「multipart/form-data」的詳細定義,可至 rfc1867 與 rfc7578 查看。

補充說明:
  1. 有些網站如上傳的檔案內容很大,會將檔案資料分割成好幾段,藉由「multipart/form-data」方式上傳檔案。
  2. 使用「multipart/form-data」要注意的是,由於未提供任何編碼動作,所以須將消息主體內容進行UTF8的轉換,不然將無法成功傳送內容。
以上講了這麼多,大家一定會覺得很奇怪,筆者為什麼廢話這麼大一篇內容介紹Content-type,這跟Line Notify傳送本機圖片有什麼關係?

原因:
  1. Line Notify並未提供任何傳送圖片的原始程式碼參考範例,以上述介紹 「multipart/form-data 」的觀念,可用來傳送文字與檔案,再加上 Line Notify API 有支援 「multipart/form-data」,故以 「multipart/form-data」作為本機圖片傳送至Line Notify操作。
  2. 筆者先行介紹傳送檔案的觀念,大家對文章後面的內容能快速上手。
先透過外部工具( Curl )搭配 Fiddler 來觀察 HTTP request,已找出傳送本機圖片至 Line Notify的變化,相關操作可參考 Line 官網部落格(目前只支援英文、日文、韓文) 的 Using LINE Notify to send stickers and upload images 文章,Curl 工具可至此下載,筆者用Windows 32位元 curl-7.52.1 版本操作。

在開啟Fiddler 後,於命令提示字元下輸入以下指令傳送圖片,再觀察Fiddler的變化。
curl -k -x http://127.0.0.1:8888 -X POST https://notify-api.line.me/api/notify -H "Authorization: Bearer dV36PcwrvXQWvySCiGcmxb5ESlbu3seOrCMEquy8am2" -F "message=中文123456789" -F "imageFile=@C:\Users\Amin\Desktop\curl\123.jpg"


搭配Fiddler的HTTP request內容,轉成程式來操作就可以完成傳送本機圖片。
以下就以 Excel VBA 作為傳送圖片的範例程式碼,請搭配以下HTTP request內容圖片:
''' WinApi function that maps a UTF-16 (wide character) string to a new character string
Private Declare Function WideCharToMultiByte Lib "kernel32" ( _
    ByVal CodePage As Long, _
    ByVal dwFlags As Long, _
    ByVal lpWideCharStr As Long, _
    ByVal cchWideChar As Long, _
    ByVal lpMultiByteStr As Long, _
    ByVal cbMultiByte As Long, _
    ByVal lpDefaultChar As Long, _
    ByVal lpUsedDefaultChar As Long) As Long
    
' CodePage constant for UTF-8
Private Const CP_UTF8 = 65001

''' Return byte array with VBA "Unicode" string encoded in UTF-8
Public Function Utf8BytesFromString(strInput As String) As Byte()
    Dim nBytes As Long
    Dim abBuffer() As Byte
    ' Get length in bytes *including* terminating null
    nBytes = WideCharToMultiByte(CP_UTF8, 0&, ByVal StrPtr(strInput), -1, vbNull, 0&, 0&, 0&)
    ' We don't want the terminating null in our byte array, so ask for `nBytes-1` bytes
    ReDim abBuffer(nBytes - 2)  ' NB ReDim with one less byte than you need
    nBytes = WideCharToMultiByte(CP_UTF8, 0&, ByVal StrPtr(strInput), -1, ByVal VarPtr(abBuffer(0)), nBytes - 1, 0&, 0&)
    Utf8BytesFromString = abBuffer
End Function

Sub Line傳讀圖與訊息()
    Dim URL As String
    Dim sToken As String
    Dim sFilepath As String
    Dim nFile           As Integer
    Dim baBuffer()      As Byte
    Dim ssPostData1     As String
    Dim ssPostData2     As String
    Dim ssPostData3     As String
    Dim Messagelength As Integer
    
    Dim arr1() As Byte
    Dim arr2() As Byte
    Dim arr3() As Byte
    Dim arr4() As Byte
     
    Const STR_BOUNDARY  As String = "------------------------058b4eeb7d99b4f6" '設定 BOUNDARY
    sToken = "你的Token"
    URL = "https://notify-api.line.me/api/notify"
    sFilepath = "C:\Users\Amin\Desktop\line\123.jpg"
    sFileName = GetFilenameFromPath(sFilepath)    

    '第一段
    ssPostData1 = "--" & STR_BOUNDARY & vbCrLf & _
                  "Content-Disposition: form-data; name=" & """message""" & vbCrLf & vbCrLf
                
    arr1 = StrConv(ssPostData1, vbFromUnicode)
                
    '第二段 訊息段
    arr2 = Utf8BytesFromString("小天使圖傳送!!!LaLaLa~~~")
    
    '第三段
    ssPostData2 = vbCrLf & _
                  "--" & STR_BOUNDARY & vbCrLf & _
                  "Content-Disposition: form-data; name=" & """imageFile""" & "; filename=" & sFileName & vbCrLf & _
                  "Content-Type: image/jpeg" & vbCrLf & vbCrLf
                
    arr3 = StrConv(ssPostData2, vbFromUnicode)
    
    '第四段,讀出圖檔,圖檔段
    nFile = FreeFile
    Open sFilepath For Binary Access Read As nFile
    If LOF(nFile) > 0 Then
        ReDim baBuffer(0 To LOF(nFile) - 1) As Byte
        Get nFile, , baBuffer
        imagear = baBuffer
    End If
    Close nFile
            
    '第五段,尾巴段
    arr4 = StrConv(vbCrLf & "--" & STR_BOUNDARY & "--" & vbCrLf, vbFromUnicode)
    
    Dim arraytotal As Long
    Dim sendarray() As Byte
    arraytotal = UBound(arr1) + UBound(arr2) + UBound(arr3) + UBound(imagear) + UBound(arr4) + 4
    ReDim sendarray(arraytotal)
    
    '組合全部資料'
    '將 http 的 POST Data 轉換成 Binary array
    '第一段內容填入
    For i = 0 To UBound(arr1)
        sendarray(i) = arr1(i)
    Next
    
    '第二段內容填入
    For i = 0 To UBound(arr2)
        sendarray(UBound(arr1) + i + 1) = arr2(i)
    Next
    
    '第三段內容填入
    For i = 0 To UBound(arr3)
        sendarray(UBound(arr1) + UBound(arr2) + i + 2) = arr3(i)
    Next
    
    '第四段內容填入 圖檔段內容  轉換成 Binary array
    For i = 0 To UBound(imagear)
        sendarray(UBound(arr1) + UBound(arr2) + UBound(arr3) + i + 3) = imagear(i)
    Next
    
    '組合 http POST 內容的尾端部分。
    For i = 0 To UBound(arr4)
        sendarray(UBound(arr1) + UBound(arr2) + UBound(arr3) + UBound(imagear) + i + 4) = arr4(i)
    Next    

    With CreateObject("Microsoft.XMLHTTP")
        '傳送request
        .Open "POST", URL, 0
        
        '設定Header內容
        .SetRequestHeader "Content-Type", "multipart/form-data; boundary=" & STR_BOUNDARY
        .SetRequestHeader "Authorization", "Bearer " & sToken
        
        '傳送request到server
        .send sndar(sendarray)
        
        '顯示傳輸是否成功
        Debug.Print .responseText
    End With
End Sub

Public Function sndar(sendarray As Variant) As Byte()
'陣列轉換
    sndar = sendarray
End Function

Public Function GetFilenameFromPath(ByVal strPath As String) As String
'以遞迴方式,從完整路徑中取得檔名
    If Right$(strPath, 1) <> "\" And Len(strPath) > 0 Then
        GetFilenameFromPath = GetFilenameFromPath(Left$(strPath, Len(strPath) - 1)) + Right$(strPath, 1)
    End If
    
End Function

Function URL_Encode(ByVal strOrg As String) As String
  With CreateObject("ScriptControl")
    .Language = "JScript"
    URL_Encode = .CodeObject.encodeURI(strOrg)
  End With
End Function

透過 Fiddler 查看傳送的HTTP request內容。


執行結果:

參考資料:
類似文章: