与标准Python客户端建立数据库接口

有很多重要的数据库遵从 Python 数据库 API 规范 2.0 版本,包括 MySQL、PostgreSQL、Oracle、Microsoft SQL Server 和 SQLite。它们的驱动一般都比较复杂且久经考验,如果为 Twisted 重新实现的话则是巨大的浪费。人们可以在 Twisted 应用中使用这些数据库客户端,比如在 Scrapy 使用 twisted.enterprise.adbapi 库。我们将使用 MySQL 作为示例演示其使用,不过对于任何其他兼容的数据库来说,也可以应用相同的原则。

用于写入MySQL的管道

MySQL 是一个非常强大且流行的数据库。我们将编写一个管道,将 item 写入到其中。我们已经在虚拟环境中运行了一个 MySQL 实例。现在只需使用 MySQL 命令行工具执行一些基本管理即可,同样该工具也已经在开发机中预安装好了,下面执行如下操作打开 MySQL 控制台。

$ mysql -h mysql -uroot -ppass

这将会得到 MySQL 的提示符,即 mysq>,现在可以创建一个简单的数据库表,其中包含一些字段,如下所示。

mysql> create database properties;
mysql> use properties
mysql> CREATE TABLE properties (
    url varchar(100) NOT NULL,
    title varchar(30),
    price DOUBLE,
    description varchar(30),
    PRIMARY KEY (url)
);
mysql> SELECT * FROM properties LIMIT 10;
Empty set (0.00 sec)

非常好,现在拥有了一个 MySQL 数据库,以及一张名为 properties 的表,其中包含了一些字段,此时可以准备创建管道了。请保持 MySQL 的控制台为开启状态,因为之后还会回来检查是否正确插入了值。如果想退出控制台,只需要输入 exit 即可。

在本节,我们将会向 MySQL 数据库中插入房产信息。如果你想擦除它们,可以使用如下命令:

mysql> DELETE FROM properties;

我们将使用 Python 的 MySQL 客户端。我们还将安装一个名为 djdatabase-url 的小工具模块,帮助我们解析连接的 URL(仅用于为我们在IP、端口、密码等不同设置中切换节省时间)。可以使用 pip install dj-database-url MySQL-python 安装这两个库,不过我们已经在开发环境中安装好它们了。我们的 MySQL 管道非常简单,如下所示。

from twisted.enterprise import adbapi
...
class MysqlWriter(object):
    ...
    def __init__(self, mysql_url):
        conn_kwargs = MysqlWriter.parse_mysql_url(mysql_url)
        self.dbpool = adbapi.ConnectionPool('MySQLdb',
                                charset='utf8',use_unicode=True,
                                connect_timeout=5,
                                **conn_kwargs)

    def close_spider(self, spider):
        self.dbpool.close()

    @defer.inlineCallbacks
    def process_item(self, item, spider):
        try:
            yield self.dbpool.runInteraction(self.do_replace, item)
        except:
            print traceback.format_exc()

        defer.returnValue(item)

    @staticmethod
    def do_replace(tx, item):
        sql = """REPLACE INTO properties (url, title, price,description) VALUES (%s,%s,%s,%s)"""

        args = (
            item["url"][0][:100],
            item["title"][0][:30],
            item["price"][0],
            item["description"][0].replace("\r\n", " ")[:30]
        )

        tx.execute(sql, args)

本示例的完整代码地址为 ch09/properties/properties/pipeline/mysql.py。

本质上,大部分代码仍然是模板化的爬虫代码。我们省略的代码用于将 MYSQL_PIPELINE_URL 设置中包含的 mysql://user:pass@ip/database 格式的 URL 解析为独立参数。在爬虫的 __init__() 中,我们将这些参数传给 adbapi.ConnectionPool(),使用 adbapi 的基础功能初始化 MySQL 连接池。第一个参数是想要导入的模块名称。在该 MySQL 示例中,为 MySQLdb。我们还为 MySQL 客户端设置了一些额外的参数,用于处理 Unicode 和超时。所有这些参数会在每次 adbapi 需要打开新连接时,前往底层的 MySQLdb.connect() 函数。当爬虫关闭时,我们为该连接池调用 close() 方法。

我们的 process_item() 方法实际上包装了 dbpool.runInteraction()。该方法将稍后调用的回调方法放入队列,当来自连接池的某个连接的 Transaction 对象变为可用时,调用该回调方法。Transaction 对象的 API 与 DB-API 游标相似。在本例中,回调方法为 do_replace(),该方法在后面几行进行了定义。@staticmethod 意味着该方法指向的是类,而不是具体的类实例,因此,可以省略平时使用的 self 参数。当不使用任何成员时,将方法静态化是个好习惯,不过即使忘记这么做,也没有问题。该方法准备了一个 SQL 字符串和几个参数,调用 Transaction 的 execute() 方法执行插入。我们的 SQL 语句使用了 REPLACE INTO 来替换已经存在的条目,而 不是更常见的 INSERT INTO, 原因是如果条目已经存在,可以使用相同的主键。在本例中这种方式非常便捷。如果想使用 SQL 返回数据,如 SELECT 语句,可以使用 dbpool.runQuery()。如果想要修改默认游标,可以通过设置 adbapi.ConnectionPool() 的 cursorclass 参数来实现,比如设置 cursorclass=MySQLdb.cursors.DictCursor,可以让数据获取更加便捷。

要想使用该管道,需要在 settings.py 文件的 ITEM_PIPELINES 字典中添加它,另外还需要设置 MYSQL_PIPELINE_URL 属性。

ITEM_PIPELINES = { ...
  'properties.pipelines.mysql.MysqlWriter': 700,
...
MYSQL_PIPELINE_URL = 'mysql://root:pass@mysql/properties'

执行如下命令。

scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=1000

该命令运行后,可以回到 MySQL 提示符下,按如下方式查看数据库中的记录。

mysql> SELECT COUNT(*) FROM properties;
+----------+
| 1006 |
+----------+
mysql> SELECT * FROM properties LIMIT 4;
+------------------+--------------------------+--------+-----------+
| url | title | price | description
+------------------+--------------------------+--------+-----------+
| http://...0.html | Set Unique Family Well | 334.39 | website c
| http://...1.html | Belsize Marylebone Shopp | 388.03 | features
| http://...2.html | Bathroom Fully Jubilee S | 365.85 | vibrant own
| http://...3.html | Residential Brentford Ot | 238.71 | go court
+------------------+--------------------------+--------+-----------+
4 rows in set (0.00 sec)

延时和吞吐量等性能和之前保持相同,相当不错。