Phát hiện ngôn ngữ lập trình từ một đoạn mã


115

Cách tốt nhất để phát hiện ngôn ngữ lập trình nào được sử dụng trong một đoạn mã là gì?


1
Trên thực tế, có vô số ngôn ngữ ngoài kia ... bạn có muốn phát hiện BẤT KỲ ngôn ngữ nào trong số đó không? Hay chúng ta chỉ đang nói những thứ phổ biến?
Spencer Ruport

Chỉ là những cái phổ biến (C / C ++, C #, Java, Pascal, Python, VB.NET. PHP, JavaScript và có thể là Haskell).
João Matos

12
Vâng, Haskell không thể nổi tiếng vì tôi chưa bao giờ nghe nói về nó. ;-)
Trang Stephanie

22
Bạn có thể không biết nhiều về ngôn ngữ lập trình nếu bạn chưa nghe nói về Haskell.
Akhorus

4
Có dịch vụ trực tuyến này mà làm nó: algorithmia.com/algorithms/PetiteProgrammer/...
Benny Neugebauer

Câu trả lời:


99

Tôi nghĩ rằng phương pháp được sử dụng trong bộ lọc thư rác sẽ hoạt động rất tốt. Bạn chia đoạn mã thành các từ. Sau đó, bạn so sánh các lần xuất hiện của những từ này với các đoạn mã đã biết và tính xác suất đoạn mã này được viết bằng ngôn ngữ X cho mọi ngôn ngữ bạn quan tâm.

http://en.wikipedia.org/wiki/Bayesian_spam_filtering

Nếu bạn có cơ chế cơ bản thì rất dễ dàng thêm các ngôn ngữ mới: chỉ cần đào tạo trình dò ​​tìm một vài đoạn mã bằng ngôn ngữ mới (bạn có thể cung cấp cho nó một dự án nguồn mở). Bằng cách này, nó biết rằng "Hệ thống" có khả năng xuất hiện trong các đoạn mã C # và "đặt" trong các đoạn mã Ruby.

Tôi đã thực sự sử dụng phương pháp này để thêm tính năng phát hiện ngôn ngữ vào các đoạn mã cho phần mềm diễn đàn. Nó hoạt động 100% thời gian, trừ những trường hợp không rõ ràng:

print "Hello"

Hãy để tôi tìm mã.

Tôi không thể tìm thấy mã nên tôi đã tạo một mã mới. Nó hơi đơn giản nhưng nó hoạt động cho các thử nghiệm của tôi. Hiện tại nếu bạn cung cấp cho nó nhiều mã Python hơn mã Ruby, nó có thể nói rằng mã này:

def foo
   puts "hi"
end

là mã Python (mặc dù nó thực sự là Ruby). Điều này là do Python cũng có một deftừ khóa. Vì vậy, nếu nó đã thấy 1000x deftrong Python và 100x deftrong Ruby thì nó vẫn có thể nói là Python mặc dù putsendlà Ruby cụ thể. Bạn có thể khắc phục điều này bằng cách theo dõi các từ được nhìn thấy cho mỗi ngôn ngữ và chia cho nó ở đâu đó (hoặc bằng cách cung cấp cho nó lượng mã bằng nhau trong mỗi ngôn ngữ).

Tôi hy vọng nó sẽ giúp bạn:

class Classifier
  def initialize
    @data = {}
    @totals = Hash.new(1)
  end

  def words(code)
    code.split(/[^a-z]/).reject{|w| w.empty?}
  end

  def train(code,lang)
    @totals[lang] += 1
    @data[lang] ||= Hash.new(1)
    words(code).each {|w| @data[lang][w] += 1 }
  end

  def classify(code)
    ws = words(code)
    @data.keys.max_by do |lang|
      # We really want to multiply here but I use logs 
      # to avoid floating point underflow
      # (adding logs is equivalent to multiplication)
      Math.log(@totals[lang]) +
      ws.map{|w| Math.log(@data[lang][w])}.reduce(:+)
    end
  end
end

# Example usage

c = Classifier.new

# Train from files
c.train(open("code.rb").read, :ruby)
c.train(open("code.py").read, :python)
c.train(open("code.cs").read, :csharp)

# Test it on another file
c.classify(open("code2.py").read) # => :python (hopefully)

1
Tôi cũng cần sử dụng nó trong phần mềm diễn đàn. Cảm ơn về mẹo về bộ lọc Bayes.
João Matos

12
Tôi đã làm điều gì đó như thế này trong lớp NLP của mình, nhưng chúng tôi đã tiến xa hơn một bước. Bạn không thích nhìn vào tần số của một từ đơn lẻ , mà là các cặp và bộ ba từ. Ví dụ: "public" có thể là một từ khóa trong nhiều ngôn ngữ, nhưng "public static void" phổ biến hơn đối với C #. Nếu không thể tìm thấy bộ ba, bạn sẽ lùi về 2 và sau đó là 1.
mpen

1
Có thể bạn cũng muốn nghĩ về nơi bạn đang tách các từ. Trong PHP, các biến bắt đầu bằng $, vì vậy có thể bạn không nên tách theo giới hạn từ, vì biến $này nên gắn liền với biến. Các nhà khai thác thích =>:=nên được gắn với nhau như một mã thông báo duy nhất, nhưng OTH có lẽ bạn nên tách ra {vì chúng luôn đứng riêng.
mpen

2
Vâng. Một cách để tránh chia nhỏ là sử dụng ngram: bạn lấy mọi chuỗi con có độ dài n. Ví dụ: 5 gam "put foo" là "put" "uts f", "ts fo" và "s foo". Chiến lược này có vẻ kỳ lạ nhưng nó hoạt động tốt hơn bạn nghĩ, nó không chỉ là cách một con người giải quyết vấn đề. Để quyết định công trình tốt hơn phương pháp bạn sẽ phải kiểm tra cả hai ...
Jules

2
Tuy nhiên, một số ngôn ngữ có rất ít cú pháp. Tôi cũng đang suy đoán rằng các tên biến phổ biến sẽ chiếm ưu thế so với các từ khóa của ngôn ngữ. Về cơ bản, nếu bạn có một đoạn mã C được viết bởi một người Hungary, với các tên biến và nhận xét bằng tiếng Hungary, trong dữ liệu đào tạo của bạn, thì bất kỳ nguồn nào khác có tiếng Hungary trong đó có khả năng được xác định là "tương tự".
tripleee

26

Phát hiện ngôn ngữ do những người khác giải quyết:

Cách tiếp cận của Ohloh: https://github.com/blackducksw/ohcount/

Cách tiếp cận của Github: https://github.com/github/linguist


4
Tôi đã xem xét cả hai giải pháp này và cả hai giải pháp sẽ không làm chính xác những gì được yêu cầu. Họ chủ yếu xem xét các phần mở rộng tệp để xác định ngôn ngữ, vì vậy họ không thể nhất thiết phải kiểm tra một đoạn mã mà không có manh mối từ phần mở rộng.
Hawkee

5
Cách tiếp cận của Github hiện cũng bao gồm trình phân loại Bayes. Nó chủ yếu phát hiện một ứng cử viên ngôn ngữ dựa trên phần mở rộng tệp, nhưng khi một phần mở rộng tệp phù hợp với nhiều ứng cử viên (ví dụ: ".h" -> C, C ++, ObjC), nó sẽ mã hóa mẫu mã đầu vào và phân loại theo tập hợp được đào tạo trước Dữ liệu. Phiên bản Github có thể buộc phải quét mã luôn mà không cần nhìn vào phần mở rộng.
Benzi


7

Guesslang là một giải pháp khả thi:

http://guesslang.readthedocs.io/en/latest/index.html

Ngoài ra còn có SourceClassifier:

https://github.com/chrislo/sourceclassifier/tree/master

Tôi bắt đầu quan tâm đến vấn đề này sau khi tìm thấy một số mã trong một bài báo trên blog mà tôi không thể xác định được. Thêm câu trả lời này vì câu hỏi này là lần truy cập tìm kiếm đầu tiên cho "xác định ngôn ngữ lập trình".


5

Nó rất khó và đôi khi là không thể. Đoạn trích ngắn này từ ngôn ngữ nào?

int i = 5;
int k = 0;
for (int j = 100 ; j > i ; i++) {
    j = j + 1000 / i;
    k = k + i * j;
}

(Gợi ý: Nó có thể là bất kỳ một trong số nhiều.)

Bạn có thể cố gắng phân tích các ngôn ngữ khác nhau và cố gắng quyết định bằng cách sử dụng phân tích tần suất của từ khóa. Nếu một số tập hợp từ khóa nhất định xuất hiện với tần suất nhất định trong một văn bản thì có khả năng là ngôn ngữ là Java, v.v. Nhưng tôi không nghĩ rằng bạn sẽ nhận được bất kỳ thứ gì hoàn toàn là bằng chứng đánh lừa, vì bạn có thể đặt tên cho một biến trong C cùng tên chẳng hạn như một từ khóa trong Java, và phân tích tần suất sẽ bị đánh lừa.

Nếu bạn đưa nó lên một mức độ phức tạp, bạn có thể tìm kiếm các cấu trúc, nếu một từ khóa nhất định luôn đi sau từ khóa khác, điều đó sẽ giúp bạn có thêm manh mối. Nhưng nó cũng sẽ khó hơn nhiều để thiết kế và thực hiện.


26
Chà, nếu có thể có nhiều ngôn ngữ, máy dò có thể đưa ra tất cả các ứng viên có thể.
Steven Haryanto

Hoặc, nó có thể đưa ra cái đầu tiên phù hợp. Nếu trường hợp sử dụng trong thế giới thực là một cái gì đó giống như đánh dấu cú pháp, thì nó thực sự sẽ không tạo ra sự khác biệt. Có nghĩa là bất kỳ ngôn ngữ nào phù hợp sẽ dẫn đến việc đánh dấu mã một cách chính xác.
jonschlinkert

5

Một giải pháp thay thế là sử dụng highlight.js , thực hiện đánh dấu cú pháp nhưng sử dụng tỷ lệ thành công của quá trình đánh dấu để xác định ngôn ngữ. Về nguyên tắc, bất kỳ cơ sở mã của trình đánh dấu cú pháp nào cũng có thể được sử dụng theo cách tương tự, nhưng điều thú vị về highlight.js là tính năng phát hiện ngôn ngữ được coi là một tính năng và được sử dụng cho mục đích thử nghiệm .

CẬP NHẬT: Tôi đã thử điều này và nó không hoạt động tốt. JavaScript nén hoàn toàn nhầm lẫn nó, tức là tokenizer nhạy cảm với khoảng trắng. Nói chung, việc chỉ đếm số lượt truy cập nổi bật có vẻ không đáng tin cậy lắm. Trình phân tích cú pháp mạnh hơn hoặc có thể là số lượng phần chưa từng có, có thể hoạt động tốt hơn.


Dữ liệu ngôn ngữ có trong highlight.js bị giới hạn ở các giá trị cần thiết để đánh dấu, điều này hóa ra không đủ để phát hiện ngôn ngữ (đặc biệt là đối với lượng mã nhỏ).
Adam Kennedy

Tôi nghĩ nó ổn, hãy kiểm tra với fiddle này jsfiddle.net/3tgjnz10
sebilasse

4

Đầu tiên, tôi sẽ cố gắng tìm các keyworks cụ thể của một ngôn ngữ, ví dụ:

"package, class, implements "=> JAVA
"<?php " => PHP
"include main fopen strcmp stdout "=>C
"cout"=> C++
etc...

3
Vấn đề là những từ khóa đó vẫn có thể xuất hiện trong bất kỳ ngôn ngữ nào, dưới dạng tên biến hoặc trong chuỗi. Điều đó, và có rất nhiều sự trùng lặp trong các từ khóa được sử dụng. Bạn sẽ phải làm nhiều việc hơn là chỉ nhìn một từ khóa.
mpen

2

Nó sẽ phụ thuộc vào loại đoạn mã bạn có, nhưng tôi sẽ chạy nó qua một loạt các trình mã hóa và xem BNF của ngôn ngữ nào mà nó tạo ra là hợp lệ.


BNF thậm chí không thể mô tả tất cả các ngôn ngữ. Nếu bạn được phép xác định lại từ khóa và tạo macro thì việc đó sẽ khó hơn nhiều. Như chúng ta đang nói về một đoạn mã bạn sẽ phải thực hiện đối sánh từng phần với BNF, đoạn mã này khó hơn và dễ xảy ra lỗi hơn.

2

Câu đố hay.

Tôi nghĩ rằng nó là bất khả thi để phát hiện tất cả các ngôn ngữ. Nhưng bạn có thể kích hoạt các mã thông báo chính. (các từ dành riêng nhất định và các tổ hợp ký tự thường được sử dụng).

Ben có rất nhiều ngôn ngữ với cú pháp tương tự. Vì vậy, nó phụ thuộc vào kích thước của đoạn mã.


1

Prettify là một gói Javascript thực hiện tốt công việc phát hiện ngôn ngữ lập trình:

http://code.google.com/p/google-code-prettify/

Nó chủ yếu là một công cụ đánh dấu cú pháp, nhưng có lẽ có một cách để trích xuất phần phát hiện nhằm mục đích phát hiện ngôn ngữ từ một đoạn mã.


1
Khi kiểm tra thêm, nó có vẻ như trước không thực sự phát hiện ra ngôn ngữ, nhưng nó đánh dấu theo cú pháp của từng phần tử.
Hawkee


1

Tôi cần cái này nên tôi đã tạo ra cái của riêng mình. https://github.com/bertyhell/CodeClassifier

Nó có thể mở rộng rất dễ dàng bằng cách thêm tệp đào tạo vào đúng thư mục. Được viết bằng c #. Nhưng tôi tưởng tượng mã dễ dàng được chuyển đổi sang bất kỳ ngôn ngữ nào khác.


0

Tôi sẽ không nghĩ rằng sẽ có một cách dễ dàng để đạt được điều này. Tôi có thể sẽ tạo danh sách các ký hiệu / từ khóa chung duy nhất cho một số ngôn ngữ / lớp ngôn ngữ nhất định (ví dụ: dấu ngoặc nhọn cho ngôn ngữ kiểu C, từ khóa Dim và Sub cho ngôn ngữ BASIC, từ khóa def cho Python, từ khóa let cho ngôn ngữ chức năng) . Sau đó, bạn có thể sử dụng các tính năng cú pháp cơ bản để thu hẹp nó hơn nữa.


0

Tôi nghĩ sự khác biệt lớn nhất giữa các ngôn ngữ là cấu trúc của nó. Vì vậy, ý tưởng của tôi là xem xét các yếu tố chung nhất định trên tất cả các ngôn ngữ và xem chúng khác nhau như thế nào. Ví dụ: bạn có thể sử dụng regex để chọn những thứ như:

  • định nghĩa hàm
  • khai báo biến
  • khai báo lớp
  • bình luận
  • cho vòng lặp
  • vòng lặp while
  • in báo cáo

Và có thể một vài thứ khác mà hầu hết các ngôn ngữ phải có. Sau đó, sử dụng một hệ thống điểm. Thưởng nhiều nhất 1 điểm cho mỗi phần tử nếu tìm thấy regex. Rõ ràng, một số ngôn ngữ sẽ sử dụng cùng một cú pháp (vòng lặp for thường được viết giống như for(int i=0; i<x; ++i)vậy nên nhiều ngôn ngữ có thể ghi một điểm cho cùng một thứ, nhưng ít nhất bạn đang giảm khả năng nó là một ngôn ngữ hoàn toàn khác). Một số người trong số họ có thể đạt điểm 0 trên bảng (ví dụ: đoạn mã không chứa một chức năng nào cả) nhưng điều đó hoàn toàn ổn.

Kết hợp điều này với giải pháp của Jules, và nó sẽ hoạt động khá tốt. Cũng có thể tìm kiếm tần suất của các từ khóa để có thêm một điểm.


0

Hấp dẫn. Tôi có một nhiệm vụ tương tự là nhận dạng văn bản ở các định dạng khác nhau. Thuộc tính YAML, JSON, XML hoặc Java? Ngay cả với các lỗi cú pháp, chẳng hạn, tôi nên tự tin phân biệt JSON với XML.

Tôi cho rằng cách chúng tôi mô hình vấn đề là rất quan trọng. Như Mark đã nói, mã hóa một từ là cần thiết nhưng có thể là chưa đủ. Chúng ta sẽ cần đến bigram, hoặc thậm chí là bát quái. Nhưng tôi nghĩ chúng ta có thể tiến xa hơn từ đó khi biết rằng chúng ta đang xem xét các ngôn ngữ lập trình. Tôi nhận thấy rằng hầu hết mọi ngôn ngữ lập trình đều có hai loại mã thông báo duy nhất - biểu tượngtừ khóa . Các ký hiệu tương đối dễ dàng (một số ký hiệu có thể là chữ không phải là một phần của ngôn ngữ) để nhận ra. Sau đó, bigram hoặc bát quái của các biểu tượng sẽ chọn các cấu trúc cú pháp độc đáo xung quanh các biểu tượng. Từ khóa là một mục tiêu dễ dàng khác nếu tập hợp đào tạo đủ lớn và đa dạng. Một tính năng hữu ích có thể là bigram xung quanh các từ khóa có thể. Một loại mã thông báo thú vị khác là khoảng trắng. Trên thực tế, nếu chúng ta mã hóa theo cách thông thường bằng khoảng trắng, chúng ta sẽ mất thông tin này. Tôi muốn nói, để phân tích ngôn ngữ lập trình, chúng tôi giữ các mã thông báo khoảng trắng vì điều này có thể mang thông tin hữu ích về cấu trúc cú pháp.

Cuối cùng nếu tôi chọn một bộ phân loại như rừng ngẫu nhiên, tôi sẽ thu thập dữ liệu github và thu thập tất cả mã nguồn công khai. Hầu hết tệp mã nguồn có thể được gắn nhãn bằng hậu tố tệp. Đối với mỗi tệp, tôi sẽ chia ngẫu nhiên nó ở các dòng trống thành các đoạn mã có kích thước khác nhau. Sau đó, tôi sẽ trích xuất các tính năng và đào tạo bộ phân loại bằng cách sử dụng các đoạn mã được gắn nhãn. Sau khi huấn luyện xong, bộ phân loại có thể được kiểm tra độ chính xác và thu hồi.


0

Giải pháp tốt nhất mà tôi đã thử là sử dụng viên ngọc nhà ngôn ngữ học trong ứng dụng Ruby on Rails. Đó là một cách cụ thể để làm điều đó, nhưng nó hoạt động. Điều này đã được đề cập ở trên bởi @nisc nhưng tôi sẽ cho bạn biết các bước chính xác của tôi để sử dụng nó. (Một số lệnh dòng lệnh sau dành riêng cho ubuntu nhưng nên dễ dàng dịch sang hệ điều hành khác)

Nếu bạn có bất kỳ ứng dụng rails nào mà bạn tạm thời không bận tâm, hãy tạo một tệp mới trong đó để chèn đoạn mã của bạn được đề cập. (Nếu bạn chưa cài đặt rails, có một hướng dẫn tốt ở đây mặc dù đối với ubuntu, tôi khuyên bạn nên làm điều này . Sau đó chạy rails new <name-your-app-dir>và cd vào thư mục đó. Mọi thứ bạn cần để chạy ứng dụng rails đã có sẵn).

Sau khi bạn có một ứng dụng rails để sử dụng, hãy thêm gem 'github-linguist'vào Gemfile của bạn (nghĩa đen chỉ được gọi Gemfiletrong thư mục ứng dụng của bạn, không có số máy lẻ).

Sau đó cài đặt ruby-dev ( sudo apt-get install ruby-dev)

Sau đó cài đặt cmake ( sudo apt-get install cmake)

Bây giờ bạn có thể chạy gem install github-linguist(nếu bạn gặp lỗi cho biết yêu cầu icu, hãy làm sudo apt-get install libicu-devvà thử lại)

(Bạn có thể cần phải làm một sudo apt-get updatehoặc sudo apt-get install makehoặc sudo apt-get install build-essentialnếu những điều trên không hiệu quả)

Bây giờ mọi thứ đã được thiết lập. Bây giờ bạn có thể sử dụng điều này bất kỳ lúc nào bạn muốn kiểm tra các đoạn mã. Trong trình soạn thảo văn bản, hãy mở tệp bạn đã tạo để chèn đoạn mã của mình (giả sử như vậy app/test.tplnhưng nếu biết phần mở rộng của đoạn mã, hãy sử dụng tệp đó thay vì sử dụng .tpl. Nếu bạn không biết phần mở rộng, đừng sử dụng ). Bây giờ, hãy dán đoạn mã của bạn vào tệp này. Đi tới dòng lệnh và chạy bundle install(phải nằm trong thư mục ứng dụng của bạn). Sau đó chạy linguist app/test.tpl(tổng quát hơn linguist <path-to-code-snippet-file>). Nó sẽ cho bạn biết loại, kiểu kịch câm và ngôn ngữ. Đối với nhiều tệp (hoặc để sử dụng chung với ứng dụng ruby ​​/ rails), bạn có thể chạy bundle exec linguist --breakdowntrong thư mục ứng dụng của mình.

Có vẻ như rất nhiều công việc bổ sung, đặc biệt nếu bạn chưa có đường ray, nhưng bạn thực sự không cần biết BẤT CỨ ĐIỀU GÌ về đường ray nếu bạn làm theo các bước sau và tôi thực sự không tìm ra cách tốt hơn để phát hiện ngôn ngữ của tệp / đoạn mã.


0

Tôi tin rằng không có giải pháp duy nhất nào có thể xác định được ngôn ngữ của một đoạn mã, chỉ dựa trên đoạn mã đó. Lấy từ khóa print. Nó có thể xuất hiện ở bất kỳ số ngôn ngữ nào, mỗi ngôn ngữ dành cho các mục đích khác nhau và có cú pháp khác nhau.

Tôi có một số lời khuyên. Tôi hiện đang viết một đoạn mã nhỏ cho trang web của mình có thể được sử dụng để xác định ngôn ngữ lập trình. Giống như hầu hết các bài viết khác, có thể có rất nhiều ngôn ngữ lập trình mà bạn chỉ đơn giản là chưa nghe, bạn không thể giải thích tất cả.

Những gì tôi đã làm là mỗi ngôn ngữ có thể được xác định bằng một số từ khóa. Ví dụ, Python có thể được xác định theo một số cách. Có lẽ sẽ dễ dàng hơn nếu bạn chọn các 'đặc điểm' chắc chắn cũng là duy nhất cho ngôn ngữ. Đối với Python, tôi chọn đặc điểm sử dụng dấu hai chấm để bắt đầu một tập hợp các câu lệnh, mà tôi tin rằng đây là một đặc điểm khá độc đáo (hãy sửa cho tôi nếu tôi sai).

Nếu, trong ví dụ của tôi, bạn không thể tìm thấy dấu hai chấm để bắt đầu một bộ câu lệnh, thì hãy chuyển sang một đặc điểm có thể có khác, giả sử sử dụng deftừ khóa để xác định một hàm. Bây giờ điều này có thể gây ra một số vấn đề, vì Ruby cũng sử dụng từ khóa defđể định nghĩa một hàm. Chìa khóa để phân biệt hai (Python và Ruby) là sử dụng các cấp độ lọc khác nhau để có được kết quả phù hợp nhất. Ruby sử dụng từ khóa endđể kết thúc một hàm, trong khi Python không có bất cứ thứ gì để kết thúc một hàm, chỉ là một đoạn thụt lề nhưng bạn không muốn đến đó. Nhưng một lần nữa, endcũng có thể là Lua, một ngôn ngữ lập trình khác để thêm vào hỗn hợp.

Bạn có thể thấy rằng các ngôn ngữ lập trình chỉ đơn giản là phủ quá nhiều. Một từ khóa có thể là từ khóa ở một ngôn ngữ có thể trở thành từ khóa ở ngôn ngữ khác. Sử dụng kết hợp các từ khóa thường đi cùng nhau, như Java public static void main(String[] args)sẽ giúp loại bỏ những vấn đề đó.

Như tôi đã nói, cơ hội tốt nhất của bạn là tìm kiếm các từ khóa hoặc bộ từ khóa tương đối độc đáo để tách biệt từ khóa này với từ khóa khác. Và, nếu bạn hiểu sai, ít nhất bạn đã có một cơ hội.


0

Thiết lập scrambler ngẫu nhiên như

matrix S = matrix(GF(2),k,[random()<0.5for _ in range(k^2)]); while (rank(S) < k) : S[floor(k*random()),floor(k*random())] +=1;

0

Trang web này có vẻ khá tốt trong việc xác định ngôn ngữ, nếu bạn muốn một cách nhanh chóng để dán một đoạn mã vào biểu mẫu web, thay vì thực hiện theo chương trình: http://dpaste.com/

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.