PHP로 모든 유형의 스마트 인용부호 변환하기
워드 프로세서나 텍스트 편집기는 일반 따옴표 대신 '스마트 인용부호(smart quotes)'라 불리는 특수 유니코드 문자를 자동으로 삽입하는 경우가 많습니다. 데이터베이스 저장, CSV 파싱, API 통신 등에서 이러한 문자들은 예기치 않은 오류를 일으킬 수 있으므로, 표준 ASCII 따옴표(" 와 ')로 통일하는 것이 안전합니다. 아래 코드는 UTF-8 인코딩 입력을 전제로 모든 유형의 스마트 인용부호를 한 번에 변환해 줍니다.
1. 기본 변환 코드
$chr_map = array(
// Windows codepage 1252
"\xC2\x82" => "'", // U+0082⇒U+201A single low-9 quotation mark
"\xC2\x84" => '"', // U+0084⇒U+201E double low-9 quotation mark
"\xC2\x8B" => "'", // U+008B⇒U+2039 single left-pointing angle quotation mark
"\xC2\x91" => "'", // U+0091⇒U+2018 left single quotation mark
"\xC2\x92" => "'", // U+0092⇒U+2019 right single quotation mark
"\xC2\x93" => '"', // U+0093⇒U+201C left double quotation mark
"\xC2\x94" => '"', // U+0094⇒U+201D right double quotation mark
"\xC2\x9B" => "'", // U+009B⇒U+203A single right-pointing angle quotation mark
// Regular Unicode // U+0022 quotation mark (")
// U+0027 apostrophe (')
"\xC2\xAB" => '"', // U+00AB left-pointing double angle quotation mark
"\xC2\xBB" => '"', // U+00BB right-pointing double angle quotation mark
"\xE2\x80\x98" => "'", // U+2018 left single quotation mark
"\xE2\x80\x99" => "'", // U+2019 right single quotation mark
"\xE2\x80\x9A" => "'", // U+201A single low-9 quotation mark
"\xE2\x80\x9B" => "'", // U+201B single high-reversed-9 quotation mark
"\xE2\x80\x9C" => '"', // U+201C left double quotation mark
"\xE2\x80\x9D" => '"', // U+201D right double quotation mark
"\xE2\x80\x9E" => '"', // U+201E double low-9 quotation mark
"\xE2\x80\x9F" => '"', // U+201F double high-reversed-9 quotation mark
"\xE2\x80\xB9" => "'", // U+2039 single left-pointing angle quotation mark
"\xE2\x80\xBA" => "'", // U+203A single right-pointing angle quotation mark
);
$char_val = array_keys ($chr_map); // but: for efficiency you should
$rpl = array_values($chr_map); // pre-calculate these two arrays
$str = str_replace($char_val, $rpl, html_entity_decode($str, ENT_QUOTES, "UTF-8"));이 코드는 먼저 html_entity_decode()로 HTML 엔티티를 디코딩한 뒤, 매핑 테이블에 정의된 바이트 시퀀스를 일반 따옴표로 치환합니다. 성능을 위해서는 array_keys()와 array_values()의 결과를 반복 호출마다 계산하지 말고 미리 캐싱해 두는 것이 좋습니다.
2. 동작 원리 설명
모든 유니코드 문자는 정확히 하나의 '일반 범주(General Category)'에 속합니다. 그중 인용부호 문자가 포함될 수 있는 범주는 다음과 같습니다.
Ps "Punctuation, Open" Pe "Punctuation, Close" Pi "Punctuation, Initial quote (사용 맥락에 따라 Ps 또는 Pe처럼 동작)" Pf "Punctuation, Final quote (사용 맥락에 따라 Ps 또는 Pe처럼 동작)" Po "Punctuation, Other"
3. UTF-8 인코딩 여부 확인
입력 문자열이 UTF-8로 인코딩되어 있는지 확실하지 않다면, 다른 작업을 수행하기 전에 아래 코드를 실행해 인코딩을 보정할 수 있습니다.
if ( !preg_match('/^\\X*$/u', $str)) {
$str = utf8_encode($str);
}참고로 utf8_encode()는 PHP 8.2부터 폐기(deprecated) 상태이므로, 최신 환경에서는 mb_convert_encoding()이나 iconv() 사용을 권장합니다.
4. 0x80~0x9F 범위 데이터 정규화
Windows-1252(CP1252) 코드 페이지에서 넘어온 데이터처럼 0x80~0x9F 범위의 바이트가 포함된 경우, 아래 정규화 맵을 사용해 해당 바이트를 올바른 유니코드 문자로 변환할 수 있습니다.
$normalization_map = array( "\xC2\x80" => "\xE2\x82\xAC", // U+20AC Euro sign "\xC2\x82" => "\xE2\x80\x9A", // U+201A single low-9 quotation mark "\xC2\x83" => "\xC6\x92", // U+0192 latin small letter f with hook "\xC2\x84" => "\xE2\x80\x9E", // U+201E double low-9 quotation mark "\xC2\x85" => "\xE2\x80\xA6", // U+2026 horizontal ellipsis "\xC2\x86" => "\xE2\x80\xA0", // U+2020 dagger "\xC2\x87" => "\xE2\x80\xA1", // U+2021 double dagger "\xC2\x88" => "\xCB\x86", // U+02C6 modifier letter circumflex accent "\xC2\x89" => "\xE2\x80\xB0", // U+2030 per mille sign "\xC2\x8A" => "\xC5\xA0", // U+0160 latin capital letter s with caron "\xC2\x8B" => "\xE2\x80\xB9", // U+2039 single left-pointing angle quotation mark "\xC2\x8C" => "\xC5\x92", // U+0152 latin capital ligature oe "\xC2\x8E" => "\xC5\xBD", // U+017D latin capital letter z with caron "\xC2\x91" => "\xE2\x80\x98", // U+2018 left single quotation mark "\xC2\x92" => "\xE2\x80\x99", // U+2019 right single quotation mark "\xC2\x93" => "\xE2\x80\x9C", // U+201C left double quotation mark "\xC2\x94" => "\xE2\x80\x9D", // U+201D right double quotation mark "\xC2\x95" => "\xE2\x80\xA2", // U+2022 bullet "\xC2\x96" => "\xE2\x80\x93", // U+2013 en dash "\xC2\x97" => "\xE2\x80\x94", // U+2014 em dash "\xC2\x98" => "\xCB\x9C", // U+02DC small tilde "\xC2\x99" => "\xE2\x84\xA2", // U+2122 trade mark sign "\xC2\x9A" => "\xC5\xA1", // U+0161 latin small letter s with caron "\xC2\x9B" => "\xE2\x80\xBA", // U+203A single right-pointing angle quotation mark "\xC2\x9C" => "\xC5\x93", // U+0153 latin small ligature oe "\xC2\x9E" => "\xC5\xBE", // U+017E latin small letter z with caron "\xC2\x9F" => "\xC5\xB8", // U+0178 latin capital letter y with diaeresis ); $chr = array_keys ($normalization_map); // but: for efficiency you should $rpl = array_values($normalization_map); // pre-calculate these two arrays $str = str_replace($chr, $rpl, $str);
이처럼 변환 맵, 인코딩 검증, 정규화 세 단계를 조합하면 출처가 불분명한 텍스트 데이터에서도 스마트 인용부호 문제를 안정적으로 해결할 수 있습니다.