Bạn có bao giờ tò mò là trong quá trình viết blog xem là tần suất sử dụng từ của mình như thế nào không? Bạn viết gì nhiều nhất? Bài viết này sẽ hướng dẫn các bạn cách dùng kiến thức data science để trích xuất và xử lý dữ liệu bài viết trên wordpress. Sau đó chúng ta sẽ vẽ 3 biểu đồ: [bar chart] biểu thị tần suất các từ xuất hiện nhiều nhất trên blog, [word cloud] để visualize chúng thành tạo một đám mây chữ và [histogram] để xem sự phân bố của tần suất các chữ được sử dụng. Ngôn ngữ chúng ta sử dụng là Python, và sẽ có một số bước xử lý ngôn ngữ tự nhiên (NPL).
Bước 1: Dữ liệu
Đầu tiên, chúng ta cần lấy dữ liệu đã. Blog của mình mở hàng từ năm 2014, cho đến nay mình đã viết được ngót nghét 8 năm rồi với khoảng gần 300 bài viết nên dữ liệu cũng có khá nhiều. Đầu tiên, giống như ở bài viết [Cách chạy trốn/backup khỏi WordPress.com], các bạn vào trang wp-admin của mình và chọn:
Tools > Export > Posts
Và download về một file .xml. Bên trong đó chứa đầy đủ toàn bộ nội dung bạn viết. Tuy nhiên, bước quan trọng hơn cả là làm sạch dữ liệu thì mới phân tích được.
Các library sẽ dùng trong bài:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import json
import bleach
Bước 2: Clean
Ở đây có 2 nhiệm vụ chính: (1) là convert định dạng xml sang json thì dễ làm việc hơn với pandas, (2) là chúng ta phải khử đi các kí tự không cần thiết (bởi vì file content trong xml được wordpress lưu dưới dạng html nên có rất nhiều tags), chỉ lấy về chữ thôi.
(1) Để convert từ xml sang json, chúng ta dùng thư viện pandas và json. Các bạn dùng lệnh sau:
import pandas as pd
import pandas_read_xml as pdx
import json
#convert xml to json
file = 'xmlfilepath'
data = pdx.read_xml(file, encoding='utf-8')
result = data.to_json()
parsed = json.loads(result)
Vì chúng ta dùng tiếng Việt nên các bạn nhớ chọn encoding ‘utf-8’. Sau khi có được file json (tên là parsed) rồi thì các bạn cần tìm xem là mục content nằm ở cây nào của file json. Để làm điều đó dễ dàng hơn, các bạn có thể upload file json của mình vào trang: [jsonviewer]. Sau khi xác định xong thì chuẩn hoá dữ liệu:
#normalize results
df = pd.json_normalize(parsed['rss']['0']['channel']['item'])
File content của mình nằm ở rss -> 0 -> channel -> item. Chắc của các bạn cũng vậy đó.
(2) Tiếp theo, quan trọng hơn cả là clean dữ liệu. Nếu các bạn print dataframe ra thì có thể sẽ thấy như sau. Mỗi hàng tương ứng với 1 post.

Phần nội dung của chúng ta thực chất nằm ở cột content:encoded. Nhưng các bạn sẽ thấy là ở cột này có rất nhiều giá trị None (nội dung rỗng), khiến cho số lượng post (rows) lên tận 814. Chúng ta phải loại bỏ chúng ra. Cái này thì đơn giản:
df.dropna(subset=['content:encoded'], axis=0, inplace=True)
Kết quả đã được thanh lọc hơn:

Vấn đề thứ hai như các bạn thấy mình đã khoanh đỏ, đó là nó xuất hiện một số kí tự lạ nằm trong tag < >. Mấy cái này không giúp ích gì cho công cuộc phân tích của chúng ta nên cũng cần phải loại bỏ. Để loại bỏ hết HTML, CSS tags ra khỏi dữ liệu, chúng ta dùng library bleach và regex. Ai chưa cài chúng thì vào shelll/terminal và cài đặt như sau:
pip install bleach
pip install regex
Để minh hoạ mục tiêu cần làm, các bạn có thể coi một cái content chưa qua xử lý (mình lấy bài viết gần đây nhất). Rất nhiều thứ linh tinh cần bỏ, ví dụ như link url, hoặc các tags của wordpress.
Quá trình loại bỏ được thực hiện qua các dòng lệnh sau:
# clear all html and css tags with bleach
df['content:encoded'] = df['content:encoded'].apply(lambda x: bleach.clean(x, tags=[], attributes=[], protocols=[], strip_comments=True, strip=True))
#remove '\n', '\t' and '\td' from the text
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'\n|\t|\r', ' ', x))
#remove all 'caption'
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'caption', '', x))
#remove all 'figure'
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'figure', '', x))
#remove all words containing 'align'
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'align', '', x))
#remove all words containing 'center'
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'center', '', x))
#remove all words containing 'class'
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'class', '', x))
#remove all words containing 'style'
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'style', '', x))
#remove all width
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'width', '', x))
#remove all aligncenter
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'aligncenter', '', x))
#remove all id
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'id', '', x))
#remove all nbsp
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'nbsp', '', x))
#remove all http
df['content:encoded'] = df['content:encoded'].apply(lambda x: re.sub(r'http\S+', '', x))
Một số phần mình buộc phải làm khá thủ công, nhưng mà chấp nhận vậy. Kết quả như thế này:


Nhìn sạch hơn rất nhiều. Bây giờ chúng ta sẽ dùng dữ liệu đã được làm sạch để làm nhiều điều thú vị. Nhưng trước hết…
Bước 3: Xử lý ngôn ngữ tự nhiên (NPL)
Ở phần trên thì mới tách được những thứ không cần thiết thôi. Ở phần tiếp theo thì chúng ta cần tách lấy các từ trong các câu trên ra. Ở đây thì chúng ta sẽ dùng library [underthesea] chuyên dụng để xử lý tiếng Việt.
from underthesea import word_tokenize
df['split'] = df['content:encoded'].apply(word_tokenize)
#convert all split words to lowercase
df['split'] = df['split'].apply(lambda x: [i.lower() for i in x])
Ở đây thì mình sẽ tạo thêm 1 cột split chứa từ đã được tách ra. Chưa hết, chúng ta cần loại bỏ dấu câu và chữ số nữa. Phần loại bỏ dấu câu khá phức tạp vì tiếng Việt là unicode và cần dấu cách giữa các từ. Mình đã thử nhiều cách nhưng liệt kê chay các dấu cần bỏ vẫn cho kết quả vừa ý nhất. Nếu bạn nào biết code nào tối ưu hơn thì comment nhé.
#convert all split to lowercase
df['split'] = df['split'].apply(lambda x: [i.lower() for i in x])
#remove all numbers
df['split'] = df['split'].apply(lambda x: [i for i in x if not i.isdigit()])
#remove all brackets, colons, commas, dots, exclamation marks, question marks, semicolons, slashes, and underscores
df['split'] = df['split'].apply(lambda x: [i for i in x if not i in ['[ =','(','\"','\'', ')', ':', ',', '.', '!', '?', ';', '/','-','[',']', '_', '“', '”', '‘', '’','...','=','+','*','%','$','#','@','^','&','|','~','`','<','>','\\','{','}']])
#remove all commas and dots
df['split'] = df['split'].apply(lambda x: [i.replace('.', '') for i in x])
#remove all space at the beginning and the end
df['split'] = df['split'].apply(lambda x: [i.strip() for i in x])
#remove beginning with [ =
df['split'] = df['split'].apply(lambda x: [i.replace('[ =', '') for i in x])
#remove all blanks
df['split'] = df['split'].apply(lambda x: [i for i in x if i != ''])
Tại sao phải đổi hết sang lower case, bởi vì sẽ tiện hơn khi lọc ra các từ không phải stop words (ví dụ như: “như”, “nhưng”, v.v..).
Tiếp theo, chúng ta sẽ loại bỏ tiếp những stop words. Vì trong bài của mình có cả tiếng Anh và tiếng Việt, nên mình sẽ lấy 2 lists chứa stop words của cả 2 tiếng và lọc theo list này. Cái stop list tiếng Việt mình lấy từ đây [vietnamese-stopwords], nhưng sau khi vẽ biểu đồ phát hiện ra là một số từ phổ biến như “người ta”, “mặc dù”, “ta” vẫn chưa được lọc đi. Vì thế mình đã fork và add thêm.
import collections
word_list = []
for i in range(0, df.shape[0]):
word_list.extend(df.iloc[i]['split'])
#filter stop words
stop_words_vn = pd.read_csv('https://raw.githubusercontent.com/thanhqtran/vietnamese-stopwords/master/vietnamese-stopwords.txt', header=None)
stop_words_en = pd.read_csv('https://raw.githubusercontent.com/stopwords-iso/stopwords-en/master/stopwords-en.txt', header=None)
stop_words_en_list = stop_words_en[0].tolist()
stop_words_vn_list = stop_words_vn[0].tolist()
stop_words = stop_words_en_list + stop_words_vn_list
# remove stop words
word_list_no_stop = [word for word in word_list if word not in stop_words]
Tiếp theo, mình sẽ tạo một dữ liệu con chứa top 500 từ được sử dụng nhiều nhất và sort theo thứ tự giảm dần.
# collector
word_freq = collections.Counter(word_list_no_stop)
# collect top 500 words
word_freq_hist = word_freq.most_common(500)
# make a dataframe
word_freq_df = pd.DataFrame(word_freq_hist, columns=['word', 'freq'])
# sort the dataframe
word_freq_df = word_freq_df.sort_values(by=['freq'], ascending=False)
Bước khó khăn nhất đã qua, bây giờ chúng ta sẽ vẽ biểu đồ minh hoạ trực quan.
Bước 4: Visualization
Biểu đồ cột giảm dần
Chú ý là các bạn cần có câu lệnh cuối để biểu đồ đi theo dạng từ cao xuống thấp.
# make a bar chart
import matplotlib.pyplot as plt
top50 = word_freq_df.head(50)
plt.figure(figsize=(7,15))
plt.barh(top50['word'], top50['freq'])
plt.xticks(rotation=90, fontsize=15)
plt.yticks(fontsize=15)
plt.xlabel('Frequency', fontsize=15)
plt.ylabel('Word', fontsize=15)
plt.title('nipponkiyoshi.com (top 50 most used words)', fontsize=15)
plt.gca().invert_yaxis()

Blog về Nhật Bản và ngôn ngữ nên biểu đồ này phản ánh cũng khá chính xác đấy chứ. Kinh tế các thứ cũng đứng cao.

Word cloud
Tiếp theo, mình tạo một đám mây các chữ theo tần suất. Chữ nào xuất hiện càng nhiều thì chữ đó càng to. Ở đây, các bạn sẽ dùng thư viện wordcloud.
import wordcloud
wc = wordcloud.WordCloud(background_color='white', width=500, height=300, max_words=250, collocations=False, max_font_size=100, random_state=30)
wc=wc.generate(" ".join(word_list_no_stop).lower())
plt.figure(figsize=(15,10))
plt.imshow(wc, interpolation='bilinear')
plt.axis("off")

Nhìn trực quan hơn hẳn.
Histogram
Cuối cùng, mình sẽ vẽ biểu đồ histogram để xem xem là viết lách có được phong phú hay không.
from matplotlib.ticker import PercentFormatter
plt.figure(figsize=(8,10))
freq_list = list(word_freq_df['freq'])
plt.hist(freq_list, weights=np.ones(len(freq_list)) / len(freq_list), bins=50, facecolor='blue', edgecolor='black', alpha=0.5)
plt.xlabel('Frequency', fontsize=15)
plt.ylabel('Percent of Words with that frequency', fontsize=15)
plt.title('Frequency Distribution', fontsize=15)
plt.gca().yaxis.set_major_formatter(PercentFormatter(1))
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
Ở đây chú ý là cái dataframe word_freq_df là một dictionary. Chúng ta chỉ cần lấy values ‘freq’ của nó là vẽ được histogram rồi. Ngoài ra thì chúng ta không dùng density nhé.

Như vậy có thể thấy có đến gần 50% các từ sử dụng với tần suất khoảng 30 lần gì đó. Các từ mình dùng với tần suất cao thì chỉ chiếm rất rất nhỏ. Chứng tỏ dùng từ cũng khá đa dạng đấy chứ.
Đọc thêm
Cách trích xuất dữ liệu WordPress Stats bằng Python
Viết chi tiết dễ hiểu lắm Thanh Trần. Ông lấy động lực đâu để tự mò rồi clean hết mớ tag thế =)).
Rồi hóa ra content Thanh viết thì toàn về thần thánh đạo giáo à =)))
ThíchĐã thích bởi 1 người
Test thử cả buổi chiều mới ra dc code để clean đống tag đấy. Cơ mà tôi thấy khả năng tách từ của underthesea cũng chưa được tốt lắm. Tôi cũng mò ra được 1 cái model chạy ML dựa trên underthesea nhưng không biết xài :p
Content viết nguyên một series về thần đạo mà nên tần suất cao là đúng rồi 🙂
ThíchThích
Có đề xuất thêm project nào không? Có gì thú vị kêu tôi nhé, cũng muốn mày mò làm thử.
ThíchThích
Tôi đọc thì hiểu mấy cái ông làm. Cơ mà bảo tự tay làm thì lười vcl .
Thanh thử làm mấy cái gì mà ra insight của Nhật bản hoặc của trường đang học xem, kiểu như yếu tố ảnh hưởng đến số sinh viên nước ngoài học ở Sendai chẳng hạn =)))
Nó ra được insight hay ho thì chắc những ng non-tech dễ đọc hơn
ThíchThích
Micro data như thế thì tôi chưa tìm.
Cơ mà data macro thì Nhật có nhiều lắm, ví dụ như số học sinh lên đại học, số sinh viên lên cao học, lương trung bình và lương bắt đầu theo trình độ học vấn, v.v.. từ tận năm 1955 cơ.
Chỉ việc vẽ lên thôi, chứ cũng không cần phải xử lý nhiều.
ThíchThích
Ở đây nè. Data clean rồi, chỉ việc vẽ :))
https://rihe.hiroshima-u.ac.jp/en/statistics/synthesis/
ThíchThích
Bài hay thì like đi 🙂
ThíchThích