【ORACLE】详解oracle数据库UTL_ENCODE包各个函数的模拟算法
前言
在前后端交互的開發(fā)中,經(jīng)常會遇到需要將一些二進制數(shù)據(jù),比如圖片文件,編碼成可打印的ascii字符進行傳遞;又者,開發(fā)人員不希望數(shù)據(jù)在傳遞中明文顯示傳遞的文本內(nèi)容,并且有些字符不是ascii字符,無法很好地兼容各種環(huán)境。所以這就涉及到了各種編碼的轉(zhuǎn)換。
雖然目前各種開發(fā)語言均可處理此類編碼,但的確是存在一些場景需要在數(shù)據(jù)庫中對數(shù)據(jù)直接進行編解碼的操作。比如應用直接將編碼后的數(shù)據(jù)存到了數(shù)據(jù)庫,但如果直接在數(shù)據(jù)庫去進行查找時,發(fā)現(xiàn)編碼后的數(shù)據(jù)無法讀,無法寫出想要的數(shù)據(jù)的檢索條件。
在oracle數(shù)據(jù)庫中,有一個UTL_ENCODE包,里面就包含了各種編解碼的函數(shù)
官方文檔 https://docs.oracle.com/en/database/oracle/oracle-database/21/arpls/UTL_ENCODE.html
函數(shù)列表
| BASE64_DECODE | Reads the base 64-encoded RAW input string and decodes it to its original RAW value |
| BASE64_ENCODE | Encodes the binary representation of the RAW value into base 64 elements and returns it in the form of a RAW string |
| MIMEHEADER_DECODE | Decodes a string from mime header format |
| MIMEHEADER_ENCODE | Encodes a string into mime header format |
| QUOTED_PRINTABLE_DECODE | Reads the varchar2 quoted printable format input string and decodes it to the corresponding RAW string |
| QUOTED_PRINTABLE_ENCODE | Reads the RAW input string and encodes it to the corresponding quoted printable format string |
| TEXT_DECODE | Decodes a character set sensitive text string |
| TEXT_ENCODE | Encodes a character set sensitive text string |
| UUDECODE | Reads the RAW uuencode format input string and decodes it to the corresponding RAW string |
| UUENCODE | Reads the RAW input string and encodes it to the corresponding uuencode format string |
函數(shù)分類
首先,“encode"在這里意思是"編碼”,"decode"為解碼,編碼后的東西無法直接閱讀,需要解碼后才能識別。
然后來給這幾個函數(shù)劃分一下類型
從這個列表可以看出,它一共包含了5對編解碼函數(shù),但是,實際上,關(guān)于編解碼的算法,這里只有3個,
即
- BASE64
- QUOTED_PRINTABLE
- UUENCODE
并且這3個函數(shù)的編碼前和編碼后的數(shù)據(jù)參數(shù)類型都是二進制類型(RAW),
而另外的MIMEHEADER_ENCODE和TEXT_ENCODE這兩個函數(shù),需要傳入一個編碼類型的參數(shù),如下
-- Define constants for use by text_encode/decode and mimeheader_encode-- in the 'encoding' parameterbase64 CONSTANT PLS_INTEGER := 1;quoted_printable CONSTANT PLS_INTEGER := 2;可以發(fā)現(xiàn)能夠通過指定編碼類型,來使用base64或者quoted_printable的編碼方式(就是上面3個編碼函數(shù)剔除了UUENCODE)。
并且MIMEHEADER_ENCODE和TEXT_ENCODE這兩個函數(shù)的編碼前和編碼后的數(shù)據(jù)參數(shù)類型都是文本(varchar2),
其中TEXT_ENCODE只會輸出編碼后的字符串本身,而MIMEHEADER_ENCODE會同時輸出字符集、編碼方式、以及編碼后的字符串。
可以看下表的對比
| BASE64 | raw | Y | 基于64個可打印字符來表示二進制數(shù)據(jù) |
| QUOTED_PRINTABLE | raw | Y | 可打印字符引用編碼 |
| UUENCODE | raw | Y | 非標準uuencode算法,存在bug |
| TEXT | varchar2 | N | 可以選擇BASE64或者QUOTED_PRINTABLE兩種編碼方式之一 |
| MIMEHEADER | varchar2 | N | 編碼結(jié)果是在TEXT編碼結(jié)果的前面加上字符集和編碼方式 |
下面開始逐個進行說明
BASE64
用法:
---編碼 select utl_raw.cast_to_varchar2( utl_encode.base64_encode( utl_raw.cast_to_raw('今天天氣不錯哇~'))) from dual; --輸出 '5LuK5aSp5aSp5rCU5LiN6ZSZ5ZOHfg=='--解碼 select utl_raw.cast_to_varchar2( utl_encode.base64_decode( utl_raw.cast_to_raw('5LuK5aSp5aSp5rCU5LiN6ZSZ5ZOHfg=='))) from dual; --輸出 '今天天氣不錯哇~'BASE64太常見了,oracle/mysql/postgresql等常用數(shù)據(jù)庫和java/python等常用開發(fā)語言都有支持。BASE64的算法其實和UUENCODE重合度很高,由于后面會詳細介紹UUENCODE的算法,因此本篇暫不會詳細說明BASE64的算法了。
簡單來說,這個編碼就是將數(shù)據(jù)對應的二進制值取3個字節(jié)(即24位2進制數(shù)據(jù)),按6位作為一個新字節(jié),得到4個新字節(jié),然后每個字節(jié)都可以表示64以內(nèi)的數(shù)值,將其對應到64個可打印字符表即可得到其對應的BASE64編碼,當然中間還是有一些換算及特殊處理,具體可參考百科
https://baike.baidu.com/item/BASE64/8545775
QUOTED_PRINTABLE
用法:
--編碼 select utl_raw.cast_to_varchar2( utl_encode.quoted_printable_encode( utl_raw.cast_to_raw('ABC567,今天天氣不錯哇~,A'))) A from dual; --輸出 'ABC567,=E4=BB=8A=E5=A4=A9=E5=A4=A9=E6=B0=94=E4=B8=8D=E9=94=99=E5=93=87~,A'--解碼 select utl_raw.cast_to_varchar2( utl_encode.quoted_printable_decode( utl_raw.cast_to_raw('ABC567,=E4=BB=8A=E5=A4=A9=E5=A4=A9=E6=B0=94=E4=B8=8D=E9=94=99=E5=93=87~,A'))) from dual; --輸出 'ABC567,今天天氣不錯哇~,A'通過對比編碼前和編碼后可以發(fā)現(xiàn),如果本身就是可打印字符,那么編碼后會保持不變;如果是不可打印字符,比如中文,那么編碼后會根據(jù)其對二進制數(shù)據(jù)的十六進制數(shù)值,使用等于號拼上字節(jié)的形式表示,
比如這里默認是UTF8編碼,“今"字的UTF8編碼的二進制數(shù)據(jù)的十六進制數(shù)值為 “E4BB8A”,然后把這三個字節(jié)的每個字節(jié)前面加上等于號,就得到了”=E4=BB=8A".
既然等于號是個關(guān)鍵的符號,那么如果原始數(shù)據(jù)里有等于號會不會導致解碼出錯呢?答案是不會,因為編碼的時候如果遇到了等于號,會轉(zhuǎn)換成"=3D"這個"3D" 其實就是等于號的十六進制ascii碼。
另外,這種編碼方式其實像極了URLENCODE,只是符號有所區(qū)別而已,然后URLENCODE還多了幾個保留符號
TEXT
用法
--編碼,指定字符集,不指定編碼方式,默認為 2,即 quoted_printable select utl_encode.text_encode('A今天天氣不錯哇~2', encode_charset => 'ZHS16GBK') from dual; --輸出 'A=BD=F1=CC=EC=CC=EC=C6=F8=B2=BB=B4=ED=CD=DB~2'--編碼,指定字符集,指定編碼方式為1,即BASE64 select utl_encode.text_encode('A今天天氣不錯哇~2', encode_charset => 'ZHS16GBK',encoding => 1) from dual; --輸出 'Qb3xzOzM7Mb4sru07c3bfjI='--解碼,針對base64字符串,指定字符集 select utl_encode.text_decode('Qb3xzOzM7Mb4sru07c3bfjI=', encode_charset => 'ZHS16GBK',encoding => 1) from dual --輸出 'A今天天氣不錯哇~2'其實可以看到,如果是要對純文本進行編碼或者解碼,用TEXT的這種方式會比用base64或者quoted_printable本身的那兩個函數(shù)要簡單得多,不用在raw和varchar2之間轉(zhuǎn)來轉(zhuǎn)去,而且還能指定字符集,要知道對于中文來說,GBK的長度只有UTF8的三分之二。
但是,這種方式解碼的時候,必須要知道這個字符串是用什么字符集及什么方式編碼得到的,否則很可能就無法正確解碼,所以,此時可以用 MIMEHEADER 這種方式
MIMEHEADER
用法
--編碼,指定字符集,不指定編碼方式,默認為 2,即 quoted_printable select utl_encode.mimeheader_encode('A今天天氣不錯哇~2', encode_charset => 'ZHS16GBK') from dual; --輸出 '=?ZHS16GBK?Q?A=BD=F1=CC=EC=CC=EC=C6=F8=B2=BB=B4=ED=CD=DB~2?='--編碼,指定字符集,指定編碼方式為1,即BASE64 select utl_encode.mimeheader_encode('A今天天氣不錯哇~2', encode_charset => 'ZHS16GBK',encoding => 1) from dual; --輸出 '=?ZHS16GBK?B?Qb3xzOzM7Mb4sru07c3bfjI=?='--解碼, select utl_encode.mimeheader_decode('=?ZHS16GBK?Q?A=BD=F1=CC=EC=CC=EC=C6=F8=B2=BB=B4=ED=CD=DB~2?=') from dual; --輸出 'A今天天氣不錯哇~2'--解碼 select utl_encode.mimeheader_decode('=?ZHS16GBK?B?Qb3xzOzM7Mb4sru07c3bfjI=?=') from dual; --輸出 'A今天天氣不錯哇~2'和text方式對比可以發(fā)現(xiàn),mimeheader這種方式只是在text的基礎上,前后增加了一點東西而已,具體格式為
=?字符集?編碼方式?text編碼字符串?=
即等于號開始、等于號結(jié)尾,用問號把3個參數(shù)及開始結(jié)束符分隔開,其中編碼方式只有2種
B :BASE64
Q :quoted_printable
由于在編碼后輸出的字符串中已經(jīng)帶上了編碼信息,因此解碼的時候就不需要再指定字符集和編碼方式了。
UUENCODE
用法:
有4個輸入?yún)?shù),
其中文件名和許可都包含在輸出的頭部信息里,也就是說,只有類型為1或2時,文件名和許可才會被輸出
--編碼,其余參數(shù)值默認 select utl_raw.cast_to_varchar2( utl_encode.uuencode( utl_raw.cast_to_raw('今天天氣真好哇~') )) from dual; --輸出 begin 0 uuencode.txt >Y+N*Y:2IY:2IYK"4YYR?Y:6]Y9.'?@ end --編碼,指定type,指定文件名,指定許可 select utl_raw.cast_to_varchar2( utl_encode.uuencode(r => utl_raw.cast_to_raw('今天天氣真好哇~'),type => 1 ,filename => 'filename.txt', permission => 777) ) from dual; --輸出 begin 777 filename.txt >Y+N*Y:2IY:2IYK"4YYR?Y:6]Y9.'?@ end首先要說明一下,以上執(zhí)行結(jié)果是來自于oracle數(shù)據(jù)庫,但實際上這個輸出結(jié)果是錯誤的!
將生成的這串編碼放到在線uuencode解碼的網(wǎng)站中去解碼的話,解碼后的數(shù)據(jù)會存在數(shù)據(jù)缺失的情況!
因此不建議使用ORACLE數(shù)據(jù)庫中的UUENCODE編碼函數(shù),這個函數(shù)有嚴重的BUG!
這是Oracle中少見的持續(xù)了幾十年還不修復的BUG!
ORACLE數(shù)據(jù)庫的utl_encode.uuencode函數(shù),和標準的uuencode有區(qū)別,標準里規(guī)定每行60個字符,除最后一行外,應該都是大寫字母"M"開頭,但oracle中的是每行77個字符,以小寫字母"l"開頭,在mos上有記錄BUG,
UTL_ENCODE.UUENCODE Does Not Follow The Standard Uuencode Format (文檔 ID 2197134.1)
官方解決方案是
Until Bug:6655881 is addressed, use a workaround or use a 3rd party external procedure based on the uuencode/uudecode standard
簡單來說就是這個bug已經(jīng)收錄,官方建議使用第三方外部過程,直到BUG修復
但這個bug是2016年報告的呀,這都5年多了。。。不過utl_encode.uudecode倒是能解析各種各樣的長度(但是oracle解析標準的uuencode編碼會丟失數(shù)據(jù))
當然,uuencode這種編碼已經(jīng)被base64取代了,oracle不把這個bug當回事也情有可原,畢竟這個功能開發(fā)出來幾十年了,也沒多少人反饋有問題。
但強迫癥難忍啊。
于是,我仔細研究uuencode的通用標準,然后在openGauss里,完整重寫了一次uuencode和uudecode函數(shù)。
uuencode算法
以下面這句話為例,字符集為UTF8
今天天氣真好哇~明天天氣怎么樣呢?
獲取其二進制數(shù)據(jù)(這里為了節(jié)省長度,以十六進制展示)
e4bb8ae5a4a9e5a4a9e6b094e79c9fe5a5bde593877ee6988ee5a4a9e5a4a9e6b094e6808ee4b988e6a0b7e591a2
取前3個字節(jié)
e4 bb 8a
其二進制表達形式為
11100100 10111011 10001010
然后按6位一組,前面補兩位0(可以不補,這里僅做完整字節(jié)的示意),變成4個字節(jié)
00111001 00001011 00101110 00001010
得到這4個字節(jié)的十進制數(shù)值為
57 11 46 10
這4個數(shù)字分別都加上十進制的32,得
89 43 78 42
對照ASCII碼表,得這4個數(shù)字分別對應的ascii字符為
Y + N *
這樣就處理好了前三個字節(jié)的數(shù)據(jù)。
這里有個需要注意的地方,由于6位二進制最小可以為’000000’,其十進制為0,再加32得32,對應的ascii字符為一個空格(space),此時需要將其替換成十進制96對應的ascii字符 “`”,即常規(guī)電腦鍵盤左上角的重音符號。
接著繼續(xù)處理后三個字節(jié),就這么一直循環(huán)處理到最后,如果最后不足3個字節(jié),則用二進制的"00000000"補足到3個字節(jié)來處理,因此你會發(fā)現(xiàn),使用uuencode編碼的結(jié)果,后面經(jīng)常會出現(xiàn)重音符號,這和base64編碼末尾經(jīng)常出現(xiàn)的等于號其實是一個原因。
所有的三個字節(jié)編碼完后,拼起來得到:
Y+N*Y:2IY:2IYK"4YYR?Y:6]Y9.'?N:8CN6DJ>6DJ>:PE.:`CN2YB.:@M^61HN^\GP``接下來就要做換行處理了,標準是60個字符一行,即
Y+N*Y:2IY:2IYK"4YYR?Y:6]Y9.'?N:8CN6DJ>6DJ>:PE.:`CN2YB.:@M^61 HN^\GP``然后在每行前面補一個代表長度的字符
此時的字符,其實每4個字符來源之前的3個字節(jié),也就是說,60個字符是對應之前的45個字節(jié),將這個45加32,得77,取77對應的ascii字符"M",所以這個編碼的第一行前面要拼個"M",
同理,第2行有8個字符,對應之前6個字節(jié),6加32得38,即 “&” (其實這里有個問題,有些算法會算出來這里為36,即"$",但這一般不會影響解碼結(jié)果,因為大部分解碼程序都不會去校驗這個長度,所以這也是這種編碼的一個槽點),
然后換一行,加一個重音符號
MY+N*Y:2IY:2IYK"4YYR?Y:6]Y9.'?N:8CN6DJ>6DJ>:PE.:`CN2YB.:@M^61 $HN^\GP`` `到此,主體部分編碼就完成了,然后根據(jù)需要,在前面或者后面加上頭部信息及尾部信息
begin 0 uuencode.txt MY+N*Y:2IY:2IYK"4YYR?Y:6]Y9.'?N:8CN6DJ>6DJ>:PE.:`CN2YB.:@M^61 $HN^\GP`` ` end這樣就完成了。
可以看到,一般情況下,如果生成多行主體,那么除去最后一行外的每一行,第一個字符都應該為大寫的"M"(ascii碼77),而ORACLE為小寫的"l",很可能是因為ORACLE開發(fā)人員自作聰明,以為是每行放77個字符,減去表示長度的這個字符,還剩76個字符,然后76加32得108,108對應的ascii字符就是小寫的英文字母"l"!
從整個編碼過程來看,中間的3字節(jié)轉(zhuǎn)4字節(jié)是相當巧妙,但是那個分行加長度,以及空格替換成重音符號又是相當坑,而且這64個字符中是可能出現(xiàn)其他開發(fā)語言的保留符號的,傳輸過程中還得進行轉(zhuǎn)義,所以這種編碼方式已經(jīng)被base64取代了。
base64與uuencode的不同點是,uuencode是直接映射到ascii碼表,而base64則是單獨定義了要映射到哪64個字符,并且base64沒有這么坑爹的要加行長度。
下面是兩種編碼方式的映射字符表,可以看到都是64個字符,但BASE64少了很多奇奇怪怪的符號
base64 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ uuencode `!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_因此,如果懂了UUENCODE是如何編碼的,那么同理也可以寫出BASE64的編碼程序
總結(jié)
這篇雖然不及上一篇關(guān)于UTL_RAW的更接近底層邏輯,但是這篇更接近開發(fā)的實際使用場景,讓大家知道其實數(shù)據(jù)庫里也是可以做一些復雜的事情的。
另外,這篇文章也算是我在寫完openGauss的UTL_ENCODE兼容包的一個總結(jié)吧,兼容代碼可以在我的項目或者compat_tools項目中找到
utl_encode.sql
compat-tools
- 本文作者: DarkAthena
- 本文鏈接: https://www.darkathena.top/archives/about-utl-encode-and-emulate-cal
- 版權(quán)聲明: 本博客所有文章除特別聲明外,均采用CC BY-NC-SA 3.0 許可協(xié)議。轉(zhuǎn)載請注明出處!
總結(jié)
以上是生活随笔為你收集整理的【ORACLE】详解oracle数据库UTL_ENCODE包各个函数的模拟算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在blog中添加attachments功
- 下一篇: 梦三国解析服务器spl文件头失败怎么解决