วิธีที่ดีที่สุดในการทำให้ NSData เป็นอนุกรมเป็นสตริงเลขฐานสิบหก


101

ฉันกำลังมองหาวิธีที่ดีในการจัดลำดับวัตถุ NSData ให้เป็นสตริงเลขฐานสิบหก แนวคิดคือการทำให้อุปกรณ์เป็นอนุกรม Token ที่ใช้สำหรับการแจ้งเตือนก่อนที่จะส่งไปยังเซิร์ฟเวอร์ของฉัน

ฉันมีการใช้งานต่อไปนี้ แต่ฉันคิดว่าต้องมีวิธีที่สั้นและดีกว่านี้

+ (NSString*) serializeDeviceToken:(NSData*) deviceToken
{
    NSMutableString *str = [NSMutableString stringWithCapacity:64];
    int length = [deviceToken length];
    char *bytes = malloc(sizeof(char) * length);

    [deviceToken getBytes:bytes length:length];

    for (int i = 0; i < length; i++)
    {
        [str appendFormat:@"%02.2hhX", bytes[i]];
    }
    free(bytes);

    return str;
}

คำตอบ:


206

นี่คือหมวดหมู่ที่ใช้กับ NSData ที่ฉันเขียน ส่งคืน NSString เลขฐานสิบหกแทน NSData โดยที่ข้อมูลสามารถมีความยาวเท่าใดก็ได้ ส่งคืนสตริงว่างถ้า NSData ว่างเปล่า

NSData + การแปลง h

#import <Foundation/Foundation.h>

@interface NSData (NSData_Conversion)

#pragma mark - String Conversion
- (NSString *)hexadecimalString;

@end

NSData + การแปลง m

#import "NSData+Conversion.h"

@implementation NSData (NSData_Conversion)

#pragma mark - String Conversion
- (NSString *)hexadecimalString {
    /* Returns hexadecimal string of NSData. Empty string if data is empty.   */

    const unsigned char *dataBuffer = (const unsigned char *)[self bytes];

    if (!dataBuffer)
        return [NSString string];

    NSUInteger          dataLength  = [self length];
    NSMutableString     *hexString  = [NSMutableString stringWithCapacity:(dataLength * 2)];

    for (int i = 0; i < dataLength; ++i)
        [hexString appendString:[NSString stringWithFormat:@"%02lx", (unsigned long)dataBuffer[i]]];

    return [NSString stringWithString:hexString];
}

@end

การใช้งาน:

NSData *someData = ...;
NSString *someDataHexadecimalString = [someData hexadecimalString];

นี่ "น่าจะ" ดีกว่าการโทร[someData description]แล้วขีดเว้นวรรค <'s และ> การลอกตัวละครให้ความรู้สึก "แฮ็ค" เกินไป นอกจากนี้คุณไม่มีทางรู้ว่า Apple จะเปลี่ยนรูปแบบของ NSData -descriptionในอนาคตหรือไม่

หมายเหตุ:ฉันมีคนติดต่อฉันเกี่ยวกับการออกใบอนุญาตสำหรับรหัสในคำตอบนี้ ฉันขออุทิศลิขสิทธิ์ของฉันในรหัสที่ฉันโพสต์ไว้ในคำตอบนี้ให้เป็นสาธารณสมบัติ


4
ดี แต่คำแนะนำสองประการ: (1) ฉันคิดว่า appendFormat มีประสิทธิภาพมากกว่าสำหรับข้อมูลขนาดใหญ่เนื่องจากหลีกเลี่ยงการสร้าง NSString ระดับกลางและ (2)% x แสดงถึง int ที่ไม่ได้ลงนามแทนที่จะเป็นแบบยาวที่ไม่ได้ลงนามแม้ว่าความแตกต่างจะไม่เป็นอันตราย
svachalek

ไม่ต้องเป็นคนโง่เพราะนี่เป็นโซลูชันที่ดีที่ใช้งานง่าย แต่โซลูชันของฉันในวันที่ 25 ม.ค. มีประสิทธิภาพมากกว่ามาก หากคุณกำลังมองหาคำตอบที่เพิ่มประสิทธิภาพโปรดดูคำตอบนั้น การโหวตให้คำตอบนี้เป็นวิธีแก้ปัญหาที่ดีและเข้าใจง่าย
NSProgrammer

5
ฉันต้องลบการแคสต์ (แบบยาวที่ไม่ได้ลงนาม) และใช้ @ "% 02hhx" เป็นสตริงรูปแบบเพื่อให้ทำงานได้
Anton

1
ถูกต้องตามdeveloper.apple.com/library/ios/documentation/cocoa/conceptual/…รูปแบบควรเป็น"%02lx"ไปตามการแคสต์(unsigned int)นั้นหรือแคสต์ลงในหรือวางแคสต์และใช้@"%02hhx":)
qix

1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];ดีกว่ามาก (ขนาดหน่วยความจำน้อยลง)
Marek R

31

นี่คือวิธีการจัดหมวดหมู่ NSData ที่ได้รับการปรับให้เหมาะสมที่สุดสำหรับการสร้างสตริงฐานสิบหก ในขณะที่คำตอบของ @Dave Gallagher นั้นเพียงพอสำหรับขนาดที่ค่อนข้างเล็ก แต่ประสิทธิภาพของหน่วยความจำและซีพียูจะด้อยลงสำหรับข้อมูลจำนวนมาก ฉันทำโปรไฟล์นี้ด้วยไฟล์ 2MB บน iPhone 5 ของฉันการเปรียบเทียบเวลาคือ 0.05 กับ 12 วินาที รอยเท้าหน่วยความจำมีน้อยมากเมื่อใช้วิธีนี้ในขณะที่วิธีอื่นเพิ่มฮีปเป็น 70MB!

- (NSString *) hexString
{
    NSUInteger bytesCount = self.length;
    if (bytesCount) {
        const char *hexChars = "0123456789ABCDEF";
        const unsigned char *dataBuffer = self.bytes;
        char *chars = malloc(sizeof(char) * (bytesCount * 2 + 1));       
        if (chars == NULL) {
            // malloc returns null if attempting to allocate more memory than the system can provide. Thanks Cœur
            [NSException raise:NSInternalInconsistencyException format:@"Failed to allocate more memory" arguments:nil];
            return nil;
        }
        char *s = chars;
        for (unsigned i = 0; i < bytesCount; ++i) {
            *s++ = hexChars[((*dataBuffer & 0xF0) >> 4)];
            *s++ = hexChars[(*dataBuffer & 0x0F)];
            dataBuffer++;
        }
        *s = '\0';
        NSString *hexString = [NSString stringWithUTF8String:chars];
        free(chars);
        return hexString;
    }
    return @"";
}

Nice one @Peter - อย่างไรก็ตามมีวิธีแก้ปัญหาที่เร็วกว่า (ไม่มากไปกว่าของคุณ) - ด้านล่าง;)
Moose

1
@ มูสโปรดอ้างอิงให้ชัดเจนยิ่งขึ้นว่าคำตอบใดที่คุณกำลังพูดถึง: คะแนนโหวตและคำตอบใหม่อาจส่งผลต่อการวางตำแหน่งของคำตอบ [แก้ไข: โอ้ให้ฉันเดาคุณหมายถึงคำตอบของคุณเอง ... ]
Cœur

1
เพิ่มการตรวจสอบ malloc null ขอบคุณ @ Cœur
ปีเตอร์

17

การใช้คุณสมบัติคำอธิบายของ NSData ไม่ควรถือเป็นกลไกที่ยอมรับได้สำหรับการเข้ารหัส HEX สตริง คุณสมบัตินั้นเป็นเพียงคำอธิบายเท่านั้นและสามารถเปลี่ยนแปลงได้ตลอดเวลา หมายเหตุก่อน iOS คุณสมบัติคำอธิบาย NSData ไม่ได้ส่งคืนข้อมูลในรูปฐานสิบหก

ขออภัยที่ใช้วิธีแก้ปัญหา แต่สิ่งสำคัญคือต้องใช้พลังงานในการทำให้เป็นอนุกรมโดยไม่ต้องสำรอง API ที่มีไว้สำหรับอย่างอื่นนอกเหนือจากการจัดลำดับข้อมูล

@implementation NSData (Hex)

- (NSString*)hexString
{
    NSUInteger length = self.length;
    unichar* hexChars = (unichar*)malloc(sizeof(unichar) * (length*2));
    unsigned char* bytes = (unsigned char*)self.bytes;
    for (NSUInteger i = 0; i < length; i++) {
        unichar c = bytes[i] / 16;
        if (c < 10) {
            c += '0';
        } else {
            c += 'A' - 10;
        }
        hexChars[i*2] = c;

        c = bytes[i] % 16;
        if (c < 10) {
            c += '0';
        } else {
            c += 'A' - 10;
        }
        hexChars[i*2+1] = c;
    }
    NSString* retVal = [[NSString alloc] initWithCharactersNoCopy:hexChars length:length*2 freeWhenDone:YES];
    return [retVal autorelease];
}

@end

อย่างไรก็ตามคุณต้องว่าง (hexChars) ก่อนกลับ
karim

3
@karim ไม่ถูกต้อง โดยใช้ initWithCharactersNoCopy: length: freeWhenDone: และการมี freeWhenDone be YES NSString จะเข้าควบคุมบัฟเฟอร์ไบต์นั้น การโทรฟรี (hexChars) จะทำให้เกิดความผิดพลาด ประโยชน์ที่นี่มีมากเนื่องจาก NSString ไม่จำเป็นต้องโทรด้วย memcpy ราคาแพง
NSProgrammer

@NSProgrammer ขอบคุณ ฉันไม่สังเกตเห็นโปรแกรมเริ่มต้น NSSting
karim

เอกสารระบุว่าdescriptionส่งคืนสตริงที่เข้ารหัสฐานสิบหกเพื่อให้ฉันเห็นว่าสมเหตุสมผล
ผิดปกติ

เราไม่ควรตรวจสอบค่าที่ส่งคืน malloc ว่าอาจเป็นโมฆะ?
Cœur

9

นี่คือวิธีที่เร็วกว่าในการทำ Conversion:

BenchMark (เวลาเฉลี่ยสำหรับการแปลงข้อมูล 1024 ไบต์ซ้ำ 100 ครั้ง):

Dave Gallagher: ~ 8.070 ms
โปรแกรมเมอร์ NSP: ~ 0.077 ms
Peter: ~ 0.031 ms อัน
นี้: ~ 0.017 ms

@implementation NSData (BytesExtras)

static char _NSData_BytesConversionString_[512] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff";

-(NSString*)bytesString
{
    UInt16*  mapping = (UInt16*)_NSData_BytesConversionString_;
    register UInt16 len = self.length;
    char*    hexChars = (char*)malloc( sizeof(char) * (len*2) );

    // --- Coeur's contribution - a safe way to check the allocation
    if (hexChars == NULL) {
    // we directly raise an exception instead of using NSAssert to make sure assertion is not disabled as this is irrecoverable
        [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc" arguments:nil];
        return nil;
    }
    // ---

    register UInt16* dst = ((UInt16*)hexChars) + len-1;
    register unsigned char* src = (unsigned char*)self.bytes + len-1;

    while (len--) *dst-- = mapping[*src--];

    NSString* retVal = [[NSString alloc] initWithBytesNoCopy:hexChars length:self.length*2 encoding:NSASCIIStringEncoding freeWhenDone:YES];
#if (!__has_feature(objc_arc))
   return [retVal autorelease];
#else
    return retVal;
#endif
}

@end

1
คุณสามารถดูวิธีที่ฉันใช้ malloc ตรวจสอบได้ที่นี่ ( _hexStringวิธีการ): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/…
Cœur

ขอบคุณสำหรับข้อมูลอ้างอิง - BTW ฉันชอบ 'ยาวเกินไป' - นั่นเป็นเรื่องจริง แต่ตอนนี้ฉันพิมพ์แล้วใคร ๆ ก็สามารถคัดลอก / วางได้ - ฉันล้อเล่น - ฉันสร้างมันขึ้นมา - คุณรู้แล้ว :) คุณพูดถูก ความยาวฉันแค่พยายามตีทุกที่ที่ฉันสามารถชนะไมโครวินาที! มันแบ่งการวนซ้ำด้วย 2 แต่ฉันยอมรับว่ามันขาดความสง่างาม Bye
Moose

8

เวอร์ชัน Swift ที่ใช้งานได้

หนึ่งในสายการบิน:

let hexString = UnsafeBufferPointer<UInt8>(start: UnsafePointer(data.bytes),
count: data.length).map { String(format: "%02x", $0) }.joinWithSeparator("")

นี่คือแบบฟอร์มส่วนขยายที่ใช้ซ้ำได้และจัดทำเอกสารด้วยตนเอง:

extension NSData {
    func base16EncodedString(uppercase uppercase: Bool = false) -> String {
        let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(self.bytes),
                                                count: self.length)
        let hexFormat = uppercase ? "X" : "x"
        let formatString = "%02\(hexFormat)"
        let bytesAsHexStrings = buffer.map {
            String(format: formatString, $0)
        }
        return bytesAsHexStrings.joinWithSeparator("")
    }
}

หรือใช้reduce("", combine: +)แทนjoinWithSeparator("")เพื่อให้เพื่อนของคุณมองว่าเป็นผู้เชี่ยวชาญด้านการทำงาน


แก้ไข: ฉันเปลี่ยน String ($ 0, radix: 16) เป็น String (รูปแบบ: "% 02x", $ 0) เนื่องจากต้องใช้ตัวเลขหลักเดียวในการมีช่องว่างภายใน


7

คำตอบของ Peter ส่งไปยัง Swift

func hexString(data:NSData)->String{
    if data.length > 0 {
        let  hexChars = Array("0123456789abcdef".utf8) as [UInt8];
        let buf = UnsafeBufferPointer<UInt8>(start: UnsafePointer(data.bytes), count: data.length);
        var output = [UInt8](count: data.length*2 + 1, repeatedValue: 0);
        var ix:Int = 0;
        for b in buf {
            let hi  = Int((b & 0xf0) >> 4);
            let low = Int(b & 0x0f);
            output[ix++] = hexChars[ hi];
            output[ix++] = hexChars[low];
        }
        let result = String.fromCString(UnsafePointer(output))!;
        return result;
    }
    return "";
}

รวดเร็ว 3

func hexString()->String{
    if count > 0 {
        let hexChars = Array("0123456789abcdef".utf8) as [UInt8];
        return withUnsafeBytes({ (bytes:UnsafePointer<UInt8>) -> String in
            let buf = UnsafeBufferPointer<UInt8>(start: bytes, count: self.count);
            var output = [UInt8](repeating: 0, count: self.count*2 + 1);
            var ix:Int = 0;
            for b in buf {
                let hi  = Int((b & 0xf0) >> 4);
                let low = Int(b & 0x0f);
                output[ix] = hexChars[ hi];
                ix += 1;
                output[ix] = hexChars[low];
                ix += 1;
            }
            return String(cString: UnsafePointer(output));
        })
    }
    return "";
}

สวิฟต์ 5

func hexString()->String{
    if count > 0 {
        let hexChars = Array("0123456789abcdef".utf8) as [UInt8];
        return withUnsafeBytes{ bytes->String in
            var output = [UInt8](repeating: 0, count: bytes.count*2 + 1);
            var ix:Int = 0;
            for b in bytes {
                let hi  = Int((b & 0xf0) >> 4);
                let low = Int(b & 0x0f);
                output[ix] = hexChars[ hi];
                ix += 1;
                output[ix] = hexChars[low];
                ix += 1;
            }
            return String(cString: UnsafePointer(output));
        }
    }
    return "";
}

4

ฉันต้องการแก้ปัญหานี้และพบว่าคำตอบที่นี่มีประโยชน์มาก แต่ฉันกังวลเรื่องประสิทธิภาพ คำตอบเหล่านี้ส่วนใหญ่เกี่ยวข้องกับการคัดลอกข้อมูลจำนวนมากจาก NSData ดังนั้นฉันจึงเขียนสิ่งต่อไปนี้เพื่อทำการแปลงโดยมีค่าใช้จ่ายต่ำ:

@interface NSData (HexString)
@end

@implementation NSData (HexString)

- (NSString *)hexString {
    NSMutableString *string = [NSMutableString stringWithCapacity:self.length * 3];
    [self enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop){
        for (NSUInteger offset = 0; offset < byteRange.length; ++offset) {
            uint8_t byte = ((const uint8_t *)bytes)[offset];
            if (string.length == 0)
                [string appendFormat:@"%02X", byte];
            else
                [string appendFormat:@" %02X", byte];
        }
    }];
    return string;
}

สิ่งนี้จะจัดสรรพื้นที่ล่วงหน้าในสตริงสำหรับผลลัพธ์ทั้งหมดและหลีกเลี่ยงการคัดลอกเนื้อหา NSData โดยใช้ enumerateByteRangesUsingBlock การเปลี่ยน X เป็น x ในสตริงรูปแบบจะใช้เลขฐานสิบหกตัวพิมพ์เล็ก หากคุณไม่ต้องการตัวคั่นระหว่างไบต์คุณสามารถลดคำสั่งได้

if (string.length == 0)
    [string appendFormat:@"%02X", byte];
else
    [string appendFormat:@" %02X", byte];

ลงไปแค่

[string appendFormat:@"%02X", byte];

2
ผมเชื่อว่าดัชนีเพื่อเรียกค่าไบต์จำเป็นต้องปรับเพราะNSRangeแสดงให้เห็นช่วงภายในที่มีขนาดใหญ่NSDataเป็นตัวแทนไม่ใช่ภายในไบต์มีขนาดเล็กบัฟเฟอร์ (ว่าพารามิเตอร์แรกของบล็อกจ่ายให้enumerateByteRangesUsingBlock) NSDataที่แสดงถึงส่วนที่อยู่ติดกันเดียวของที่มีขนาดใหญ่ ดังนั้นจึงbyteRange.lengthสะท้อนให้เห็นขนาดของบัฟเฟอร์ไบต์ แต่เป็นที่ตั้งภายในขนาดใหญ่byteRange.location NSDataดังนั้นคุณต้องการใช้เพียงแค่offsetไม่byteRange.location + offsetดึงข้อมูลไบต์
Rob

1
@Rob ขอบคุณฉันเข้าใจว่าคุณหมายถึงอะไรและได้ปรับรหัสแล้ว
John Stephen

1
ถ้าคุณปรับเปลี่ยนคำสั่งลงไปเพียงแค่ใช้เพียงครั้งเดียวappendFormatที่คุณควรอาจจะเปลี่ยนself.length * 3ไปself.length * 2
ตันคอลลิกัน

1

ฉันต้องการคำตอบที่ใช้ได้กับสตริงความยาวผันแปรดังนั้นนี่คือสิ่งที่ฉันทำ:

+ (NSString *)stringWithHexFromData:(NSData *)data
{
    NSString *result = [[data description] stringByReplacingOccurrencesOfString:@" " withString:@""];
    result = [result substringWithRange:NSMakeRange(1, [result length] - 2)];
    return result;
}

ทำงานได้ดีเป็นส่วนเสริมสำหรับคลาส NSString


1
จะเกิดอะไรขึ้นถ้า Apple เปลี่ยนวิธีแสดงคำอธิบาย
Brenden

1
ในวิธีคำอธิบาย iOS13 จะส่งคืนรูปแบบอื่น
nacho4d

1

คุณสามารถใช้ [yourString uppercaseString] เพื่อใช้อักษรตัวพิมพ์ใหญ่ในคำอธิบายข้อมูลได้ตลอดเวลา


1

วิธีที่ดีกว่าในการทำให้เป็นซีเรียลไลซ์ / deserialize NSData ลงใน NSString คือการใช้ตัวเข้ารหัส / ตัวถอดรหัสGoogle Toolbox สำหรับ Mac Base64 เพียงลากเข้าไปใน App Project ของคุณไฟล์ GTMBase64.m, GTMBase64.he GTMDefines.h จาก package Foundation และทำสิ่งที่ชอบ

/**
 * Serialize NSData to Base64 encoded NSString
 */
-(void) serialize:(NSData*)data {

    self.encodedData = [GTMBase64 stringByEncodingData:data];

}

/**
 * Deserialize Base64 NSString to NSData
 */
-(NSData*) deserialize {

    return [GTMBase64 decodeString:self.encodedData];

}

เมื่อดูซอร์สโค้ดดูเหมือนว่าคลาสที่ให้ตอนนี้คือ GTMStringEncoding ฉันยังไม่ได้ลอง แต่ดูเหมือนว่าจะเป็นทางออกใหม่ที่ดีสำหรับคำถามนี้
sarfata

1
สำหรับ Mac OS X 10.6 / iOS 4.0 NSData ทำการเข้ารหัส Base-64 string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc

@jrc นั้นเป็นจริง แต่ให้พิจารณาเข้ารหัสสตริงการทำงานจริงใน Base-64 คุณต้องจัดการกับการเข้ารหัส "web safe" ซึ่งคุณไม่มีใน iOS / MacOS เช่นใน GTMBase64 # webSafeEncodeData นอกจากนี้คุณอาจต้องเพิ่ม / ลบ Base64 "padding" ดังนั้นคุณจึงมีตัวเลือกนี้เช่นกัน: GTMBase64 # stringByWebSafeEncodingData: (NSData *) data padded: (BOOL) padded;
loretoparisi

1

นี่คือวิธีแก้ปัญหาโดยใช้ Swift 3

extension Data {

    public var hexadecimalString : String {
        var str = ""
        enumerateBytes { buffer, index, stop in
            for byte in buffer {
                str.append(String(format:"%02x",byte))
            }
        }
        return str
    }

}

extension NSData {

    public var hexadecimalString : String {
        return (self as Data).hexadecimalString
    }

}

0
@implementation NSData (Extn)

- (NSString *)description
{
    NSMutableString *str = [[NSMutableString alloc] init];
    const char *bytes = self.bytes;
    for (int i = 0; i < [self length]; i++) {
        [str appendFormat:@"%02hhX ", bytes[i]];
    }
    return [str autorelease];
}

@end

Now you can call NSLog(@"hex value: %@", data)

0

เปลี่ยน%08xเพื่อ%08Xรับอักขระตัวพิมพ์ใหญ่


6
สิ่งนี้จะดีกว่าในฐานะความคิดเห็นเนื่องจากคุณไม่ได้ใส่บริบทใด ๆ Just sayin '
Brenden

0

คุณสมบัติ Swift +

ฉันชอบที่จะมีการแทนค่าฐานสิบหกเป็นคุณสมบัติ (เช่นเดียวกับbytesและdescriptionคุณสมบัติ):

extension NSData {

    var hexString: String {

        let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(self.bytes), count: self.length)
        return buffer.map { String(format: "%02x", $0) }.joinWithSeparator("")
    }

    var heXString: String {

        let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(self.bytes), count: self.length)
        return buffer.map { String(format: "%02X", $0) }.joinWithSeparator("")
    }
}

Idea ยืมมาจากคำตอบนี้


-4
[deviceToken description]

คุณจะต้องลบช่องว่างออก

โดยส่วนตัวฉันbase64เข้ารหัสdeviceTokenแต่มันเป็นเรื่องของรสนิยม


สิ่งนี้ไม่ได้ผลลัพธ์เดียวกัน คำอธิบายผลตอบแทน: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> ขณะที่ฉันกำลังมองหา: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.