Truyện Kiều

Đem Python tìm các cặp vần khác âm chính trong Truyện Kiều

Khi chuẩn bị làm xong công cụ tra cứu âm tiết theo vần của Dự án S, anh mới nghĩ là cần mở rộng kết quả tìm kiếm ra một chút. Hiện tại thì nếu bỏ qua âm đệm thì kết quả mới chỉ là những âm có vần giống nhau hoàn toàn. Nhưng thực tế thì không phải lúc nào cũng nhất thiết phải giống nhau 100% mới gọi là vần.

Ví dụ như trong những dòng đầu tiên của Truyện Kiều:

“Trăm năm trong cõi người ta,
Chữ tài chữ mệnh khéo là ghét nhau
Trải qua một cuộc bể dâu
Những điều trông thấy mà đau đớn lòng”…

Theo quy tắc thơ lục bát thì âm 6 của câu 6 vần với âm 6 của câu 8 liền sau và âm 8 của câu 8 sẽ vần với âm 6 của câu 6 liền sau. (Còn quy tắc âm 6 câu 6 vần với âm 4 câu 8 thì hình như Truyện Kiều không dùng) Ở đoạn trên thì “ta” với “là” chung vần “a”. Nhưng đến các câu tiếp theo thì lại là “au” – “âu” – “au”.

Rồi ở đoạn tiếp theo:

“Lạ gì bỉ sắc tư phong
Trời xanh quen thói má hồng đánh ghen.
Cảo thơm lần giở trước đèn,
Phong tình cổ lục còn truyền sử xanh.
Rằng, năm Gia Tĩnh triều Minh
Bốn phương phẳng lặng, hai kinh vững vàng.
Có nhà viên ngoại họ Vương,
Gia tư nghỉ cũng thường thường bậc trung.
Một trai con thứ rốt lòng,
Vương Quan là chữ nối dòng nho gia”…

Một đoạn 10 câu mà có hẳn 5 cặp vần không khớp hoàn toàn: ong – ông, en – yên, anh – inh, ang – ương, ung – ong.

Thế mà bảo đây là kiệt tác :v

Đùa thế thôi, chứ ai cũng biết là trong các cặp vần đấy thì âm cuối phải giống nhau và giữa hai âm chính có sự tương đồng nào đấy về vị trí hoặc phương thức cấu âm. Còn chi li cụ tỉ như nào thì anh cũng chịu.

Nhưng tóm lại là nếu những cặp vần như vậy được chấp nhận trong Truyện Kiều thì cũng có thể dùng ở bất kì đâu. Kiệt tác cơ mà, hehe. Hơn nữa độ dài của nó cũng rất lớn (3.254 câu, mà thấy bảo là không có câu nào trùng nhau) nên kì vọng là sẽ cho ra một danh sách khá dài.

Và danh sách những cặp vần này có thể đưa vào phần gợi ý thêm trong kết quả tìm kiếm âm theo vần ở Dự án S.

Mục đích là vậy.

Thế bây giờ mà ngồi dò từng câu trong tổng số 3.254 dòng để ra những trường hợp như vậy thì lâu quá. Mà có khi cũng có người làm như thế rồi đấy. Nhưng thôi, kệ họ, anh đi dùng Python cho nhanh.

Nói thêm về vần

Trước khi đi vào nội dung chính thì anh thấy phải nói rõ hơn quan điểm về vần trong trường hợp này. Trong cấu trúc âm tiết thì vần sẽ gồm âm đệm + âm chính + âm cuối. Nhưng trong thơ ca thì người ra bỏ qua âm đệm: Chữ tiên cùng với nguyên một vần. Tức là “yên”, “tiên”, “nguyên” đều chung vần “iên” (âm chính -iê-, âm cuối -n).

Dùng Python để thống kê các cặp vần không cùng nguyên âm

Giờ thì đã đến nội dung chính. Các thư viện anh sử dụng là:

import re, vos
from pprint import pprint

re thì đương nhiên rồi.

vos thì là một thư viện chuẩn hoá chữ viết (Vietnamese Orthography Standardization :p) do anh tự làm, trong này sẽ có các hàm vos.y2i() để chuẩn hoá i/y và vos.get_rhyme() để xác định vần của âm tiết.

pprint là tuỳ chọn.

Về dữ liệu đầu vào thì anh dùng đại một bản Kiều tìm trên Google. Nhà anh cũng có quyển của cụ Nguyễn Thạch Giang biên soạn, nhưng thời gian đâu mà gõ lại. Biết là ở những bản lấy từ Internet thì không đáng tin cậy, kiểu gì cũng có chỗ sai, nhưng cứ tạm thế đã. Bản này anh lưu vào tập tin kieu.txt rồi sơ chế:

with open('kieu.txt', 'r') as k:
    kieu_text = k.read()

# Remove line numbers
kieu_text = re.sub(r'\n\s?[0-9]+\.', '\n', kieu_text)

# Remove blank lines
kieu_text = re.sub('\n\n+', '\n', kieu_text)

# Remove punctuations, double white spaces, etc.
kieu_text = re.sub(r'\W+(\s)', '\\1', kieu_text)

# Convert to lower case
kieu_text = kieu_text.lower()

# I/Y standardization
kieu_text = vos.y2i(kieu_text)

Đại loại là văn bản sẽ chỉ còn thuần tuý các âm tiết để từ đó chuyển đổi thành một mảng (list) đa chiều:

KIEU = [line.split() for line in kieu_text.split('\n')]

Tiếp theo là kiểm tra độ dài từng câu (vì dữ liệu đầu vào có độ tin cậy thấp):

def check_length():

    print('Check for sentence length')
    
    for i, s in enumerate(KIEU):
        if ((i % 2) == 0 and len(s) != 6) or ((i % 2) != 0 and len(s) != 8):
            print('Invalid line {}: {}'.format(i+1, ' '.join(s)))


check_length()

Kiểm tra một lần thôi nên viết thành hàm để lần sau tắt đi cho tiện, kaka.

def make_pairs():

    pairs = {}

    for i in range(len(KIEU) - 1):
        if (i % 2) == 0:
            r6 = vos.get_rhyme(KIEU[i][5])
            r8 = vos.get_rhyme(KIEU[i+1][5])
            
        else:
            r8 = vos.get_rhyme(KIEU[i][7])
            r6 = vos.get_rhyme(KIEU[i+1][5])

        if r6 != r8:
            if r6 not in pairs:
                pairs[r6] = [r8]
            elif r8 not in pairs[r6]:
                pairs[r6].append(r8)

            if r8 not in pairs:
                pairs[r8] = [r6]
            elif r6 not in pairs[r8]:
                pairs[r8].append(r6)

    return pairs

pairs = make_pairs()
pprint(pairs)

Hàm vos.get_rhyme() sẽ đưa ra vần của một âm tiết, trong đó vần chỉ gồm âm chính và âm cuối như đã nói ở trên. Nếu âm 6 ở câu 6 (hoặc âm 8 ở câu 8) có vần không khớp 100% với âm 6 ở câu 8 liền sau (hoặc âm 6 ở câu 6 liền sau) thì ghi nhận thành một cặp và lưu ở dạng dict{key: [value,]}.

Nếu A vần với B, B vần với C thì A chưa chắc đã vần với C. Vì thế, với từng vần A, B, C thì anh sẽ cho là một key với giá trị là danh sách các vần tương ứng.

Đoạn mã trên sau hai giây thì cho ra kết quả như này. Tưởng là nhiều mà hoá ra rất nhiều:

{
    'ai': ['ơi', 'oi', 'ươi', 'ôi', 'uôi', 'ay', 'ao', 'ui'],
    'am': ['ươm'],
    'an': ['ơn', 'ang', 'ân', 'ên', 'iên'],
    'ang': ['ương', 'an', 'iêng', 'iên'],
    'anh': ['inh', 'ênh'],
    'ao': ['êu', 'ai', 'au', 'iêu'],
    'au': ['âu', 'ao'],
    'ay': ['ây', 'ai'],
    'e': ['ê', 'ia', 'i'],
    'em': ['êm', 'iêm', 'im'],
    'en': ['iên', 'ên', 'in', 'ơn'],
    'eo': ['iêu', 'êu', 'iu'],
    'i': ['ê', 'ia', 'e'],
    'ia': ['i', 'ê', 'e'],
    'im': ['em', 'êm'],
    'in': ['iên', 'ên', 'en', 'inh'],
    'inh': ['anh', 'ênh', 'in'],
    'iu': ['iêu', 'eo'],
    'iêm': ['em', 'êm'],
    'iên': ['en', 'ên', 'in', 'êm', 'ơn', 'an'],
    'iêng': ['ênh', 'ang'],
    'iêu': ['eo', 'êu', 'iu', 'ao'],
    'o': ['ô', 'u', 'ua'],
    'oi': ['ai', 'ươi', 'ơi', 'ôi', 'uôi', 'ui'],
    'on': ['uôn', 'ôn', 'ơn', 'un'],
    'ong': ['ông', 'ung'],
    'u': ['o', 'ô', 'âu'],
    'ua': ['ô', 'o'],
    'ui': ['ơi', 'ôi', 'ai', 'ươi', 'oi'],
    'un': ['on'],
    'ung': ['ong', 'ông'],
    'uôi': ['ai', 'oi', 'ôi', 'ơi', 'ươi'],
    'uôm': ['ôm'],
    'uôn': ['ôn', 'on'],
    'uông': ['ương'],
    'âm': ['ăm'],
    'ân': ['ăn', 'an'],
    'âng': ['ăng', 'ưng'],
    'âu': ['au', 'u', 'ô'],
    'ây': ['ay'],
    'ê': ['e', 'i', 'ia'],
    'êm': ['ên', 'em', 'im', 'iên', 'iêm'],
    'ên': ['êm', 'iên', 'en', 'in', 'an'],
    'ênh': ['anh', 'iêng', 'inh'],
    'êu': ['eo', 'iêu', 'ao'],
    'ô': ['o', 'ua', 'u', 'âu'],
    'ôi': ['ơi', 'ai', 'ui', 'ươi', 'oi', 'uôi'],
    'ôm': ['uôm'],
    'ôn': ['uôn', 'on'],
    'ông': ['ong', 'ung'],
    'ăm': ['âm'],
    'ăn': ['ân'],
    'ăng': ['ưng', 'âng'],
    'ơ': ['ưa', 'ư'],
    'ơi': ['ươi', 'ai', 'ui', 'ôi', 'oi', 'uôi'],
    'ơm': ['ươm'],
    'ơn': ['an', 'en', 'iên', 'on'],
    'ư': ['ưa', 'ơ'],
    'ưa': ['ơ', 'ư'],
    'ưng': ['ăng', 'âng'],
    'ươi': ['ơi', 'ai', 'oi', 'ôi', 'ui', 'uôi'],
    'ươm': ['am', 'ơm'],
    'ương': ['ang', 'uông']
}

Thực ra ban đầu thì không hoàn toàn như này mà có hai cặp có âm cuối khác xa nhau. Anh thử kiểm tra thì hoá ra là dữ liệu đầu vào không chính xác. Ngoài ra cũng có một số cặp khác bị “lạc vận”: ao – ai, âu – u, âu – ô.

Cũng vì dữ liệu đầu vào không tin cậy nên anh sẽ phải làm thống kê để lọc ra những trường hợp nghi ngờ.

def rhyme_pairs_stats(pairs : dict, rhyme : str, show_lines = 1, show_stats = True):

    """
    pairs : dict
        {'rhyme': ['other', 'rhymes']
    rhyme: str
        Rhyme to compare
    show_lines : [0, 1, 'all']
        0: Hide matched line
        1: Show first matched line only
        'all': Show all matched lines
    show_stats : bool
        Show statistics or not
    """
    # Check for variables
    if rhyme not in pairs:
        print(rhyme, 'does not exist in given pairs')
        return False

    found = {}

    print('Finding by rhyme:', rhyme)

    for i in range(len(KIEU) - 1):
        
        if (i % 2) == 0:
            r6 = get_rhyme(KIEU[i][5])
            r8 = get_rhyme(KIEU[i+1][5])
            
        else:
            r8 = get_rhyme(KIEU[i][7])
            r6 = get_rhyme(KIEU[i+1][5])

        if (r6 == rhyme and r8 in pairs[rhyme]) or (r8 == rhyme and r6 in pairs[rhyme]):
            if r6 == rhyme:
                matched = r8
            else:
                matched = r6

            if matched not in found:
                found[matched] = 1
            else:
                found[matched] = found[matched] + 1

            # Show the lines if set
            if show_lines == found[matched] or show_lines == 'all':
                print(i+1, rhyme, '-', matched, '\n   ', ' '.join(KIEU[i]), '\n   ', ' '.join(KIEU[i+1]))


    if len(found) > 0:
        if show_stats == True:
            print('-' * 25, '\nStats:')
            for k in found:
                print(k, '-', found[k])

        return found

    else:
        print('Not found!!!')

Anh còn định kiểm tra cả âm cuối xem có trùng nhau không. Nhưng như thế hơi mất công nên lại thôi.

Ví dụ để kiểm tra vần “ươi”:

rhyme_pairs_stats(pairs, 'ươi')

Nó sẽ in ra kết quả sau (mỗi cặp vần chỉ hiển thị mẫu một lần):

18 ươi - ơi 
    mỗi người một vẻ mười phân vẹn mười 
    vân xem trang trọng khác vời
104 ươi - ai 
    sầu tuôn đứt nối châu sa vắn dài 
    vân rằng chị cũng nực cười
301 ươi - oi 
    tan sương đã thấy bóng người 
    quanh tường ra ý tìm tòi ngẩn ngơ
400 ươi - ôi 
    mặt khen nét bút càng nhìn càng tươi 
    sinh rằng phác hoạ vừa rồi
2604 ươi - ui 
    trăm phần nào có phần nào phần tươi 
    đành thân cát lấp sóng vùi
3018 ươi - uôi 
    khóc than mình kể sự tình đầu đuôi 
    từ con lưu lạc quê người
------------------------- 
Stats:
ơi - 78
ai - 49
oi - 6
ôi - 21
ui - 1
uôi - 1

Như vậy, có những cặp chỉ hiệp vần 01 lần. Đối với các trường hợp chỉ có một hai lần hiệp vần thì anh sẽ lôi quyển Truyện Kiều xấu xấu bẩn bẩn mua gần 30 năm trước ra đối chiếu. Đấy là rảnh thì anh làm thế, chứ thời gian viết cái bài này còn lâu gấp đôi thời gian viết đoạn Python trên.

Kết quả tạm thời

Chắc phải lâu lâu nữa anh mới bổ sung tính năng mở rộng kết quả tìm vần. Còn hiện tại như thế cũng là tốt rồi. Nếu ai muốn tham khảo thì có thể xem tạm danh sách này (đã loại những trường hợp chỉ hiệp vần duy nhất một lần):

ai
    oi - 4
    ôi - 15
    ơi - 74
    ui - 3
    ươi - 49
an
    ang - 4
    ân - 2
    iên - 2
    ơn - 5
ang
    an - 4
    iêng - 2
    ương - 126
anh
    ênh - 7
    inh - 97
ao
    au - 2
    êu - 2
au
    ao - 2
    âu - 84
ay
    ây - 125
ăm
    âm - 12
ăn
    ân - 11
ăng
    âng - 3
    ưng - 10
âm
    ăm - 12
ân
    an - 2
    ăn - 11
âng
    ăng - 3
âu
    au - 84
ây
    ay - 125
e
    ê - 21
    i - 5
    ia - 4
em
    êm - 2
    iêm - 2
    im - 3
en
    ên - 7
    iên - 19
    in - 2
eo
    iêu - 14
ê
    e - 21
    i - 21
    ia - 10
êm
    em - 2
    im - 5
ên
    en - 7
    iên - 38
    in - 2
ênh
    anh - 7
    inh - 6
êu
    ao - 2
    iêu - 6
i
    e - 5
    ê - 21
    ia - 17
ia
    e - 4
    ê - 10
    i - 17
iêm
    em - 2
iên
    an - 2
    en - 19
    ên - 38
    in - 14
iêng
    ang - 2
iêu
    eo - 14
    êu - 6
    iu - 8
im
    em - 3
    êm - 5
in
    en - 2
    ên - 2
    iên - 14
inh
    anh - 97
    ênh - 6
iu
    iêu - 8
o
    ô - 5
    u - 4
oi
    ai - 4
    ôi - 7
    ơi - 6
    uôi - 2
    ươi - 6
on
    ôn - 7
    uôn - 5
ong
    ông - 92
    ung - 27
ô
    o - 5
    u - 2
    ua - 5
ôi
    ai - 15
    oi - 7
    ơi - 39
    ui - 6
    uôi - 5
    ươi - 21
ôn
    on - 7
    uôn - 3
ông
    ong - 92
    ung - 34
ơ
    ư - 6
    ưa - 63
ơi
    ai - 74
    oi - 6
    ôi - 39
    ui - 5
    uôi - 5
    ươi - 78
ơn
    an - 5
u
    o - 4
    ô - 2
ua
    ô - 5
ui
    ai - 3
    ôi - 6
    ơi - 5
ung
    ong - 27
    ông - 34
uôi
    oi - 2
    ôi - 5
    ơi - 5
uôn
    on - 5
    ôn - 3
uông
    ương - 2
ư
    ơ - 6
    ưa - 6
ưa
    ơ - 63
    ư - 6
ưng
    ăng - 10
ươi
    ai - 49
    oi - 6
    ôi - 21
    ơi - 78
ương
    ang - 126
    uông - 2

À, danh sách trên anh sắp xếp theo ABC bằng công cụ Sắp xếp ở Dự án S với tuỳ chọn là Danh sách nhiều cấp. Tiện phết các mẹ ạ.