使用Scrapy建立一个网站抓取器


英文原文:Build a Website Crawler based upon Scrapy

标签:
209人收藏此文章,我要收藏renwofei423推荐于 11个月前 (共 9 段, 翻译完成于 12-30) (14评)

Scrapy是一个用于爬行网站以及在数据挖掘、信息处理和历史档案等大量应用范围内抽取结构化数据的应用程序框架,广泛用于工业。

在本文中我们将建立一个从Hacker News爬取数据的爬虫,并将数据按我们的要求存储在数据库中。

安装

我们将需要Scrapy以及BeautifulSoup用于屏幕抓取,SQLAlchemy用于存储数据.

如果你使用ubuntu已经其他发行版的unix可以通过pip命令安装Scrapy。

1
pipinstallScrapy

如果你使用Windows,你需要手工安装scrapy的一些依赖。

Windows用户需要pywin32、pyOpenSSL、Twisted、lxml和zope.interface。你可以下载这些包的编译版本来完成简易安装。

可以参照官方文档查看详情指导。

都安装好后,通过在python命令行下输入下面的命令验证你的安装:

1
2
>>importscrapy
>>

如果没有返回内容,那么你的安装已就绪。



安装HNScrapy

为了创建一个新项目,在终端里输入以下命令

1
$scrapystartprojecthn

这将会创建一系列的文件帮助你更容易的开始,cd 到 hn 目录然后打开你最喜欢的文本编辑器。

在items.py文件里,scrapy需要我们定义一个容器用于放置爬虫抓取的数据。如果你原来用过Djangotutorial,你会发现items.py与Django中的models.py类似。

你将会发现class HnItem已经存在了,它继承自Item--一个scrapy已经为我们准备好的预定义的对象。

让我们添加一些我们真正想抓取的条目。我们给它们赋值为Field()是因为这样我们才能把元数据(metadata)指定给scrapy。

1
2
3
4
5
fromscrapy.itemimportItem,Field
classHnItem(Item):
title=Field()
link=Field()

没什么难的--恩,就是这样。在scrapy里,没有别的filed类型,这点和Django不同。所以,我们和Field()杠上了。

scrapy的 Item类的行为类似于Python里面的dictionary,你能从中获取key和value。


开始写爬虫

在spiders文件夹下创建一个hn_spider.py文件。这是奇迹发生的地方--这正是我们告诉scrapy如何找到我们寻找的确切数据的地方。正如你所想的那样,一个爬虫只针对一个特定网页。它可能不会在其他网站上工作。

在ht_spider.py里,我们将定义一个类,HnSpider以及一些通用属性,例如name和urls。

首先,我们先建立HnSpider类以及一些属性(在类内部定义的变量,也被称为field)。我们将从scrapy的BaseSpider继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fromscrapy.spiderimportBaseSpider
fromscrapy.selectorimportSelector
classHnSpider(BaseSpider):
name='hn'
allowed_domains=[]
defparse(self,response):
sel=Selector(response)
sites=sel.xpath('//td[@class="title"]')
forsiteinsites:
title=site.xpath('a/text()').extract()
link=site.xpath('a/@href').extract()
printtitle,link

前面的几个变量是自解释的:name定义了爬虫的名字,allowed_domains列出了 供爬虫爬行的允许域名(allowed domain)的base-URL,start_urls列出了爬虫从这里开始爬行的URL。后续的URL将从爬虫从start_urls下载的数据的URL开始。

接着,scrapy使用XPath选择器从网站获取数据--通过一个给定的XPath从HTML数据的特定部分进行选择。正如它们的文档所说,"XPath 是一种用于从XML选择节点的语言,它也可以被用于HTML"。你也可以阅读它们的文档了解更多关于XPath选择器的信息。

注意在抓取你自己的站点并尝试计算XPath 时, Chrome的开发工具提供了检查html元素的能力, 可以让你拷贝出任何你想要的元素的xpath. 它也提供了检测xpath的能力,只需要在javascript控制台中使用$x, 例如$x("//img"). 而在这个教程就不多深究这个了, Firefox 有一个插件,FirePath同样也可以编辑,检查和生成XPath.

我们一般会基于一个定义好的Xpath来告诉scrapy 到哪里去开始寻找数据. 让我们浏览我们的Hacker News 站点,并右击选择”查看源代码“:

使用Scrapy建立一个网站抓取器

你会看到那个sel.xpath('//td[@class="title"]')有点貌似我们见过的HTML的代码. 从它们的文档中你可以解读出构造XPath 并使用相对XPath 的方法. 但本质上, '//td[@class="title"]'是在说: 所有的<td>元素中, 如果一个<a class="title"></a> 被展现了出来,那就到 <td> 元素里面去寻找那个拥有一个被称作title的类型的<a>元素.

parse()方法使用了一个参数:response. 嘿,等一下– 这个 self 是干什么的– 看起来像是有两个参数!

每一个实体方法(在这种情况下,parse() 是一个实体方法) 接受一个对它自身的引用作为其第一个参数. 为了方便就叫做“self”.

response 参数是抓取器在像Hacker News发起一次请求之后所要返回的东西. 我们会用我们的XPaths转换那个响应.

现在我们将使用BeautifulSoup 来进行转换.Beautiful Soup 将会转换任何你给它的东西.

下载BeautifulSoup 并在抓取器目录里面创建soup.py 文件,将代码复制到其中.

在你的hn_spider.py文件里面引入beautifulSoup 和来自 items.py的Hnitem,并且像下面这样修改转换方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fromsoupimportBeautifulSoupasbs
fromscrapy.httpimportRequest
fromscrapy.spiderimportBaseSpider
fromhn.itemsimportHnItem
classHnSpider(BaseSpider):
name='hn'
allowed_domains=[]
defparse(self,response):
if'news.ycombinator.com'inresponse.url:
soup=bs(response.body)
items=[(x[0].text,x[0].get('href'))forxin
filter(None,[
x.findChildren()forxin
soup.findAll('td',{'class':'title'})
])]
foriteminitems:
printitem
hn_item=HnItem()
hn_item['title']=item[0]
hn_item['link']=item[1]
try:
yieldRequest(item[1],callback=self.parse)
exceptValueError:
yieldRequest('http://news.ycombinator.com/'+item[1],callback=self.parse)
yieldhn_item

我们正在迭代这个items,并且给标题和链接赋上抓取来的数据.

现在就试试对Hacker News域名进行抓取,你会看到连接和标题被打印在你的控制台上.

1
scrapycrawlhn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
2013-12-1216:57:06+0530[scrapy]INFO:Scrapy0.20.2started(bot:hn)
2013-12-1216:57:06+0530[scrapy]DEBUG:Optionalfeaturesavailable:ssl,http11,django
2013-12-1216:57:06+0530[scrapy]DEBUG:Overriddensettings:{'NEWSPIDER_MODULE':'hn.spiders','SPIDER_MODULES':['hn.spiders'],'BOT_NAME':'hn'}
2013-12-1216:57:06+0530[scrapy]DEBUG:Enabledextensions:LogStats,TelnetConsole,CloseSpider,WebService,CoreStats,SpiderState
2013-12-1216:57:06+0530[scrapy]DEBUG:Enableddownloadermiddlewares:HttpAuthMiddleware,DownloadTimeoutMiddleware,UserAgentMiddleware,RetryMiddleware,DefaultHeadersMiddleware
,MetaRefreshMiddleware,HttpCompressionMiddleware,RedirectMiddleware,CookiesMiddleware,ChunkedTransferMiddleware,DownloaderStats
2013-12-1216:57:06+0530[scrapy]DEBUG:Enabledspidermiddlewares:HttpErrorMiddleware,OffsiteMiddleware,RefererMiddleware,UrlLengthMiddleware,DepthMiddleware
2013-12-1216:57:06+0530[scrapy]DEBUG:Enableditempipelines:
2013-12-1216:57:06+0530[hn]INFO:Spideropened
2013-12-1216:57:06+0530[hn]INFO:Crawled0pages(at0pages/min),scraped0items(at0items/min)
2013-12-1216:57:06+0530[scrapy]DEBUG:Telnetconsolelisteningon0.0.0.0:6023
2013-12-1216:57:06+0530[scrapy]DEBUG:Webservicelisteningon0.0.0.0:6080
2013-12-1216:57:07+0530[hn]DEBUG:Redirecting(301)to<GEThttps://news.ycombinator.com/>from<GEThttp://news.ycombinator.com>
2013-12-1216:57:08+0530[hn]DEBUG:Crawled(200)<GEThttps://news.ycombinator.com/>(referer:None)
(u'CaltechAnnouncesOpenAccessPolicy|Caltech',u'http://www.caltech.edu/content/caltech-announces-open-access-policy')
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
'title':u'CaltechAnnouncesOpenAccessPolicy|Caltech'}
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
'title':u'CoinbaseRaises$25MillionFromAndreessenHorowitz'}
(u'BackpackerstrippedoftechgearatAucklandAirport',u'http://www.nzherald.co.nz/nz/news/article.cfm?c_id=1&objectid=11171475')
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
'title':u'BackpackerstrippedoftechgearatAucklandAirport'}
(u'HowIintroduceda27-year-oldcomputertotheweb',u'http://www.keacher.com/1216/how-i-introduced-a-27-year-old-computer-to-the-web/')
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
'title':u'HowIintroduceda27-year-oldcomputertotheweb'}
(u'ShowHN:BitcoinPulse-TrackingBitcoinAdoption',u'http://www.bitcoinpulse.com')
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
'title':u'ShowHN:BitcoinPulse-TrackingBitcoinAdoption'}
(u'Whywasthissecret?',u'http://sivers.org/ws')
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
{'link':u'http://sivers.org/ws','title':u'Whywasthissecret?'}
(u'PostgreSQLExercises',u'http://pgexercises.com/')
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
{'link':u'http://pgexercises.com/','title':u'PostgreSQLExercises'}
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
'title':u'Whatitfeelslikebeinganipadonastickonwheels'}
(u'Prototypeergonomicmechanicalkeyboards',u'http://blog.fsck.com/2013/12/better-and-better-keyboards.html')
2013-12-1216:57:08+0530[hn]DEBUG:Scrapedfrom<200https://news.ycombinator.com/>
'title':u'Prototypeergonomicmechanicalkeyboards'}
.............
.............
.............
2013-12-1216:58:41+0530[hn]INFO:Closingspider(finished)
2013-12-1216:58:41+0530[hn]INFO:DumpingScrapystats:
{'downloader/exception_count':2,
'downloader/exception_type_count/twisted.internet.error.DNSLookupError':2,
'downloader/request_bytes':22401,
'downloader/request_count':71,
'downloader/request_method_count/GET':71,
'downloader/response_bytes':1482842,
'downloader/response_count':69,
'downloader/response_status_count/200':61,
'downloader/response_status_count/301':4,
'downloader/response_status_count/302':3,
'downloader/response_status_count/404':1,
'finish_reason':'finished',
'finish_time':datetime.datetime(2013,12,12,11,28,41,289000),
'item_scraped_count':63,
'log_count/DEBUG':141,
'log_count/INFO':4,
'request_depth_max':2,
'response_received_count':62,
'scheduler/dequeued':71,
'scheduler/dequeued/memory':71,
'scheduler/enqueued':71,
'scheduler/enqueued/memory':71,
'start_time':datetime.datetime(2013,12,12,11,27,6,843000)}
2013-12-1216:58:41+0530[hn]INFO:Spiderclosed(finished)

你将会在终端上看到大约400行的大量输出 ( 上面的输出之所以这么短,目的是为了方便观看).

你可以通过下面这个小命令将输出包装成JSON格式

1
$scrapycrawlhn-oitems.json-tjson

现在我们已经基于正在找寻的项目实现了我们抓取器.

!

保存抓取到的数据

我们开始的步骤是创建一个保存我们抓取到的数据的数据库。打开 settings.py 并且像下面展现的代码一样定义数据库配置。

1
2
3
4
5
6
7
8
9
BOT_NAME='hn'
SPIDER_MODULES=['hn.spiders']
NEWSPIDER_MODULE='hn.spiders'
DATABASE={'drivername':'xxx',
'username':'yyy',
'password':'zzz',
'database':'vvv'}

再在 hn 目录下创建一个 mdels.py 文件。我们将要使用SQLAlchemy作为ORM框架建立数据库模型。

首先,我们需要定义一个直接连接到数据库的方法。为此,我们需要引入 SQLAlchemy 以及settings.py文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fromsqlalchemyimportcreate_engine,Column,Integer,String
fromsqlalchemy.ext.declarativeimportdeclarative_base
fromsqlalchemy.engine.urlimportURL
importsettings
DeclarativeBase=declarative_base()
defdb_connect():
returncreate_engine(URL(**settings.DATABASE))
defcreate_hn_table(engine):
DeclarativeBase.metadata.create_all(engine)
classHn(DeclarativeBase):
__tablename__="hn"
id=Column(Integer,primary_key=True)
title=Column('title',String(200))
link=Column('link',String(200))

在开始下一步之前,我还想说明一下在 URL() 方法里两个星号的用法:**settings.DATABASE。首先,我们通过 settings.py 里的变量来访问数据库。这个 ** 实际上会取出所有在 DATABASE 路径下的值。URL 方法,一个在SQLAlchemy里定义的构造器,将会把key和value映射成一个SQLAlchemy能明白的URL来连接我们的数据库。

接着,URL() 方法将会解析其他元素,然后创建一个下面这样的将被 create_engine() 方法读取的URL。

接下来,我们要为我们的ORM创建一个表。我们需要 从SQLAlchemy引入declarative_base()以便把我们为表结构定义的类映射到Postgres上,以及一个从表的元数据里创建我们所需要的表的方法,还有我们已经定义好的用于存储数据的表和列。

!

管道管理

我们已经建立了用来抓取和解析HTML的抓取器, 并且已经设置了保存这些数据的数据库. 现在我们需要通过一个管道来将两者连接起来.

打开 pipelines.py 并引入SQLAlchemy的sessionmaker 功能,用来绑定数据库(创建那个连接), 当然也要引入我们的模型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fromsqlalchemy.ormimportsessionmaker
frommodelsimportHn,db_connect,create_hn_table
classHnPipeline(object):
def__init__(self):
engine=db_connect()
create_hn_table(engine)
self.Session=sessionmaker(bind=engine)
defprocess_item(self,item,spider):
session=self.Session()
hn=Hn(**item)
session.add(hn)
session.commit()
returnitem

我们在这里创建了一个类,HnPipeline(). 我们有一个构造器函数def __init__(self) 来通过定义引擎初始化这个类, hn表格,还使用定义的这个引擎绑定/连接到数据库.

然后我们定义 _process_item() 来获取参数, _item_ 和 _spider_. 我们建立了一个同数据库的会话, 然后打开一个我们的Hn()模型中的数据项. 然后我们通过电泳session.add()来将Hn 添加到我们的数据库中– 在这一步, 它还没有被保存到数据库中– 它仍然处于SQLAlchemy 级别. 然后, 通过调用session.commit(), 它就将被放入数据库中,过程也将会被提交.



我们这里几乎还没有向settings.py 中添加一个变量来告诉抓取器在处理数据时到哪里去找到我们的管道.

那就在 settings.py加入另外一个变量,ITEM_PIPELINES:

1
2
3
ITEM_PIPELINES={
'hn.pipelines.HnPipeline':300
}

这就是我们刚才所定义管道的目录/模块的路径.

现在我们就可以将所有抓取到的数据放到我们的数据库中, 让我们试试看我们获取到了什么,
再一次运行 crawl命令,并一直等到所有的处理过程完毕为止.

万岁!我们现在已经成功地把我们所抓取到的数据存入了数据库.
使用Scrapy建立一个网站抓取器

定时任务

如果我们不得不定期手动去执行这个脚本,那将会是很烦人的. 所有这里需要加入定时任务.
定时任务将会在你指定的任何时间自动运行. 但是! 它只会在你的计算机处在运行状态时 (并不是在休眠或者关机的时候),并且特定于这段脚本需要是在和互联网处于联通状态时,才能运行. 为了不管你的计算机是出在何种状态都能运行这个定时任务, 你应该将hn 代码 和bash 脚本,还有 cron 任务放在分开的将一直处在”运行“状态的服务器上伺服.

总结

这是有关抓取的最简短小巧的教程,而scrapy拥有提供高级功能和可用性的更多特性.

Github下载整个源代码.