Viết lại hàm PHP sửa lỗi dính trong họ tên người (v1.2)

Lỗi dính họ tên có tỷ lệ không quá lớn, trong dữ liệu tôi khảo sát, con số nằm trong khoảng 0,2 – 0,3%. Ví dụ về lỗi dính họ tên: Nguyễn ĐứcAnh

Một trong điều dễ chịu với lỗi này là lỗi dính họ tên rất dễ phát hiện, và cũng dễ sửa.

Để đảm bảo quá trình chỉnh sửa có tỷ lệ chính xác cao và đáng tin cậy, việc tách từ dính được tiến hành như sau.


Dựa trên ký tự viết hoa:

  • Dựa trên ký tự Viết Hoa của từ, như trong ví dụ Nguyễn ĐứcAnh thì sẽ tách dựa trên từ viết hoa thứ 2 trong tên. Và chỉ tách dựa trên từ viết hoa với từ có 2 ký tự viết hoa, nếu nhiều hơn thì không sử dụng biện pháp này;
  • Để tăng độ tin cậy, tôi tạo các mảng họ, đệm, tên phổ biến, và viết lệnh xác thực cả 2 chuỗi vừa tách cần phải thuộc vào nhóm họ, đệm, tên phổ biến. Cách này có thể khiến bỏ sót vài trường hợp họ tên thực nhưng không phổ biến nên không tách, nhưng nó sẽ giúp giảm thiểu sai sót xuống tối đa;

Dựa trên việc chia tách từ:

  • Từ cần kiểm tra được tách ra và đối sánh với các mảng họ, đệm, tên phổ biến, và cũng yêu cầu cả 2 chuỗi vừa tách nằm trong nhóm này;
  • Ngoài ra để tăng thêm độ tin cậy thì từ cần tách phải có từ 5 ký tự đổi lên, các chuỗi tách ra phải có từ 2 ký tự đổ lên cho từng chuỗi;

Thử nghiệm cho thấy, với biện pháp như vậy tỷ lệ tách chính xác rất cao (xấp xỉ 100%).


Về phần dữ liệu thô ban đầu về họ đệm tên phổ biến

Đây là dữ liệu thô bạn có thể dùng luôn dữ liệu mẫu trong mã, hoặc nếu dữ liệu họ tên của bạn có đặc thù riêng, ví dụ của người già (dữ liệu của tôi là cho nhóm trẻ tuổi, trích xuất từ số lượng trên 230 ngàn tên), hoặc số lượng họ tên của bạn lớn hơn đáng kể (ví dụ 1 triệu họ tên) thì bạn nên chủ động tạo dữ liệu thô ban đầu từ chính dữ liệu của bạn, điều đó sẽ cho kết quả chính xác hơn.

Ứng dụng thực tế: được dùng để xử lý dữ liệu thô họ tên người, để gia tăng số lượng các họ tên hợp lệ trong dữ liệu thô ban đầu, qua đó cải thiện chất lượng các kết quả thống kê.


Cách sử dụng

  • Để kiểm tra có cần sửa họ tên dính hay không, bạn dùng hàm vn_check_pop_fix_sticky_upp_big($name)vn_check_pop_fix_sticky_big($name), trong đó $name là biến đầu vào là một chuỗi họ tên;
  • Để sửa lỗi dính tên bạn dùng hàm vn_upp_fix_sticky_big($name)vn_sep_fix_sticky_big($name)
  • Hàm đầu vn_upp_fix_sticky_big($name) tách theo cách dùng ký tự hoa đầu từ kết hợp với việc xác định mức phổ biến của từ tách. Như vậy biến đầu vào của hàm này, bạn không được chuyển nó về dạng ký tự thường, mà chỉ được chuyển mã hóa dạng viết Hoa của nó về dạng hex phổ thông mà thôi (hàm pop_hex_upp_convert);
  • Hàm sau dùng đơn thuần dựa trên mức phổ biến, kết hợp với số lượng từ tối thiểu, số lượng từ tối thiểu của mỗi chuỗi tách
  • Hai hàm trên độc lập nhau, như 2 lớp lọc khác nhau để tăng cường độ chính xác, không bỏ sót trường hợp;
  • Hàm vn_upp_fix_sticky_big($name) nên dùng trước, sau đó bạn lọc tiếp bằng hàm vn_sep_fix_sticky_big($name)

Cú pháp mẫu:

if (vn_check_pop_fix_sticky_upp_big($name) { // tách dựa trên từ viết hoa                             
           vn_upp_fix_sticky_big($name);
}

Và:

if (vn_check_pop_fix_sticky_big($name)) { // tách kiểu khác
          vn_sep_fix_sticky_big($name);
}

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

Đối với việc chỉnh sửa chính tả tên người nói riêng, hàm này nên kết hợp thêm hàm sửa lỗi thả dấu trong tiếng Việt.


Mã hoàn chỉnh

Các giải thích thêm về các hàm có sẵn trong phần chú thích của mã.

<?php

/* 
 * Fix the error of words sticking together in Vietnamese names v1.2 (final)
 * MIT License
 * Nguyen Duc Anh - freehost.page
 */



////////////////////////////////////////////////////////////////////////////////
// các tên phổ biến, bao gồm 200 tên phổ biến của cả nam lẫn nữ, dữ liệu thô
function fore_name_rough() {
    $fore_name_rough = array("anh","vy","huy","khang","ngọc","bảo","nhi","hân","thư","minh","linh","phúc","như","ngân","an","khoa","đạt","phát","phương","khôi","nguyên","thảo","long","my","nam","quân","duy","trân","kiệt","quỳnh","nghi","trang","thịnh","hiếu","tuấn","trâm","hoàng","hưng","khánh","châu","nhân","thy","trúc","trí","tài","uyên","phong","yến","phú","tâm","tú","thành","ý","đức","dũng","lộc","tiên","lâm","mai","dương","hà","thanh","vinh","tiến","vân","ân","thiện","nghĩa","hào","hải","đăng","hương","quang","nhật","giang","bình","kim","quyên","trung","duyên","thắng","trinh","sang","tuyền","hằng","hùng","thái","vũ","sơn","cường","toàn","hiền","thuận","chi","lam","tường","khanh","ánh","danh","trường","kỳ","kiên","thiên","huyền","phước","tân","vi","hậu","việt","ly","thùy","khải","tín","quý","tùng","dung","trọng","nhung","phụng","lan","thi","mẫn","triết","luân","nga","mỹ","quốc","hòa","đan","thông","hoa","nhiên","khiêm","tuyết","xuân","kha","hạnh","thương","khuê","thúy","oanh","thủy","diệp","băng","lợi","vỹ","bách","mạnh","hồng","phi","văn","nhã","đông","đại","hiệp","loan","nhựt","thơ","phượng","tấn","mi","giàu","hy","đào","vương","nguyệt","tuệ","bích","công","hiển","diễm","kiều","khương","di","nguyễn","vĩ","doanh","quyền","trà","tiền","nhàn","liên","huỳnh","thắm","hảo","diệu","chương","thu","điền","gia","thọ","tính","triều","san","giao","triệu","chiến","huệ","hoàn","đình");

return $fore_name_rough;     
}



////////////////////////////////////////////////////////////////////////////////
// các họ phổ biến, bao gồm 100 họ phổ biến của cả nam lẫn nữ, dữ liệu thô
function sure_name_rough() {
    $sure_name_rough = array("nguyễn","trần","lê","phạm","huỳnh","võ","phan","trương","bùi","đặng","đỗ","ngô","vũ","hồ","hoàng","dương","đinh","đoàn","lâm","mai","trịnh","đào","cao","lý","hà","lưu","lương","châu","thái","tạ","tô","phùng","vương","văn","tăng","quách","lại","hứa","thạch","từ","diệp","chu","la","đàm","tống","giang","chung","triệu","tôn","kiều","trang","hồng","đồng","danh","lư","lữ","thân","kim","mã","bạch","liêu","tiêu","bành","dư","âu","khưu","sơn","tất","nghiêm","lục","phương","quan","mạc","vòng","lai","mạch","thiều","trà","đậu","lã","nhan","trình","ninh","vi","trầm","biện","hàng","chế","ôn","thi","nhâm","doãn","khổng","phù","ông","đường","viên","tào","cù");

return $sure_name_rough;    
}



////////////////////////////////////////////////////////////////////////////////
// các đệm phổ biến, bao gồm 200 đệm phổ biến của cả nam lẫn nữ, dữ liệu thô
function mid_name_rough() {
    $mid_name_rough = array("ngọc","thị","hoàng","minh","nguyễn","gia","thanh","lê","trần","quốc","bảo","anh","huỳnh","văn","thành","tấn","đức","tuấn","phương","phạm","quang","khánh","nhật","hồng","hữu","kim","vũ","đình","võ","duy","quỳnh","thiên","trọng","","đăng","phúc","xuân","trung","thái","hà","tiến","chí","hải","phan","mỹ","công","đặng","mai","hồ","như","huy","hoài","đỗ","dương","cao","phước","thế","thùy","lâm","thảo","trí","nguyên","trương","phú","việt","đoàn","yến","thụy","vĩnh","bá","mạnh","ngô","trường","tường","thiện","bùi","tuyết","nhã","phi","châu","thu","trúc","thúy","nam","đại","an","viết","tú","kiều","ánh","lý","bình","nhựt","kiến","bích","hiếu","trịnh","cẩm","khả","đào","vân","đinh","khải","tâm","lưu","hùng","chấn","lương","kỳ","triệu","khắc","đông","diệp","vương","ái","khôi","bội","thục","diệu","hương","uyên","cát","tùng","tuệ","long","vĩ","thủy","huệ","quý","sỹ","diễm","song","lan","huyền","linh","nhất","hạo","phát","đắc","hưng","vinh","quế","ngân","thuận","sơn","trang","nữ","trà","hoàn","danh","mẫn","uyển","tiểu","thạch","phùng","nguyệt","khang","tăng","hạnh","trâm","hiền","hiểu","tô","đan","nhân","sĩ","tố","doãn","khoa","hòa","thi","hào","triều","lệ","cảnh","phong","bách","quách","bửu","ý","lam","giang","hòang","thuỵ","toàn","vỹ","thư","phụng","tân","thuỳ","tôn","thất","tài","tạ","đạt","quân","chánh","tống","vy","tất");

return $mid_name_rough;    
}  




////////////////////////////////////////////////////////////////////////////////
// Kiểm tra tính phổ biến của từ dựa trên danh sách tên, họ đệm, phổ biến
// Đầu vào là một từ, nếu nó nằm trong danh sách phổ biến, kết quả là 1
function vn_pop_name_check($name) {
    $pop = 0; // giá trị khởi tạo là không phổ biến
    $fname = fore_name_rough(); // mảng tên
    $sname = sure_name_rough(); // mảng họ
    $mname = mid_name_rough(); // mảng đệm
                
    $namex = pop_hex_convert($name); // chuẩn hóa ký tự, và chuyển nó về ký tự thường
    if (in_array($namex,$fname) || in_array($namex,$sname) || in_array($namex,$mname)) {
        $pop = 1; // chỉ cần $name thuộc về một trong 3 mảng nghĩa là nó phổ biến
    }

return $pop;               
}



////////////////////////////////////////////////////////////////////////////////
function vn_check_pop_fix_sticky($name) { // kiểm tra trước xem một từ nào đó có cần tách hay không
        $rs = 0; // giả trị khởi tạo mặc định là không cần tách
        $pop_name = pop_hex_convert($name); // chuẩn hóa ký tự, và chuyển nó về ký tự thường
        $arr_pop_name = preg_split('//u', $pop_name, -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
        
        $count_ch = mb_strlen($pop_name, 'UTF-8'); // số lượng ký tự của từ cần kiềm tra
        
        // chạy vòng lặp, một từ có n ký tự, cần chạy ít nhất n-1 vòng lặp
            // ví dụ từ có 5 ký tự sẽ chạy 1-4, 2-3, 3-2, và 4-1, dĩ nhiên chúng ta không cần chạy vòng 0-5 và 5-0
        for ($i=1; $i<$count_ch; $i++) { 
            $str2="";$str3=""; // khởi tạo 2 mảng rỗng để chứa các ký tự tách từ từ ban đầu
                                                                         
            for ($c=0; $c<$i; $c++) {if (isset ($arr_pop_name[$c])) {$str2=$str2.$arr_pop_name[$c];}} // lấy chuỗi đầu, lệnh isset để chống tình trạng offset

            for ($d=$i; $d<$count_ch; $d++) {if (isset ($arr_pop_name[$d])) {$str3=$str3.$arr_pop_name[$d];}} // lấy chuỗi cuối
            
            if (vn_pop_name_check($str2) && vn_pop_name_check($str3) && vn_pop_name_check($pop_name)==0) { // điều kiện chặt khi cả 2 từ tách phải đều thuộc nhóm họ, têm, đệm, phổ biến
                    $rs=1; break; // nếu kết quả phù hợp thì đưa vào mảng và cắt vòng lặp
            } 
        }

return $rs;          
}



////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function vn_check_pop_fix_sticky_upp($name) { // kiểm tra trước xem một từ nào đó có cần tách hay không
    $rs = 0; // giả định là không  
    // dành cho việc tách viết hoa
    if (vn_check_pop_fix_sticky($name) && vn_count_upp($name) == 2) {
        $rs = 1;
    }
    
return $rs;    
}



////////////////////////////////////////////////////////////////////////////////
// Đếm số ký tự viết hoa trong một từ
function vn_count_upp($name) {
    $upp = vn_upp_letters(); // lấy mảng viết hoa từ tiếng Việt
    $upp_name = pop_hex_upp_convert($name); // chuẩn hóa dạng viết HOA của tên đầu vào
    $count_upp = 0;

    $sep_upp_name = preg_split('//u', $upp_name, -1, PREG_SPLIT_NO_EMPTY); // tách ký tự tên gốc
    foreach ($sep_upp_name as $char_upp_name) { // đếm ký tự viết hoa trong tên gốc
            if (in_array($char_upp_name, $upp)) {$count_upp++;}
    }

return $count_upp;  // trả về số lượng từ viết hoa      
}



////////////////////////////////////////////////////////////////////////////////
function vn_upp_fix_sticky($name) { // sửa lỗi dính tên dựa trên từ viết hoa
//$name là tên đầu vào dạng một từ, đã tách, chưa xác định đây là họ hay đệm hay tên
    $rs = $name; // dự phòng
    $low_name = pop_hex_convert($name); // tên dạng viết thường
    
    $upp = vn_upp_letters(); // lấy mảng viết hoa từ tiếng Việt 
    $upp_name = pop_hex_upp_convert($name); // chuẩn hóa dạng viết HOA của tên
    
    $pos_upp = 0; // tính vị trí từ viết hoa thứ 2 trong tên gốc nếu nó có
    $count_upp = 0; // đếm số lượng từ viết HOA
    
    $sp_upp_name = preg_split('//u', $upp_name, -1, PREG_SPLIT_NO_EMPTY); // tách ký tự tên gốc
    foreach ($sp_upp_name as $ch_upp_name) { // đếm ký tự viết hóa trong tên gốc
                                $pos_upp++; // vị trí của từ viết hoa
                                if (in_array($ch_upp_name, $upp)) {$count_upp++;}
                                if ($count_upp===2) {break;} // nếu điều kiện thỏa mãn ta lấy được vị trí của từ viết Hoa 
                        }
                        
    $frc = vn_foreign_check_low($low_name); // kiểm tra ký tự nước ngoài, nếu không có mới tách
    $pop_name = vn_pop_name_check($name); // kiểm tra xem bản thân từ cần tách đó có nằm trong nhóm phổ biến không, nếu không mới tách
    $true_count_upp = vn_count_upp($name); // số lượng từ viết HOA thực tế có trong từ
    
    if (($true_count_upp==2) && ($frc==0) && ($pop_name==0)) {
        $len = mb_strlen($upp_name, 'UTF-8'); // số lượng ký tự
        $rest_one = $pos_upp - 1; // tách từ ký tự viết hoa
        $rest_two = 1 + $len - $pos_upp;
        
        $upp_one = mb_substr($upp_name, 0, $rest_one, 'UTF-8'); // tách lấy từ thứ nhất
        $upp_two = mb_substr($upp_name, $rest_one, $rest_two, 'UTF-8'); // tách lấy từ thứ hai
        
        // cả hai tên tách phải có độ phổ biến thì mới có độ tin cậy để tách từ dính dựa trên ký tự hoa
        // đây là điều kiện quan trọng cần để ý
        if (vn_pop_name_check($upp_one) == 1 && vn_pop_name_check($upp_two) == 1) {$rs = $upp_one.' '.$upp_two;} // tách tên dính
    }

return $rs;    
}



////////////////////////////////////////////////////////////////////////////////
/// sửa lỗi dính tên dựa vào việc tách từ, yêu cầu 2 từ đó phải có trong danh sách họ tên phổ biến, hàm dành cho một tên
// số lượng ký tự từ cần lớn hơn 4, và mỗi từ tách ra cần có ít nhất 2 ký tự
function vn_sep_fix_sticky($name) {
    $rs = $name; // dự phòng
    $num_char = vn_num_char($name); // lấy số lượng ký tự
    $pop_name = pop_hex_convert($name); // chuẩn hóa ký tự, và chuyển nó về ký tự thường
    $arr_pop_name = preg_split('//u', $pop_name, -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
    $count_ch = mb_strlen($pop_name, 'UTF-8'); // số lượng ký tự của từ cần kiềm tra
    // chạy vòng lặp, một từ có n ký tự, cần chạy ít nhất n-1 vòng lặp
        // ví dụ từ có 5 ký tự sẽ chạy 1-4, 2-3, 3-2, và 4-1, dĩ nhiên chúng ta không cần chạy vòng 0-5 và 5-0
    for ($i=1; $i<$count_ch; $i++) { 
        $str2="";$str3=""; // khởi tạo 2 mảng rỗng để chứa các ký tự tách từ từ ban đầu
                                                                         
        for ($c=0; $c<$i; $c++) {if (isset ($arr_pop_name[$c])) {$str2=$str2.$arr_pop_name[$c];}} // lấy chuỗi đầu, lệnh isset để chống tình trạng offset

        for ($d=$i; $d<$count_ch; $d++) {if (isset ($arr_pop_name[$d])) {$str3=$str3.$arr_pop_name[$d];}} // lấy chuỗi cuối

        if (vn_pop_name_check($str2) && vn_pop_name_check($str3) && vn_pop_name_check($pop_name)==0 && $num_char > 4 & vn_num_char($str2) > 1 && vn_num_char($str3) > 1) { 
        // điều kiện chặt khi cả 2 từ tách phải đều thuộc nhóm họ, têm, đệm, phổ biến, và bản thân từ tách không nằm trong trạng thái phổ biến
            $rs=$str2." ".$str3; break;} // nếu kết quả phù hợp thì đưa vào mảng và cắt vòng lặp
    }

return $rs;                        
}



////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function vn_check_pop_fix_sticky_big($name) { // // kiểm tra trước xem một CỤM từ nào đó có cần tách hay không
    // dành cho từ chỉ có tối đa một ký tự viết hoa mỗi từ
    $rs = 0;  
    $namex = vn_rmv_wsp($name); // xóa bỏ khoảng trắng dư thừa
    $words = mb_split(' ', $namex); // tách từ
    
    foreach ($words as $word) {
        $rs = vn_check_pop_fix_sticky($word); // đưa vào mảng kiểm tra từng từ
        if ($rs == 1) {break;} // phát hiện ra một từ là đủ để tiến hành xử lý
    }
    
return  $rs;   
}

////////////////////////////////////////////////////////////////////////////////
// Sửa lỗi dính tên dựa trên tách từ, bao gồm nhiều từ
function vn_sep_fix_sticky_big($name) { // sửa lỗi dính tên của một tên hoàn chỉnh gồm cả họ, đệm và tên
    // dành cho từ chỉ có tối đa một ký tự viết hoa mỗi từ
    $rs = "";  
    $namex = vn_rmv_wsp($name); // xóa bỏ khoảng trắng dư thừa
    $words = mb_split(' ', $namex);
    
    foreach ($words as $word) {
        $rs = $rs.vn_sep_fix_sticky($word).' ';
    }

    if ($rs == "") {$rs = $name;} // dự phòng
    
return vn_rmv_wsp($rs); // loại bỏ khoảng trắng dư thừa trước và sau từ   
}



////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function vn_check_pop_fix_sticky_upp_big($name) { // // kiểm tra trước xem một CỤM từ nào đó có cần tách hay không
    // dành riêng cho ký tự có từ viết hoa
    $rs = 0;  
    $namex = vn_rmv_wsp($name); // xóa bỏ khoảng trắng dư thừa
    $words = mb_split(' ', $namex); // tách từ
    
    foreach ($words as $word) {
        $rs = vn_check_pop_fix_sticky_upp($word); // đưa vào mảng kiểm tra từng từ
        if ($rs == 1) {break;} // phát hiện ra một từ là đủ để tiến hành xử lý
    }
    
return  $rs;   
}

////////////////////////////////////////////////////////////////////////////////
// Sửa lỗi dính tên dựa trên các từ viết hoa đầu từ
function vn_upp_fix_sticky_big($name) { // sửa lỗi dính tên của một tên hoàn chỉnh gồm cả họ, đệm và tên
    $rs = ""; 
    $namex = vn_rmv_wsp($name); // xóa bỏ khoảng trắng dư thừa
    $words = mb_split(' ', $namex); // tách từ
    
    foreach ($words as $word) {
        $rs = $rs.vn_upp_fix_sticky($word).' ';
    }

    if ($rs == "") {$rs = $name;} // dự phòng
    
return vn_rmv_wsp($rs); // loại bỏ khoảng trắng dư thừa trước và sau từ  
}

/////////////////////////////////////////////////////////////////////// End code