成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

用Java語言進(jìn)行Unicode代理編程

開發(fā) 后端
早期Java版本使用16位char數(shù)據(jù)類型表示Unicode字符。Java語言就提供一些API來支持不能通過一個單一16位char數(shù)據(jù)類型表示的Unicode增補(bǔ)字符。本文討論這些API的特性,展示它們的正確用法,并評估它們的處理性能。

我們曾在四種語言的Unicode處理簡述中簡述過Java以外的其他三種語言的Unicode處理方法,對Java的Unicode代理編程介紹很簡略。本文討論這些 API 的特性,展示它們的正確用法,并評估它們的處理性能。

早期 Java 版本使用 16 位 char 數(shù)據(jù)類型表示 Unicode 字符。這種設(shè)計方法有時比較合理,因?yàn)樗?Unicode 字符擁有的值都小于 65,535 (0xFFFF),可以通過 16 位表示。但是,Unicode 后來將最大值增加到 1,114,111 (0x10FFFF)。由于 16 位太小,不能表示 Unicode version 3.1 中的所有 Unicode 字符,32 位值 — 稱為碼位(code point) — 被用于 UTF-32 編碼模式。

但與 32 位值相比,16 位值的內(nèi)存使用效率更高,因此 Unicode 引入了一個種新設(shè)計方法來允許繼續(xù)使用 16 位值。UTF-16 中采用的這種設(shè)計方法分配 1,024 值給 16 位高代理(high surrogate),將另外的 1,024 值分配給 16 位低代理(low surrogate)。它使用一個高代理加上一個低代理 — 一個代理對(surrogate pair) — 來表示 65,536 (0x10000) 和 1,114,111 (0x10FFFF) 之間的 1,048,576 (0x100000) 值(1,024 和 1,024 的乘積)。

Java 1.5 保留了 char 類型的行為來表示 UTF-16 值(以便兼容現(xiàn)有程序),它實(shí)現(xiàn)了碼位的概念來表示 UTF-32 值。這個擴(kuò)展(根據(jù) JSR 204:Unicode Supplementary Character Support 實(shí)現(xiàn))不需要記住 Unicode 碼位或轉(zhuǎn)換算法的準(zhǔn)確值 — 但理解代理 API 的正確用法很重要。

東亞國家和地區(qū)近年來增加了它們的字符集中的字符數(shù)量,以滿足用戶需求。這些標(biāo)準(zhǔn)包括來自中國的國家標(biāo)準(zhǔn)組織的 GB 18030 和來自日本的 JIS X 0213。因此,尋求遵守這些標(biāo)準(zhǔn)的程序更有必要支持 Unicode 代理對。本文解釋相關(guān) Java API 和編碼選項(xiàng),面向計劃重新設(shè)計他們的軟件,從只能使用 char 類型的字符轉(zhuǎn)換為能夠處理代理對的新版本的讀者。

順序訪問

順序訪問是在 Java 語言中處理字符串的一個基本操作。在這種方法下,輸入字符串中的每個字符從頭至尾按順序訪問,或者有時從尾至頭訪問。本小節(jié)討論使用順序訪問方法從一個字符串創(chuàng)建一個 32 位碼位數(shù)組的 7 個技術(shù)示例,并估計它們的處理時間。

示例 1-1:基準(zhǔn)測試(不支持代理對)

清單 1 將 16 位 char 類型值直接分配給 32 位碼位值,完全沒有考慮代理對:

清單 1. 不支持代理對

  1. int[] toCodePointArray(String str) { // Example 1-1  
  2.     int len = str.length();          // the length of str  
  3.     int[] acp = new int[len];        // an array of code points  
  4.  
  5.     for (int i = 0j = 0; i < len; i++) {  
  6.         acp[j++] = str.charAt(i);  
  7.     }  
  8.     return acp;  
  9. }  
  10.  

盡管這個示例不支持代理對,但它提供了一個處理時間基準(zhǔn)來比較后續(xù)順序訪問示例。

示例 1-2:使用 isSurrogatePair()

清單 2 使用 isSurrogatePair() 來計算代理對總數(shù)。計數(shù)之后,它分配足夠的內(nèi)存以便一個碼位數(shù)組存儲這個值。然后,它進(jìn)入一個順序訪問循環(huán),使用 isHighSurrogate() 和 isLowSurrogate() 確定每個代理對字符是高代理還是低代理。當(dāng)它發(fā)現(xiàn)一個高代理后面帶一個低代理時,它使用 toCodePoint() 將該代理對轉(zhuǎn)換為一個碼位值并將當(dāng)前索引值增加 2。否則,它將這個 char 類型值直接分配給一個碼位值并將當(dāng)前索引值增加 1。這個示例的處理時間比 示例 1-1 長 1.38 倍。

清單 2. 有限支持

  1. int[] toCodePointArray(String str) { // Example 1-2  
  2.     int len = str.length();          // the length of str  
  3.     int[] acp;                       // an array of code points  
  4.     int surrogatePairCount = 0;      // the count of surrogate pairs  
  5.  
  6.     for (int i = 1; i < len; i++) {  
  7.         if (Character.isSurrogatePair(str.charAt(i - 1), str.charAt(i))) {  
  8.             surrogatePairCount++;  
  9.             i++;  
  10.         }  
  11.     }  
  12.     acp = new int[len - surrogatePairCount];  
  13.     for (int i = 0j = 0; i < len; i++) {  
  14.         char ch0 = str.charAt(i);         // the current char  
  15.         if (Character.isHighSurrogate(ch0) && i + 1 < len) {  
  16.             char ch1 = str.charAt(i + 1); // the next char  
  17.             if (Character.isLowSurrogate(ch1)) {  
  18.                 acp[j++] = Character.toCodePoint(ch0, ch1);  
  19.                 i++;  
  20.                 continue;  
  21.             }  
  22.         }  
  23.         acp[j++] = ch0;  
  24.     }  
  25.     return acp;  
  26. }  
  27.  

清單 2 中更新軟件的方法很幼稚。它比較麻煩,需要大量修改,使得生成的軟件很脆弱且今后難以更改。具體而言,這些問題是:

◆需要計算碼位的數(shù)量以分配足夠的內(nèi)存

◆很難獲得字符串中的指定索引的正確碼位值

◆很難為下一個處理步驟正確移動當(dāng)前索引

一個改進(jìn)后的算法出現(xiàn)在下一個示例中。

示例:基本支持

Java 1.5 提供了 codePointCount()、codePointAt() 和 offsetByCodePoints() 方法來分別處理 示例 1-2 的 3 個問題。清單 3 使用這些方法來改善這個算法的可讀性:

清單 3. 基本支持

  1. int[] toCodePointArray(String str) { // Example 1-3  
  2.     int len = str.length();          // the length of str  
  3.     int[] acp = new int[str.codePointCount(0, len)];  
  4.  
  5.     for (int i = 0j = 0; i < leni = str.offsetByCodePoints(i, 1)) {  
  6.         acp[j++] = str.codePointAt(i);  
  7.     }  
  8.     return acp;  
  9. }  
  10.  

但是,清單 3 的處理時間比 清單 1 長 2.8 倍。

示例 1-4:使用 codePointBefore()

當(dāng) offsetByCodePoints() 接收一個負(fù)數(shù)作為第二個參數(shù)時,它就能計算一個距離字符串頭的絕對偏移值。接下來,codePointBefore() 能夠返回一個指定索引前面的碼位值。這些方法用于清單 4 中從尾至頭遍歷字符串:

清單 4. 使用 codePointBefore() 的基本支持

  1. int[] toCodePointArray(String str) { // Example 1-4  
  2.     int len = str.length();          // the length of str  
  3.     int[] acp = new int[str.codePointCount(0, len)];  
  4.     int j = acp.length;              // an index for acp  
  5.  
  6.     for (int i = len; i > 0; i = str.offsetByCodePoints(i, -1)) {  
  7.         acp[--j] = str.codePointBefore(i);  
  8.     }  
  9.     return acp;  
  10. }  
  11.  

這個示例的處理時間 — 比 示例 1-1 長 2.72 倍 — 比 示例 1-3 快一些。通常,當(dāng)您比較零而不是非零值時,JVM 中的代碼大小要小一些,這有時會提高性能。但是,微小的改進(jìn)可能不值得犧牲可讀性。

示例 1-5:使用 charCount()

示例 1-3 和 1-4 提供基本的代理對支持。他們不需要任何臨時變量,是健壯的編碼方法。要獲取更短的處理時間,使用 charCount() 而不是 offsetByCodePoints() 是有效的,但需要一個臨時變量來存放碼位值,如清單 5 所示:

清單 5. 使用 charCount() 的優(yōu)化支持

  1. int[] toCodePointArray(String str) { // Example 1-5  
  2.     int len = str.length();          // the length of str  
  3.     int[] acp = new int[str.codePointCount(0, len)];  
  4.     int j = 0;                       // an index for acp  
  5.  
  6.     for (int i = 0, cp; i < len; i += Character.charCount(cp)) {  
  7.         cp = str.codePointAt(i);  
  8.         acp[j++] = cp;  
  9.     }  
  10.     return acp;  
  11. }  
  12.  

清單 5 的處理時間降低到比 示例 1-1 長 1.68 倍。

示例 1-6:訪問一個 char 數(shù)組

清單 6 在使用 示例 1-5 中展示的優(yōu)化的同時直接訪問一個 char 類型數(shù)組:

清單 6. 使用一個 char 數(shù)組的優(yōu)化支持

  1. int[] toCodePointArray(String str) { // Example 1-6  
  2.     char[] ach = str.toCharArray();  // a char array copied from str  
  3.     int len = ach.length;            // the length of ach  
  4.     int[] acp = new int[Character.codePointCount(ach, 0, len)];  
  5.     int j = 0;                       // an index for acp  
  6.  
  7.     for (int i = 0, cp; i < len; i += Character.charCount(cp)) {  
  8.         cp = Character.codePointAt(ach, i);  
  9.         acp[j++] = cp;  
  10.     }  
  11.     return acp;  
  12. }  
  13.  

char 數(shù)組是使用 toCharArray() 從字符串復(fù)制而來的。性能得到改善,因?yàn)閷?shù)組的直接訪問比通過一個方法的間接訪問要快。處理時間比 示例 1-1 長 1.51 倍。但是,當(dāng)調(diào)用時,toCharArray() 需要一些開銷來創(chuàng)建一個新數(shù)組并將數(shù)據(jù)復(fù)制到數(shù)組中。String 類提供的那些方便的方法也不能被使用。但是,這個算法在處理大量數(shù)據(jù)時有用。

示例 1-7:一個面向?qū)ο蟮乃惴?/strong>

這個示例的面向?qū)ο笏惴ㄊ褂?CharBuffer 類,如清單 7 所示:

清單 7. 使用 CharSequence 的面向?qū)ο笏惴?/strong>

  1. int[] toCodePointArray(String str) {        // Example 1-7  
  2.     CharBuffer cBuf = CharBuffer.wrap(str); // Buffer to wrap str  
  3.     IntBuffer iBuf = IntBuffer.allocate(    // Buffer to store code points  
  4.             Character.codePointCount(cBuf, 0, cBuf.capacity()));  
  5.  
  6.     while (cBuf.remaining() > 0) {  
  7.         int cp = Character.codePointAt(cBuf, 0); // the current code point  
  8.         iBuf.put(cp);  
  9.         cBuf.position(cBuf.position() + Character.charCount(cp));  
  10.     }  
  11.     return iBuf.array();  
  12. }  
  13.  

與前面的示例不同,清單 7 不需要一個索引來持有當(dāng)前位置以便進(jìn)行順序訪問。相反,CharBuffer 在內(nèi)部跟蹤當(dāng)前位置。Character 類提供靜態(tài)方法 codePointCount() 和 codePointAt(),它們能通過 CharSequence 接口處理 CharBuffer。CharBuffer 總是將當(dāng)前位置設(shè)置為 CharSequence 的頭。因此,當(dāng) codePointAt() 被調(diào)用時,第二個參數(shù)總是設(shè)置為 0。處理時間比 示例 1-1 長 2.15 倍。

處理時間比較

這些順序訪問示例的計時測試使用了一個包含 10,000 個代理對和 10,000 個非代理對的樣例字符串。碼位數(shù)組從這個字符串創(chuàng)建 10,000 次。測試環(huán)境包括:

◆OS:Microsoft Windows® XP Professional SP2

◆Java:IBM Java 1.5 SR7

◆CPU:Intel® Core 2 Duo CPU T8300 @ 2.40GHz

◆Memory:2.97GB RAM

表 1 展示了示例 1-1 到 1-7 的絕對和相對處理時間以及關(guān)聯(lián)的 API:

表 1. 順序訪問示例的處理時間和 API

表 1. 順序訪問示例的處理時間和 API 

 

隨機(jī)訪問

隨機(jī)訪問是直接訪問一個字符串中的任意位置。當(dāng)字符串被訪問時,索引值基于 16 位 char 類型的單位。但是,如果一個字符串使用 32 位碼位,那么它不能使用一個基于 32 位碼位的單位的索引訪問。必須使用 offsetByCodePoints() 來將碼位的索引轉(zhuǎn)換為 char 類型的索引。如果算法設(shè)計很糟糕,這會導(dǎo)致很差的性能,因?yàn)?offsetByCodePoints() 總是通過使用第二個參數(shù)從第一個參數(shù)計算字符串的內(nèi)部。在這個小節(jié)中,我將比較三個示例,它們通過使用一個短單位來分割一個長字符串。

示例 2-1:基準(zhǔn)測試(不支持代理對)

清單 8 展示如何使用一個寬度單位來分割一個字符串。這個基準(zhǔn)測試留作后用,不支持代理對。

清單 8. 不支持代理對

  1. String[] sliceString(String str, int width) { // Example 2-1  
  2.     // It must be that "str != null && width > 0".  
  3.     List<String> slices = new ArrayList<String>();  
  4.     int len = str.length();       // (1) the length of str  
  5.     int sliceLimit = len - width; // (2) Do not slice beyond here.  
  6.     int pos = 0;                  // the current position per char type  
  7.  
  8.     while (pos < sliceLimit) {  
  9.         int begin = pos;                       // (3)  
  10.         int end   = pos + width;               // (4)  
  11.         slices.add(str.substring(begin, end));  
  12.         pos += width;                          // (5)  
  13.     }  
  14.     slices.add(str.substring(pos));            // (6)  
  15.     return slices.toArray(new String[slices.size()]); }  
  16.  

sliceLimit 變量對分割位置有所限制,以避免在剩余的字符串不足以分割當(dāng)前寬度單位時拋出一個 IndexOutOfBoundsException 實(shí)例。這種算法在當(dāng)前位置超出 sliceLimit 時從 while 循環(huán)中跳出后再處理最后的分割。

示例 2-2:使用一個碼位索引

清單 9 展示了如何使用一個碼位索引來隨機(jī)訪問一個字符串:

清單 9. 糟糕的性能

  1. String[] sliceString(String str, int width) { // Example 2-2  
  2.     // It must be that "str != null && width > 0".  
  3.     List<String> slices = new ArrayList<String>();  
  4.     int len = str.codePointCount(0, str.length()); // (1) code point count [Modified]  
  5.     int sliceLimit = len - width; // (2) Do not slice beyond here.  
  6.     int pos = 0;                  // the current position per code point  
  7.  
  8.     while (pos < sliceLimit) {  
  9.         int begin = str.offsetByCodePoints(0, pos);            // (3) [Modified]  
  10.         int end   = str.offsetByCodePoints(0, pos + width);    // (4) [Modified]  
  11.         slices.add(str.substring(begin, end));  
  12.         pos += width;                                          // (5)  
  13.     }  
  14.     slices.add(str.substring(str.offsetByCodePoints(0, pos))); // (6) [Modified]  
  15.     return slices.toArray(new String[slices.size()]); }  
  16.  

清單 9 修改了 清單 8 中的幾行。首先,在 Line (1) 中,length() 被 codePointCount() 替代。其次,在 Lines (3)、(4) 和 (6) 中,char 類型的索引通過 offsetByCodePoints() 用碼位索引替代。

基本的算法流與 示例 2-1 中的看起來幾乎一樣。但處理時間根據(jù)字符串長度與示例 2-1 的比率同比增加,因?yàn)?offsetByCodePoints() 總是從字符串頭到指定索引計算字符串內(nèi)部。

示例 2-3:減少的處理時間

可以使用清單 10 中展示的方法來避免 示例 2-2 的性能問題:

清單 10. 改進(jìn)的性能

  1. String[] sliceString(String str, int width) { // Example 2-3  
  2.     // It must be that "str != null && width > 0".  
  3.     List<String> slices = new ArrayList<String>();  
  4.     int len = str.length(); // (1) the length of str  
  5.     int sliceLimit          // (2) Do not slice beyond here. [Modified]  
  6.             = (len >= width * 2 || str.codePointCount(0, len) > width)  
  7.             ? str.offsetByCodePoints(len, -width) : 0;  
  8.     int pos = 0;            // the current position per char type  
  9.  
  10.     while (pos < sliceLimit) {  
  11.         int begin = pos;                                // (3)  
  12.         int end   = str.offsetByCodePoints(pos, width); // (4) [Modified]  
  13.         slices.add(str.substring(begin, end));  
  14.         pos = end;                                      // (5) [Modified]  
  15.     }  
  16.     slices.add(str.substring(pos));                     // (6)  
  17.     return slices.toArray(new String[slices.size()]); }  
  18.  

首先,在 Line (2) 中,(清單 9 中的)表達(dá)式 len-width 被 offsetByCodePoints(len,-width) 替代。但是,當(dāng) width 的值大于碼位的數(shù)量時,這會拋出一個 IndexOutOfBoundsException 實(shí)例。必須考慮邊界條件以避免異常,使用一個帶有 try/catch 異常處理程序的子句將是另一個解決方案。如果表達(dá)式 len>width*2 為 true,則可以安全地調(diào)用 offsetByCodePoints(),因?yàn)榧词顾写a位都被轉(zhuǎn)換為代理對,碼位的數(shù)量仍會超過 width 的值。或者,如果 codePointCount(0,len)>width 為 true,也可以安全地調(diào)用 offsetByCodePoints()。如果是其他情況,sliceLimit 必須設(shè)置為 0。

在 Line (4) 中,清單 9 中的表達(dá)式 pos + width 必須在 while 循環(huán)中使用 offsetByCodePoints(pos,width) 替換。需要計算的量位于 width 的值中,因?yàn)榈谝粋€參數(shù)指定當(dāng) width 的值。接下來,在 Line (5) 中,表達(dá)式 pos+=width 必須使用表達(dá)式 pos=end 替換。這避免兩次調(diào)用 offsetByCodePoints() 來計算相同的索引。源代碼可以被進(jìn)一步修改以最小化處理時間。

處理時間比較

圖 1 和圖 2 展示了示例 2-1、2-2 和 2-3 的處理時間。樣例字符串包含相同數(shù)量的代理對和非代理對。當(dāng)字符串的長度和 width 的值被更改時,樣例字符串被切割 10,000 次。

constantWidth.jpg
圖 1. 一個分段的常量寬度

constantCount.jpg
圖 2. 分段的常量計數(shù)

示例 2-1 和 2-3 按照長度比例增加了它們的處理時間,但 示例 2-2 按照長度的平方比例增加了處理時間。當(dāng)字符串長度和 width 的值增加而分段的數(shù)量固定時,示例 2-1 擁有一個常量處理時間,而示例 2-2 和 2-3 以 width 的值為比例增加了它們的處理時間。

信息 API

大多數(shù)處理代理的信息 API 擁有兩種名稱相同的方法。一種接收 16 位 char 類型參數(shù),另一種接收 32 為碼位參數(shù)。表 2 展示了每個 API 的返回值。第三列針對 U+53F1,第 4 列針對 U+20B9F,最后一列針對 U+D842(即高代理),而 U+20B9F 被轉(zhuǎn)換為 U+D842 加上 U+DF9F 的代理對。如果程序不能處理代理對,則值 U+D842 而不是 U+20B9F 將導(dǎo)致意想不到的結(jié)果(在表 2 中以粗斜體表示)。

表 2. 用于代理的信息 API

表 2. 用于代理的信息 API 

其他 API

本小節(jié)介紹前面的小節(jié)中沒有討論的代理對相關(guān) API。表 3 展示所有這些剩余的 API。所有代理對 API 都包含在表 1、2 和 3 中。

表 3. 其他代理 API

表 3. 其他代理 API

清單 11 展示了從一個碼位創(chuàng)建一個字符串的 5 種方法。用于測試的碼位是 U+53F1 和 U+20B9F,它們在一個字符串中重復(fù)了 100 億次。清單 11 中的注釋部分顯示了處理時間:

清單 11. 從一個碼位創(chuàng)建一個字符串的 5 種方法

  1. int cp = 0x20b9f; // CJK Ideograph Extension B  
  2. String str1 = new String(new int[]{cp}, 0, 1);    // processing time: 206ms  
  3. String str2 = new String(Character.toChars(cp));                  //  187ms  
  4. String str3 = String.valueOf(Character.toChars(cp));              //  195ms  
  5. String str4 = new StringBuilder().appendCodePoint(cp).toString(); //  269ms  
  6. String str5 = String.format("%c", cp);                            // 3781ms  
  7.  

str1、str2、str3 和 str4 的處理時間沒有明顯不同。相反,創(chuàng)建 str5 花費(fèi)的時間要長得多,因?yàn)樗褂?String.format(),該方法支持基于本地和格式化信息的靈活輸出。str5 方法應(yīng)該只用于程序的末尾來輸出文本。

結(jié)束語

Unicode 的每個新版本都包含了通過代理對表示的新定義的字符。東亞字符集標(biāo)準(zhǔn)并不是這樣的字符的惟一來源。例如,移動電話中還需要支持 Emoji 字符(表情圖釋),還有各種古字符需要支持。您從本文收獲的技術(shù)和性能分析將有助于您在您的 Java 應(yīng)用程序中支持所有這些字符。

【編輯推薦】

  1. 四種語言的unicode處理簡述
  2. Perl Unicode全程攻略
  3. PHP 6的國際化增強(qiáng):Unicode編程時代到來
  4. 剖析J2ME對Unicode實(shí)體編碼轉(zhuǎn)換代碼 
  5. 技術(shù)分享 J2ME中讀取Unicode和UTF-8編碼文件
責(zé)任編輯:佚名 來源: developerworks
相關(guān)推薦

2011-12-07 16:50:29

JavaNIO

2012-08-10 13:55:56

Java動態(tài)代理

2010-03-01 09:43:09

Python編程語言

2010-03-29 17:56:20

Nginx反向代理

2010-02-23 10:44:00

Python 編程語言

2024-05-17 09:49:44

RustCursive界面

2010-01-18 17:14:50

C++語言

2010-03-15 15:45:15

Python編程語言

2012-10-23 09:47:01

MapReduceJavaHadoop

2011-06-17 17:27:29

Objective-CCocoa蘋果

2012-02-07 08:48:00

編程語言排行榜

2010-03-24 16:03:51

Python編程語言

2011-07-11 17:38:42

JAVA

2010-03-11 17:24:27

Python編程語言

2012-09-10 09:09:55

編程語言漫畫語言

2021-07-31 21:08:53

工業(yè)機(jī)器人機(jī)器人編程語言

2009-06-22 15:10:00

java 編程AOP

2010-03-19 18:09:35

Java編程語言

2017-10-12 17:58:42

C語言Gtk+應(yīng)用功能測試

2009-12-09 10:52:24

ibmdwLotus
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 久久精品国产99国产 | 国产精品久久一区二区三区 | 国产精品欧美精品日韩精品 | 成人 在线| 老外几下就让我高潮了 | av在线播放国产 | 夜夜干夜夜操 | 国产成人免费视频 | 日日摸日日碰夜夜爽2015电影 | 福利片在线| 国产精品视频免费观看 | 欧美三区在线观看 | 亚洲 欧美 另类 综合 偷拍 | 日韩一区二区在线视频 | 亚洲二区在线 | 欧美激情精品久久久久久 | 国产精品视频 | 久久免费精品 | 我爱操| 亚洲永久在线 | 日韩在线免费视频 | 亚洲一二三视频 | 国产精品99久久久久久久久 | 日韩一区中文字幕 | 日韩一区三区 | 欧美高清免费 | 国产精品视频久久 | 精品一区电影 | 亚洲一区国产 | 欧美日韩大陆 | 91精品国产高清一区二区三区 | 一级h片 | 色综合色综合 | 青青伊人久久 | 国产激情一区二区三区 | 国产精品免费一区二区三区四区 | 亚洲视频a| 97高清国语自产拍 | 伊人久久综合 | 亚洲一区在线免费观看 | 91国产精品在线 |