Scrapy爬虫笔记之基本流程介绍

Scrapy爬虫笔记之基本流程介绍

 

本博客地址【http://blog.csdn.net/xiantian7】


1.一个项目的基本流程

创建新项目

scrapy startproject <name>

会自动生成目录:

熟悉一下目录结构:

lawson
├── lawson
│ ├── __init__.py
│ ├── items.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│   └── __init__.py
└── scrapy.cfg
  • items.py 定义抓取结果中单个项所需要包含的所有内容,比如便利店的地址、分店名称等。【目标】
  • pipelines.py 定义如何对抓取到的内容进行再处理,例如输出文件、写入数据库等。 【处理】
  • settings.py 是 scrapy 的设置文件,可对其行为进行调整。【设置】
  • spiders 目录下存放写好的 spider,也即是实际抓取逻辑。【工具】
  • scrapy.cfg 是整个项目的设置,主要用于部署 scrapyd 服务,本文不会涉及。

各个部分

核心:scrapy 中最为重要的部分就是spider。它包含了分析网页与抓取网页数据的具体逻辑,也就是说对网页上任何内容的任何处理都在 spider中实现。因此,这是 scrapy 整个框架的核心。

 

item【是用来装搜索得到数据的容器

Items将会用下载来的数据填满,要创建我们自己的item的时候, 需要

1、导入scrapy.item.Item

2、定义scrapy.item.Field ,通过我们自己的定义,我们就为我们的容器定制了一个”模型“

 

from scrapy.item import Item, Field


class ConvStore(Item):
  name = Field()
  branch = Field()
  alias = Field()
  address = Field()
  city = Field()
  district = Field()
  longitude = Field(serializer=float)
  latitude = Field(serializer=float)

这里定义了一个便利店(ConvStore)所应包含的内容(Field ),会在spider 中用到,用来承载其抓取下来的实际数据。
scrapy的 Item类的行为类似于Python里面的dictionary,你能从中获取key和value。


spider 【数据抓取的主要逻辑】

在spider(名字可以自己定)这个文件中初始化了将要下载的URL,定义了怎么跟踪这些链接,怎么提取每个链接下页面内容有用数据。

如果需要创建一个我们自己的抓取类(spider),需要:

1、继承scrapy.spider.Spider【下面的例子是继承的更强大的基类,所以方式稍有区别,但是都是需要继承一个基类】

2,定义name:【spider的标识】; start_urls【一个需要爬取的链接起始列表】;parse()【这个方法当url的每个下载Response对象产生是被调用,Response是传入这个方法的唯一参数】——例子中自己定义了一个方法,并指定为回调函数,作用是一样的

parse() 方法是用来处理Response对象,返回爬取得数据(以前面定义的Item类型返回),并且获得更多等待爬取的链接

新版的例子

from scrapy.spider import Spider
class DmozSpider(Spider):
  name = "dmoz"
  allowed_domains = ["dmoz.org"]
  start_urls = ["http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
         "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"]
  def parse(self, response):
    filename = response.url.split("/")[-2]
    open(filename, ’wb’).write(response.body)
这上面都干嘛了:

1、scarpy 为每一个在start_urls中的url创建了一个对应的crapy.http.Request请求,并把这个请求放到前面讲的【任务队列】里面,并指定parse方法来处理这些请求,parse为回调函数

2、这些请求被执行,scrapy.http.Response作为执行结果被返回,通过parse()返回这个捕捉类【spider】


需要从页面中提取我们需要的数据,需要用到scrapy中的寻找机制,这种机制基于XPath or CSS,在scrapy里面被称为Scrapy Selectors

这是一些XPath表达式的例子和他们的含义

  • /html/head/title: 选择HTML文档<head>元素下面的<title> 标签。
  • /html/head/title/text(): 选择前面提到的<title> 元素下面的文本内容
  • //td: 选择所有 <td> 元素
  • //div[@class="mine"]: 选择所有包含 class="mine" 属性的div 标签元素
为了方便使用XPaths,Scrapy提供XPathSelector 类,有两种可以选择,HtmlXPathSelector(HTML数据解析)和XmlXPathSelector(XML数据解析)。

必须通过一个 Response 对象对他们进行实例化操作。

你会发现Selector对象展示了文档的节点结构。因此,第一个实例化的selector必与根节点或者是整个目录有关 。

在Scrapy里面,Selectors 有四种基础的方法

  • xpath():返回一系列的selectors,每一个select表示一个xpath参数表达式选择的节点
  • css():返回一系列的selectors,每一个select表示一个css参数表达式选择的节点
  • extract():返回一个unicode字符串,为选中的数据
  • re():返回一串一个unicode字符串,为使用正则表达式抓取出来的内容

这是一个复杂点的例子,和上面的ITems类是一致的,继承的基类不一样,多出了写更高级的功能

#coding=utf-8
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.selector import HtmlXPathSelector

from lawson.items import ConvStore  #引用自己定义的ConvStore类  


class LawsonSpider(CrawlSpider):
  name = 'lawson'#爬虫名
  start_urls = ['http://www.lawson.com.cn/shops']#spider 的入口,即是告诉它该从哪个网页开始抓取
  allowed_domains = ['lawson.com.cn']# 限定 spider 的抓取活动只能在指定的 domain 中进行
  #rules定义了一系列规则用来匹配网页中出现的内容,并根据规则分发至不同的处理方法中(parse_store_list)
  #SgmlLinkExtractor用来提取链接
  #allow 参数是正则表达式,网页中匹配的链接会被抓取
  #tags 指定从哪些标签抓取链接,默认 ['a', 'area'](通过分析网页 这里不能包含 area,故手动指定
  rules = (
      Rule(SgmlLinkExtractor(allow=r'list\?area_id=\d+', tags='a'),
        callback='parse_store_list'),
      )
  #parse_store_list 方法定义了如何抓取特定网页中的数据
  #HtmlXPathSelector 是一个选择器,使用它能方便地定位到网页中的某个位置并抓取其中内容
  def parse_store_list(self, response):
    hxs = HtmlXPathSelector(response)
    store_selectors = hxs.select('//div[@class="ShopList"]/table/tr')[1:]

    for s in store_selectors:
      store = ConvStore()
      store['name'] = u'罗森'
      store['alias'] = u'Lawson'
      store['branch'] = s.select('th/p/text()').extract()
      store['address'] = s.select('td/span/text()').extract()
      store['district'] = response.meta['link_text']
      store['city'] = u'上海'

      yield store.load_item()

 

"XPath 是一种用于从XML选择节点的语言,它也可以被用于HTML"。

我们一般会基于一个定义好的Xpath来告诉 scrapy 到哪里去开始寻找数据. 让我们浏览我们的 Hacker News 站点

'//td[@class="title"]' 是在说: 所有的 <td> 元素中, 如果一个 <a class="title"></a> 被展现了出来,那就到 <td> 元素里面去寻找那个拥有一个被称作title的类型的<a>元素.

.

 

 

CrawlSpider

这个类是整个抓取逻辑的基础,他的工作流程如下:

  1. 若有start_urls,则从这些 URL 开始抓取,若没有,则执行start_requests方法(用户须定义),并请求该方法返回的Request 对象,并从这些请求结果中开始抓龋
  2. 所有网页请求返回的 Response 默认交给 parse 方法处理。
    • parse方法在CrawlSpider 的默认实现是用已定义的rules 对获得的网页内容进行匹配并进行由Rule 所指定的进一步处理(即交给callback参数所指定的callable 去处理)。
    • 若不指定 callback, Rule 的默认处理是对匹配的网址发起请求,并再次交给parse
  3. 任何方法中返回的 Item 实例(如示例中的 ConvStore)都会被作为有效数据保存(输出文件等),再处理(Pipeline)。

HtmlXPathSelector

这是一个通过 XPath 对 HTML 页面进行结构化定位和内容读取的工具。scrapy 使用它定位到网页中用户所需要的数据并进行抓龋


def parse_store_list(self, response):
  hxs = HtmlXPathSelector(response)
  store_selectors = hxs.select('//div[@class="ShopList"]/table/tr')[1:]

  for s in store_selectors:
    ...
    store.add_value('branch', s.select('th/p/text()').extract())
    store.add_value('address', s.select('td/span/text()').extract())
    ...

  • HtmlXPathSelector需要一个Response对象来实例化。
  • //div[@class="ShopList"]/table/tr 选择了所有包含罗森门店信息的tr 标签。
  • s.select('th/p/text()').extract() 在之前的选择基础上继续对其子标签做选择,这里就确实得选择到了分店名。extract() 则将该标签的文本数据读取出来。
  • HtmlXPathSelector还有正则表达式接口,后文会提到。

Field, Item 及 Item Loader

Field 仅仅是一个 dict 的 wrapper 类,因此使用方法与dict 完全一样,在scrapy 中它负责声明单个 Item 的字段及该字段的各种行为(如序列化方法serializer)。

Item 用 Field 定义了单个有效数据的具体字段,而实际中则是主要有两种方法写入数据:

  1. 使用其类似dict 的接口进行数据的写入和读取,key 为字段名。
  2. 使用 Item Loader

dict 接口的用法如上所示很简单,这里说一下 Item Loader。

from scrapy.contrib.loader import ItemLoader
from scrapy.contrib.loader.processor import Compose

from lawson.items import ConvStore


class StoreLoader(ItemLoader):
  default_output_processor = Compose(lambda v: v[0], unicode.strip)

  def branch_in(self, values):
    for v in values:
      v = v.strip()
      yield v + u'店' if not v.endswith(u'店') else v


class LawsonSpider(CrawlSpider):
  ...

  def parse_store_list(self, response):
    hxs = HtmlXPathSelector(response)
    store_selectors = hxs.select('//div[@class="ShopList"]/table/tr')[1:]

    for s in store_selectors:
      store = StoreLoader(item=ConvStore(), response=response)
      store.add_value('name', u'罗森')
      store.add_value('alias', u'Lawson')
      store.add_value('branch', s.select('th/p/text()').extract())
      store.add_value('address', s.select('td/span/text()').extract())
      store.add_value('district', response.meta['link_text'])
      store.add_value('city', u'上海')

      yield store.load_item()

Item Loader 的主要作用是对抓取数据的各个字段进行特殊处理,在这里我们定义了一个StoreLoader 类继承(Inherit)自ItemLoader

  • default_output_processor定义默认的输出处理器,这里我们对抓取的数据值进行strip 操作。
  • branch_in 方法是对 branch 字段的特殊处理,它发生在输入的时候,也就是刚抓取到数据之后。这里的处理是为没有这个字的分店名补上这个字。
  • <field>_in<field>_out 会各对指定字段做一次处理,前者是在刚抓取到数据时,后者是在最终输出之前,用户根据需要定义相应方法。
    • scrapy 有一些 built-in processor可以直接使用,进行一些通用处理。
  • add_value将值赋予相应字段,很好理解。
  • load_item返回该条填充过数据的 Item。

使用 Item Loader 的好处显而易见,我们有一个统一的地方对所有数据字段进行处理,不用将其混入抓取逻辑,使整个流程分工明确。【统一处理】

另一个常用的 Item Loader 是 XPathItemLoader,显然这个版本利用了 XPath:

 

store = XPathItemLoader(item=ConvStore(), response=response)
store.add_xpath('branch', '//div[@class="ShopList"]/table/tr[2]/th/p/text()')

它将字段与 XPath 表达式关联起来,直接完成定位、读取和写入数据的操作,很方便。

 

加上经纬度

经纬度对于定位一个地点是很有用的,通过电子地图能够精确地定位至相关地点。我发现罗森网站提供了这个信息,但它并未明文显示,而是需要通过其所链接到的百度地图的页面中去抓取下来,听起来很麻烦,但实际却很简单。

 

from urlparse import urljoin

from scrapy.contrib.loader import XPathItemLoader
from scrapy.http import Request
from scrapy.selector import HtmlXPathSelector
from scrapy.utils.response import get_base_url

from poi_scrape.items import ConvStore


class StoreLoader(XPathItemLoader):
  ...

class LawsonSpider(BasePoiSpider):
  ...

  def parse_geo(self, response):
    hxs = HtmlXPathSelector(response)
    store = response.meta['store']

    lng, lat = hxs.re(r'(\d+\.\d+),(\d+\.\d+)')
    store.add_value('latitude', lat)
    store.add_value('longitude', lng)
    return store.load_item()

  def parse_store_list(self, response):
    ...

    for s in store_selectors:
      store = StoreLoader(item=ConvStore(), response=response)
      ...

      map_rel_url = s.select('td/a/@rel').extract()
      if map_rel_url:
        map_url = urljoin(get_base_url(response), map_rel_url[0])
        req = Request(map_url, callback=self.parse_geo)
        req.meta['store'] = store
        yield req
      else:
        yield store.load_item()

这里为 LawsonSpider 新增了一个方法 parse_geo,同时改写了parse_store_list

  • parse_store_list 的循环中我抓取每个店的tr 标签中的td/a/@rel 属性(Attribute)(这里@rel 表示a 标签的rel 属性),若有这一属性则对这个地图的链接发起请求,即yield req
    • 在 scrapy 中,spider 类中的方法若返回 Request 实例则 scrapy 会自动对该Request 包含的 URL 发出请求,并将其返回的结果封装为 Response 后交给callback 参数中指定的方法处理,若未指定callback,则交给parse 方法处理。
  • req.meta['store'] = store,每个 Request 有一个预定义的 meta 属性(dict),保存在其中的值在其对应的 Response 中可以再次取出:store = response.meta['store']。【meta:英文意思为在其中】
  • hxs.re(r'(\d+\.\d+),(\d+\.\d+)') 使用了 HtmlXPathSelector 的正则表达式接口直接从网页中通过正则表达式匹配抓取数据。

完整 spider 代码

 

 

 

from urlparse import urljoin

from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.contrib.loader import XPathItemLoader
from scrapy.contrib.loader.processor import Compose
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.http import Request
from scrapy.selector import HtmlXPathSelector
from scrapy.utils.response import get_base_url

from lawson.items import ConvStore


class StoreLoader(XPathItemLoader):
  default_output_processor = Compose(lambda v: v[0], unicode.strip)

  def branch_in(self, values):
    for v in values:
      v = v.strip()
      yield v + u'店' if not v.endswith(u'店') else v


class LawsonSpider(CrawlSpider):
  name = 'lawson'
  start_urls = ['http://www.lawson.com.cn/shops']
  allowed_domains = ['lawson.com.cn']
  rules = (
      Rule(SgmlLinkExtractor(allow=r'list\?area_id=\d+', tags='a'),
        callback='parse_store_list'),
      )

  def parse_geo(self, response):
    hxs = HtmlXPathSelector(response)
    store = response.meta['store']

    lng, lat = hxs.re(r'(\d+\.\d+),(\d+\.\d+)')
    store.add_value('latitude', lat)
    store.add_value('longitude', lng)
    return store.load_item()

  def parse_store_list(self, response):
    hxs = HtmlXPathSelector(response)
    store_selectors = hxs.select('//div[@class="ShopList"]/table/tr')[1:]

    for s in store_selectors:
      store = StoreLoader(item=ConvStore(), response=response)
      store.add_value('name', u'罗森')
      store.add_value('alias', u'Lawson')
      store.add_value('branch', s.select('th/p/text()').extract())
      store.add_value('address', s.select('td/span/text()').extract())
      store.add_value('district', response.meta['link_text'])
      store.add_value('city', u'上海')

      map_rel_url = s.select('td/a/@rel').extract()
      if map_rel_url:
        map_url = urljoin(get_base_url(response), map_rel_url[0])
        req = Request(map_url, callback=self.parse_geo)
        req.meta['store'] = store
        yield req
      else:
        yield store.load_item()





 

文章参考:

1、http://kanedou.me/2012/07/introducing-python-scrapy-part-i/

2、http://blog.csdn.net/pleasecallmewhy/article/details/19642329

3、http://www.xefan.com/archives/83883.html

4、http://www.linuxeden.com/html/news/20140102/147107.html 【提到了将数据存储在数据库中】