Dùng Python viết hàm xử lý dữ liệu dưới tầng database cho PostgreSQL

Có một bận, tôi ôm trong tay một bộ CSDL của website nọ, với nhiều thông tin danh tính người thật. Để tránh cho dữ liệu danh tính bị lộ, hưởng ứng tinh thần của Luật An Ninh Mạng, tôi quyết định phải làm xáo trộn dữ liệu đó để nó không còn phản ánh danh tính thật nữa. Cụ thể là tôi sẽ ghi thêm vài kí tự bừa bãi vào cột email, cho nó thành email "xạo" hết.

Nói tới nhu cầu này thì cách dễ nhất là viết đoạn code cho nó chạy một vòng lặp, lặp qua các dòng của bảng dữ liệu, tại mỗi dòng lấy ra cột email, ghi nội dung mới vào rồi lưu lại. Cách đó dễ, nhưng hơi cơ bắp, không tinh tế, sẽ chậm khi bảng dữ liệu hơi lớn. Tôi quyết định thử phương án tạo hàm tùy thêm cho hệ CSDL đó, để có thể sửa tất cả trong một câu truy vấn (query) duy nhất, ví dụ:

UPDATE web_users SET email = my_func(email)

Dùng qua cơ sở dữ liệu, các bạn chắc cũng biết trong câu truy vấn, thỉnh thoảng ta bắt gặp lời gọi hàm. Ví dụ phổ biến nhất là hàm COUNT, ví dụ:

SELECT COUNT(*) FROM student WHERE country = 'VN'

Ngoài những hàm có sẵn như thế, các hệ CSDL vẫn cho phép ta định nghĩa thêm hàm tùy ý, và viết theo cú pháp phương ngữ SQL của hệ CSDL đó, ví dụ trong MySQL:

DELIMITER $$

CREATE FUNCTION CustomerLevel(
	credit DECIMAL(10,2)
) 
RETURNS VARCHAR(20)
DETERMINISTIC
BEGIN
    DECLARE customerLevel VARCHAR(20);

    IF credit > 50000 THEN
		SET customerLevel = 'PLATINUM';
    ELSEIF (credit >= 50000 AND 
			credit <= 10000) THEN
        SET customerLevel = 'GOLD';
    ELSEIF credit < 10000 THEN
        SET customerLevel = 'SILVER';
    END IF;
	-- return the customer level
	RETURN (customerLevel);
END$$
DELIMITER ;

Hơi dài dòng và khó đọc, đúng không, nếu thế thì "tinh tế" qué gì? Thực ra, muốn tinh tế thì nên dùng PostgreSQL, vì PostgreSQL cho phép bạn định nghĩa hàm tùy tạo bằng ngôn ngữ Python luôn (thông qua tính năng PL/Python). Không chỉ là Python ở mặt cú pháp mà còn được import các thư viện Python hẳn hoi! Chúng ta cùng bắt đầu nhé.

Đụng vào tầng database là một việc hơi nguy hiểm, nên ta cần cẩn thận. Trước tiên, hãy cứ viết ra như một đoạn code Python thuần (file *.py), test tiếc cho cẩn thận rồi hãy chuyển vào trong PostgreSQL. Về ý tưởng xáo trộn email thì thuật toán như sau:

  • Đầu tiên, tách địa chỉ email làm hai phần, trước và sau dấu "@". Ví dụ "ng.hong.quan@gmail.com" thì tách thành "ng.hong.quan" và "gmail.com".
  • Phần tên miền không cần đụng đến. Tôi sẽ thêm kí tự ngẫu nhiên vào phần tên đăng nhập (username). Thêm một kí tự vào đằng trước, một kí tự vào cuối và một kí tự chen vào giữa, vị trí ngẫu nhiên. Ví dụ, thêm "z" vào trước, "x" vào sau, thành "zng.hong.quanz", rồi thêm "y" vào giữa, thành "zng.hyong.quanx".

Bây giờ viết ra thành file tools.py:

import string
import random


def scramble_email(email: str) -> str:
    # No need to handle the case that "@" is missing, because
    # email address was validated before.
    head, tail = email.split('@')
    # No need to handle the case that email is empty.

    # Pick 3 random letters
    c1, c2, c3 = random.sample(string.ascii_lowercase + string.digits, 3)
    # Choose a random middle position
    i = random.randint(1, max(1, len(head) - 2))

    # Put together
    return f'{c1}{head[:i]}{c2}{head[i:]}{c3}@{tail}'

Trong bộ dữ liệu của tôi, cột email là bắt buộc, và dữ liệu được kiểm tra tính hợp lệ trước khi ghi vào, nên hàm scramble_email không cần kiểm tra những trường hợp lắt léo như thiếu dấu "@", chuỗi rỗng.

Đôi lời giải thích nếu bạn đọc là người non kinh nghiệm về Python:

  • email.split('@') sẽ ngắt thành hai chuỗi con, ví dụ 'quan''agriconnect.vn'. Ta gán hai chuỗi con này cho hai biến headtail.

  • string.ascii_lowercase + string.digits cho ra một chuỗi gồm từ 'a' đến 'z' và từ '0' đến '9'.

  • random.sample(string.ascii_lowercase + string.digits, 3) là từ chuỗi trên, bốc ra ba kí tự ngẫu nhiên. Sau khi bốc ra, ta gán cho ba biến c1, c2, c3.

  • Khi lấy vị trí giữa ngẫu nhiên, ta chỉ lấy vị trí từ thứ hai (chỉ số 1) đến kế cuối (chỉ số len(head) - 2). Ta phải lường tình huống chuỗi chỉ có một kí tự, khi đó sẽ lấy vị trí cuối luôn. Đó là lí do có thêm max(1, ...) vào để "thòng".

  • Sau khi có vị trí ngăn giữa i rồi thì ta dùng cú pháp "slice" để cắt head ra thành hai khúc: head[:i]head[i:]

Mở console của Python lên, import và cho chạy thử với vài dữ liệu:

test_pure_function

Trông ổn rồi, giờ thì ta mở database lên, cho phép nó sử dụng ngôn ngữ Python:

activate

Lưu ý rằng, mặc dù extension plpython là extension gốc, được hỗ trợ chính thức từ PostgreSQL nhưng trong các bản phân phối Linux (Ubuntu, Fedora), người bảo trì lại tách nó ra gói riêng. Ta cần cài gói đó trước, ví dụ trong Ubuntu là:

sudo apt install postgresql-plpython3-12

Sau đó cấu hình cho PostgreSQL luôn nạp nó lên, bằng cách sửa file /etc/postgresql/12/main/postgresql.conf, tìm dòng "shared_preload_libraries" và thêm "plpython3" vào, ví dụ:

shared_preload_libraries = 'plpython3' 

Nhớ khởi động lại PostgreSQL sau khi sửa cấu hình:

sudo systemctl restart postgresql

Quay lại bước vừa nãy, sau khi chạy câu "CREATE EXTENSION..." thì ta bắt đầu định nghĩa hàm, chép code Python vừa nãy vào:

CREATE FUNCTION scramble_email(email text) 
  RETURNS text 
  AS $$ 
   
  import string 
  import random 
   
  head, tail = email.split('@') 
  c1, c2, c3 = random.sample(string.ascii_lowercase + string.digits, 3) 
  # Choose a random middle position 
  i = random.randint(1, max(1, len(head) - 2)) 
  return f'{c1}{head[:i]}{c2}{head[i:]}{c3}@{tail}' 
  
$$ LANGUAGE plpython3u; 

define_function

Áp dụng nó vào câu query UPDATE, thi triển võ công "đấm một phát chết luôn":

query_update

Vậy là nhiệm vụ đã hoàn thành. Ta thấy PostgreSQL và Python quả là đôi bạn thân, hỗ trợ nhau rất tốt (ngay cả các hệ CSDL "đại gia" như MS SQL, Oracle còn không có tính năng này). Các tấm hình trên còn cho thấy ngay cả code Python trong PostgreSQL (PL/Python) cũng được tô màu theo đúng cú pháp. Thực ra việc tô màu có được là nhờ một công cụ trong hệ sinh thái Python: pgcli. Tuy nhiên, có một lưu ý nhỏ: Khi sử dụng PL/Python, chỉ superuser mới có quyền tạo hàm. Đây là giới hạn PostgreSQL đặt ra vì Python không có cơ chế tự giới hạn quyền, khiến code PL/Python có thể làm những trò quá mạnh bạo, nguy cơ trở thành "phá hoại" nếu rơi vào tay "gà mờ".

-- Cập nhật ---

Theo dõi bước chân xa hơn trong việc cải thiện tốc độ hàm xử lý này: Viết hàm thêm cho PostgreSQL: Chú voi bay.

Website này sử dụng Disqus cho comment. Nếu bạn không thấy các comment dưới đây, có lẽ Disqus đang bị chặn. Bạn cần đổi DNS.
comments powered by Disqus