基于Excel文件爬取的爬虫
大多数情况下,每个源网站只会有一个爬虫;不过在某些情况下,你想要抓取的数据来自多个网站,此时唯一变化的东西就是所使用的 XPath 表达式。对于此类情况,如果为每个网站都使用一个爬虫则显得有些小题大做。那么可以只使用一个爬虫来爬取所有这些网站吗?答案是肯定的。
让我们为该实验创建一个新的爬虫,因为这次爬取的条目会和之前区别很大(实际上我们还没有在该项目中定义任何东西!)。假设此时在 ch05 下的 properties 目录中。让我们向上一层,如下面的代码所示进行操作。
$ pwd
/root/book/ch05/properties
$ cd ..
$ pwd
/root/book/ch05
bash
我们创建了一个名为 generic 的新项目,以及一个名为 fromcsv 的爬虫。
$ scrapy startproject generic
$ cd generic
$ scrapy genspider fromcsv example.com
bash
现在,创建一个.csv文件,其中包含想要抽取的信息。可以使用一个电子表格程序,比如 Microsoft Excel,来创建这个 .csv 文件。填入如图5.5所示的几个 URL 和 XPath 表达式,然后将其命名为 todo.csv,保存到爬虫目录当中(scrapy.cfg 所在目录)。要想保存为 .csv 文件,需要在保存对话框中选择 CSV 文件(Windows)作为文件格式。

很好!如果一切都已就绪,你就可以在终端上看到该文件。
$ cat todo.csv
url,name,price
a.html,"//*[@id=""itemTitle""]/text()","//*[@id=""prcIsum""]/text()"
b.html,//h1/text(),//span/strong/text()
c.html,"//*[@id=""product-desc""]/span/text()"
bash
Python 有一个用于处理 .csv 文件的内置库。只需通过 import csv 导入模块,然后就可以使用如下这些直截了当的代码,以字典的形式读取文件中的所有行了。在当前目录下打开 Python 提示符,就可以尝试如下代码。
$ pwd
/root/book/ch05/generic2
$ python
>>> import csv
>>> with open("todo.csv", "rU") as f:
reader = csv.DictReader(f)
for line in reader:
print line
bash
文件中的第一行会被自动作为标题行处理,并且会根据它们得出字典中键的名称。在接下来的每一行中,会得到一个包含行内数据的字典。我们使用 for 循环迭代每一行。当运行前面的代码时,可以得到如下输出。
{'url': ' http://a.html', 'price': '//*[@id="prcIsum"]/text()','name': '//*[@id="itemTitle"]/text()'}
{'url': ' http://b.html', 'price': '//span/strong/text()', 'name': '//h1/text()'}
{'url': ' http://c.html', 'price': '', 'name': '//*[@id="productdesc"]/span/text()'}
bash
非常好。现在,可以编辑 generic/spiders/fromcsv.py 这个爬虫了。我们将会用到 .csv 文件中的 URL,并且不希望有任何域名限制。因此,首先要做的事情就是移除 start_urls 以及 allowed_domains,然后读取 .csv 文件。
由于我们事先并不知道想要起始的 URL,而是从文件中读取得到的,因此需要实现一个 start_requests() 方法。对于每一行,创建 Request,然后对其进行 yield 操作。此外,还会在 reqeust.meta 中存储来自 csv 文件的字段名称和 XPath 表达式,以便在 parse() 函数中使用它们。然后,使用 Item 和 ItemLoader 填充 Item 的字段。下面是完整的代码。
import csv
import scrapy
from scrapy.http import Request
from scrapy.loader import ItemLoader
from scrapy.item import Item, Field
class FromcsvSpider(scrapy.Spider):
name = "fromcsv"
def start_requests(self):
with open("todo.csv", "rU") as f:
reader = csv.DictReader(f)
for line in reader:
request = Request(line.pop('url'))
request.meta['fields'] = line
yield request
def parse(self, response):
item = Item()
l = ItemLoader(item=item, response=response)
for name, xpath in response.meta['fields'].iteritems():
if xpath:
item.fields[name] = Field()
l.add_xpath(name, xpath)
return l.load_item()
python
接下来开始爬取,并将结果输出到 out.csv 文件中。
$ scrapy crawl fromcsv -o out.csv
INFO: Scrapy 0.0.3 started (bot: generic)
...
DEBUG: Scraped from <200 a.html>
{'name': [u'My item'], 'price': [u'128']}
DEBUG: Scraped from <200 b.html>
{'name': [u'Getting interesting'], 'price': [u'300']}
DEBUG: Scraped from <200 c.html>
{'name': [u'Buy this now']}
...
INFO: Spider closed (finished)
$ cat out.csv
price,name
128,My item
300,Getting interesting
,Buy this now
bash
正如爬取得到的结果一样,非常简洁直接!
在代码中,你可能已经注意到了几个事情。由于我们没有为该项目定义系统范围的 Item,因此必须像如下代码这样手动为 ItemLoader 提供。
item = Item() l = ItemLoader(item=item, response=response)
bash
此外,我们还使用了 Item 的成员变量 fields 动态添加字段。为了能够动态添加新字段,并通过 ItemLoader 对其进行填充,需要实现的代码如下。
item.fields[name] = Field() l.add_xpath(name, xpath)
bash
最后,还可以使代码更加好看。硬编码 todo.csv 文件名不是一个非常好的实践。Scrapy 提供了一个非常便捷的方法,用于传输参数到爬虫当中。当传输一个命令行参数 -a 时(比如:-a variable=value),就会为我们设置一个爬虫属性,并且可以通过 self.variable 取得该值。为了检查变量,并在未提供该变量时使用默认值,可以使用 Python 的 getattr() 方法:getattr(self, 'variable', 'default')。总之,我们将原来的 with open… 语句替换为如下语句。
with open(getattr(self, "file", "todo.csv"), "rU") as f:
bash
现在,除非明确使用 -a 参数设置源文件名,否则将会使用 todo.csv 作为其默认值。当给出另一个文件 another_todo.csv 时,可以按如下方式运行。
$ scrapy crawl fromcsv -a file=another_todo.csv -o out.csv
bash