Scrapy Pipeline之与数据库交互

有很多数据库符合Python Database API规范2.0,例如MySQL,PostgreSQL,Orcale,Microsoft SQL Server和SQLite。它们的驱动是很复杂的并且经过了很多测试,如果再为了Twisted重新实现就太浪费了。你可以在Twisted应用中使用这些数据库客户端,例如,Scrapy就使用了twisted.enterprise.adbapi库。我们会用MySQL来展示一下如何使用,不过相同的使用方法可以应用到其他兼容的数据库上。

写入MySQL的Pipeline

先来创建一个数据库:

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的表格,里面有一些准备用来创建pipeline的字段。

接下来会使用MySQL的Python客户端,并要安装一个模块叫做dj-database-url,它是用来帮助我们解析连接到数据库的URL的,可以使用pip install dj-database-url MySQL-python来安装。下面是MySQL Pipeline的代码:

import traceback

import dj_database_url
import MySQLdb

from twisted.internet import defer
from twisted.enterprise import adbapi
from scrapy.exceptions import NotConfigured


class MysqlWriter(object):
    """
    A spider that writes to MySQL databases
    """

    @classmethod
    def from_crawler(cls, crawler):
        """Retrieves scrapy crawler and accesses pipeline's settings"""

        # Get MySQL URL from settings
        mysql_url = crawler.settings.get('MYSQL_PIPELINE_URL', None)

        # If doesn't exist, disable the pipeline
        if not mysql_url:
            raise NotConfigured

        # Create the class
        return cls(mysql_url)

    def __init__(self, mysql_url):
        """Opens a MySQL connection pool"""

        # Store the url for future reference
        self.mysql_url = mysql_url
        # Report connection error only once
        self.report_connection_error = True

        # Parse MySQL URL and try to initialize a connection
        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):
        """Discard the database pool on spider close"""
        self.dbpool.close()

    @defer.inlineCallbacks
    def process_item(self, item, spider):
        """Processes the item. Does insert into MySQL"""

        logger = spider.logger

        try:
            yield self.dbpool.runInteraction(self.do_replace, item)
        except MySQLdb.OperationalError:
            if self.report_connection_error:
                logger.error("Can't connect to MySQL: %s" % self.mysql_url)
                self.report_connection_error = False
        except:
            print traceback.format_exc()

        # Return the item for the next stage
        defer.returnValue(item)

    @staticmethod
    def do_replace(tx, item):
        """Does the actual REPLACE INTO"""

        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)

    @staticmethod
    def parse_mysql_url(mysql_url):
        """
        Parses mysql url and prepares arguments for
        adbapi.ConnectionPool()
        """

        params = dj_database_url.parse(mysql_url)

        conn_kwargs = {}
        conn_kwargs['host'] = params['HOST']
        conn_kwargs['user'] = params['USER']
        conn_kwargs['passwd'] = params['PASSWORD']
        conn_kwargs['db'] = params['NAME']
        conn_kwargs['port'] = params['PORT']

        # Remove items with empty values
        conn_kwargs = dict((k, v) for k, v in conn_kwargs.iteritems() if v)

        return conn_kwargs

parse_mysql_url()函数用来把MYSQL_PIPELINE_URL设置的值解析成单个的参数并在__init__()函数中把它传递给adbapi.ConnectPool(),由这个函数使用adbapi来对MySQL连接池进行初始化。它的第一个参数是需要加载的模块,在此例中是MySQLdb。我们为MySQL客户端设置了一些其他的参数来处理Unicode和超时。所有的这些参数都会在adbapi需要打开新的连接时传递给MySQLdb.connect()函数。爬虫关闭的时候,调用close()函数。

process_item()方法只是对dbpool.runInteraction()的一个包装。这个方法把回调函数组织成一个队列并在连接池中的某个连接的Transaction对象可用时调用回调函数。Transaction对象有一个和DB-API游标类似的API。在这个例子中,回调函数是do_replace(),定义在后面几行。@staticmethod意味着这个方法是与类相关的而不是与类的实例相关的,其实可以删除前面的self参数。如果一个方法中没有使用成员变量,那么最好还是把它设置成静态方法,但是忘记这样做也无所谓。这个方法准备了一个SQL语句,一些参数,并调用了Transactionexecute()方法来执行插入操作。方法中的SQL语句使用了REPLACE INTO而不是INSERT INTO,以便当一个条目已经存在时来替换它。如果需要SQL语句返回结果,比如使用了SELECT语句,那就要使用dbpool.runQuery()。通过设置adbapi.ConnectionPool()方法的cursorclass参数还可以改变默认的游标,比如cursorclass=MySQLdb.cursors.DictCursor,因为它更利于数据的检索。

ITEM_PIPELINES中加上:

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)

至于性能和之前的一样。

你可能感兴趣的:(scrapy,网络爬虫)