Hàm PHP phát hiện các ký tự lạ trong họ tên người Việt Nam (v1.2)

Các ký tự lạ trong mẫu hơn 230 ngàn họ tên mà tôi có không nhiều, vì dữ liệu đầu vào đã khá chuẩn chỉnh rồi. Trong mẫu tỷ lệ chưa đến 0,1% dữ liệu dạng này.

Ký tự lạ là các ký tự ngoài các chữ cái. Tạm thời không phân biệt tên nước ngoài, tên lai (tức là chấp nhận các ký tự f, j, w, z – phần này sẽ được lọc ở hàm khác).


Các ký tự lạ này phần lớn là dạng chủ động, tức là người nhập liệu ý thức được việc này. Mục đích bao gồm:

  • Chú thích thêm cho tên: ví dụ các họ tên trùng nhau trong danh sách một lớp, nhóm được chú thích thêm như (1), (2), (A), (B), hoặc các chú thích khác như (HN), (KT), (SG), *
  • Là dạng viết tắt với những họ tên dài, điển hình được dùng cho họ phổ biến nhất là nguyễn, thường được viết tắt thành ng. hoặc ng~ hoặc ng, số lượng này chiếm hơn 2/3 nguyên nhân ký tự bất thường trong tên;
  • Một dạng viết tắt khác là T. dành cho Thị nhưng tôi không sửa vì số lượng quá ít;

Phân tích thêm về cú pháp regex (biểu thức chính quy) để bắt lỗi các họ tên có chú thích:

([123]|(\(.*\))|(\[.*\])|\*|\s\-*\s*[abcABC])$
  • Dấu $ là để bắt buộc các ký tự này phải nằm cuối thì mới tính;
  • Dấu | nghĩa là hoặc, tức là trùng với một trong các điều kiện là bắt lỗi;
  • \123 nghĩa là số từ 1 hoặc 2 hoặc 3 thì bắt;
  • \(.*\) nghĩa là cứ có dấu () ở cuối, ở trong có bất kỳ ký tự nào cũng bắt, ví dụ (1), (A) là đều bắt;
  • \[.*\] cũng tương tự, cứ có dấu [] ở cuối, ở trong có bất kỳ ký tự nào cũng bắt, ví dụ [1], [A] là đều bắt;
  • \* dấu * sau dấu \ nghĩa là dấu * thông thường, câu lệnh này hàm nghĩa có dấu * ở cuối tên là bắt;
  • \s-*\s*[abcABC] thì \s đại diện cho khoảng trắng, * nghĩa là có cũng được, không có cũng được, nó sẽ bắt các trường hợp kiểu như Lê Phương Thảo A, Lê Phương Thảo - B

Regex hơi khó hiểu, nhưng chỉ là lúc đầu, nếu các bạn quen dần sẽ thấy nó rất hay, vì nó giúp cho chúng ta đỡ phải viết các câu lệnh dài dòng, mất thời gian trong khi hiệu quả có thể kém hơn.


Nhiệm vụ của mã này là xóa bỏ các ký tự bất thường ở cuối từ, và chỉnh sửa từ viết tắt có độ tin cậy cao dành cho họ nguyễn.

Cả hai thao tác chỉnh sửa dữ liệu này đều rất an toàn, và giúp chúng ta cải thiện mức độ đồng bộ của dữ liệu mẫu.

Kết hợp hàm này với hàm chỉnh sửa vị trí dấu tiếng Việt, hàm chỉnh sửa dính từhàm chuyển đổi mã hex, chúng ta sẽ có dữ liệu mẫu thống nhất và đồng đều, đủ để tiến hành thống kê.


File này cần require đến các file sau:

Ứng dụng thực tế: cải thiện chất lượng dữ liệu họ tên thô ban đầu, để cho kết quả thống kê về họ tên người Việt Nam có chất lượng cao hơn.


Hướng dẫn sử dụng

  • Để kiểm tra tất họ tên đầy đủ có ký tự lạ bạn dùng hàm vn_filter_unusual_char_big($str)
  • Hàm trên có thể giúp bạn xác định được mô hình ký tự lạ trong dữ liệu của bạn để có thể bổ sung thêm các hàm khác nếu cần;
  • Phát hiện các chuỗi viết tắt cho nguyễn bạn dùng hàm vn_abbr_nguyen_check($str), nó sẽ bắt các họ tên như ng~ đức anh minh, ng~đức anh minh, ng. đức anh minh, ng.đức anh minhng đức anh minh, các ký tự bắt có thể ở trong đệm (có một số lượng lớn tên người sử dụng đệm là họ nguyễn, thường là họ mẹ);
  • Thay thế các viết tắt trong mô hình trên bằng chuỗi nguyễn bạn sử dụng hàm vn_abbr_nguyen_rep($str)
  • Kiểm tra phần cuối của họ tên có các chú thích như 1, 2, (A), (B), (), [], * bạn dùng hàm vn_unu_end_char_check($name), trong đó $name là chuỗi họ tên đầy đủ;
  • Để xóa các phần cuối với các chú thích trên bạn dùng hàm vn_rmv_unu_end_char($name)

Ví dụ về cú pháp để sửa với viết tắt của họ nguyễn:

if (vn_abbr_nguyen_check($str)) {
         vn_abbr_nguyen_rep($str);
}

Mã hoàn chỉnh

<?php

/* 
 * Filter unusual characters in Vietnamese names v1.2 (final)
 * MIT License
 * Nguyen Duc Anh - freehost.page
 */

///=============================================================================


// phát hiện tất cả ký tự lạ, dành cho một từ
function vn_filter_unusual_char($str) {
    $rs = 0; // giả định là không bị vấn đề ký tự lạ    
    // gộp mảng 29 ký tự và mảng các nguyên âm có dấu để ra tất cả các ký tự được phép dùng trong từ tiếng Việt
    $all_lett = vna_all_lett(); // bảng chữ cái
    $acc_char = vna_acc_char_array(); // các nguyên âm đơn có dấu
    $frc = array("f","j","w","z"); // không loại bỏ các tên nước ngoài
    
    $usual = array_merge($all_lett, $acc_char, $frc); // gộp 3 mảng lại

    $str2 = pop_hex_convert($str); // chuyển mã hóa và chuyển nó về ký tự thường
    $chars = preg_split('//u', $str2, -1, PREG_SPLIT_NO_EMPTY); // tách các ký tự của tên viết thường và đưa nó vào mảng

    foreach ($chars as $char) {
        if (!in_array($char, $usual)) {$rs = 1; break;} // phát hiện ra ký tự lạ
    }

return $rs;    
}

////////////////////////////////////////////////////////////////////////////////
// phát hiện tất cả ký tự lạ, dành cho một chuỗi nhiều từ
function vn_filter_unusual_char_big($str) {
    $rs = 0;  
    $namex = vn_rmv_wsp($str); // xóa bỏ khoảng trắng dư thừa
    $words = mb_split(' ', $namex); // tách ra thành mảng nhiều từ
    
    foreach ($words as $word) {
        $rs = vn_filter_unusual_char($word);
        if ($rs) {break;}
    }
 
return $rs;   
}


///=============================================================================
// kiểm tra phần cuối có các ký tự như *, (A), 1, 2 (HN),vv thì loại bỏ, dành cho
// A, B, C ở cuối cũng được bắt
// một cụm từ họ tên đầy đủ
function vn_unu_end_char_check($name) {
    $pattern = '/([123]|(\(.*\))|(\[.*\])|\*|\s\-*\s*[abcABC])$/'; // cú pháp của regex
    $namex = pop_hex_convert($name); // chuyển thành mã phổ biến và ký tự thường
    $rs = preg_match($pattern, $namex); // so khớp xem có hay không

return $rs;     
}

////////////////////////////////////////////////////////////////////////////////
//Loại bỏ phần cuối có các ký tự như *, (A), 1, 2 (HN),vv, dành cho một cụm từ họ tên đầy đủ
//A, B, C ở cuối cũng được sửa
function vn_rmv_unu_end_char($name) {
    $pattern = '/([123]|(\(.*\))|(\[.*\])|\*|\s\-*\s*[abcABC])$/'; 
    $namex = preg_replace($pattern, '', $name); // thực hiện xóa

return vn_rmv_wsp($namex); // loại bỏ khoảng trắng dư thừa      
} 


///=============================================================================


// Dấu chấm tồn tại trong tên
function vn_dot_in_name_check($name) {
    $pattern = '/\./'; // cú pháp của regex
    $rs = preg_match($pattern, $name); // so khớp xem có hay không

return $rs;     
}

////////////////////////////////////////////////////////////////////////////////
// Thay thế bằng khoảng trắng sẽ phù hợp hơn trong nhiều trường hợp
function vn_dot_in_name_rep($name) {
    $pattern = '/\./'; // cú pháp của regex 
    $namex = preg_replace($pattern, ' ', $name); // thực hiện thay thế

return vn_rmv_wsp($namex); // loại bỏ khoảng trắng dư thừa     
} 



///=============================================================================


// phát hiện các từ viết tắt của nguyễn, dành cho một cụm từ họ tên đầy đủ
function vn_abbr_nguyen_check($str) {
    $rs = 0;
    $abbr_nguyen = array ("ng.","ng","ng~"); // các mẫu viết tắt đầu vào
    $namex = pop_hex_convert($str); // xóa bỏ khoảng trắng dư thừa, chuyển về ký tự thường
    $words = mb_split(' ', $namex); // tách ra thành mảng nhiều từ
    
    foreach ($words as $word) {
        $rs1 = in_array($word,$abbr_nguyen); // kiểm tra từ tách có thuộc mảng viết tắt
        
        $pt2 = '/ng\./';
        $rs2 = preg_match($pt2, $word); // kiểm tra ng. có trong từ tách?
        
        $pt3 = '/ng\~/';
        $rs3 = preg_match($pt3, $word); // kiểm tra ng~ có trong từ tách       
        
        if ($rs1 || $rs2 || $rs3) {$rs = 1; break;} // nếu có thì ghi nhận
    }

return $rs;    
}

////////////////////////////////////////////////////////////////////////////////
// thay thế từ viết tắt của nguyễn, dành cho một cụm từ họ tên đầy đủ
function vn_abbr_nguyen_rep($str) {
    $new_name = '';
    $abbr_nguyen = array ("ng.","ng","ng~"); // các mẫu viết tắt đầu vào
    $namex = pop_hex_convert($str); // xóa bỏ khoảng trắng dư thừa, chuyển về ký tự thường
    $words = mb_split(' ', $namex); // tách ra thành mảng nhiều từ
    
    foreach ($words as $word) {
        // nếu phát hiện thì gán
        if (in_array($word,$abbr_nguyen)) {$word = "nguyễn";}
        
        $pt2 = '/ng\./';
        $rs2 = preg_match($pt2, $word);
        // nếu phát hiện thì thay thế
        if ($rs2) {$word = preg_replace($pt2, 'nguyễn ', $word);}
        
        $pt3 = '/ng\~/';
        $rs3 = preg_match($pt3, $word); 
        // nếu phát hiện thì thay thế
        if ($rs3) {$word = preg_replace($pt3, 'nguyễn ', $word);}
        
        $new_name .= $word.' ';
    }
    
    if ($new_name == '') {$new_name = $str;} // dự phòng

return vn_rmv_wsp($new_name); // loại bỏ khoảng trắng dư thừa     
}


///=============================================================================


// phát hiện các từ có số 0 thì sửa thành chữ o
function vn_zero_err_check($str) {
    $rs = 0;
    $namex = pop_hex_convert($str); // xóa bỏ khoảng trắng dư thừa, chuyển về ký tự thường
    $words = mb_split(' ', $namex); // tách ra thành mảng nhiều từ
    
    foreach ($words as $word) {
        $ptz = '/0/';
        $rsz = preg_match($ptz, $word); // kiểm tra số 0 có trong từ tách       
        
        if ($rsz && vn_num_char($word) > 1) {$rs = 1; break;} // nếu có thì ghi nhận
    }

return $rs;    
}


// thay thế số 0 thành chữ o
function vn_zero_err_rep($str) {
    $new_name = '';
    $namex = pop_hex_convert($str); // xóa bỏ khoảng trắng dư thừa, chuyển về ký tự thường
    $words = mb_split(' ', $namex); // tách ra thành mảng nhiều từ
    
    foreach ($words as $word) {        
        $ptz = '/0/';
        $rsz = preg_match($ptz, $word); // kiểm tra số 0 có trong từ tách    
        // nếu phát hiện thì thay thế
        if ($rsz && vn_num_char($word) > 1) {$word = preg_replace($ptz, 'o', $word);}
        
        $new_name .= $word.' ';
    }
    
    if ($new_name == '') {$new_name = $str;} // dự phòng

return vn_rmv_wsp($new_name); // loại bỏ khoảng trắng dư thừa     
}

///==================================================================== End code