scrapy学习和尝试

最近在学python,用的是2.7版,跟着网上的一些例子在学习,使用urllib和urllib2去抓取网页,然后学着学着就发现了scrapy这个爬虫框架。在这里记录一些东西,以免自己忘记。

最终目的是想爬取淘女郎网站上的每个mm主页(比如:田媛媛)下的所有展示图片。不过最后涉及到淘宝登录以及触发反爬虫策略,并没有成功。

最开始是看到这篇文章:Python爬虫实战四之抓取淘宝MM照片,用他的代码跑了一下,感觉很爽,可以用mm名称自动建目录,然后保存图片;但是爬了一会就报错了,当时没记录错误输出,但是大概意思是找不到response页面里的html代码,正则表达式匹配不到。通过一些了解,知道了原因:有些html代码是动态js产生的,而python代码没有去执行js,所以就获取不到。如果用chrome浏览器的审查元素能看到代码,但是直接打开网页源代码却没有的话,也是这个原因。

那么问题来了,怎么让爬虫也执行js?通过搜索,了解到selenium和PhantomJS。selenium是自动化测试经常用到的,python可以用它调用浏览器,模拟人的操作。因为调用浏览器是会执行js的,那么问题就应该可以解决了。然后发现效率真的很低,因为每次调用都要打开浏览器,等页面完全加载完成。PhantomJS是一个headless browser,意思是不需要界面,那么效率就应该提高很多了,然后发现selenium和PhantomJS已经整合在一起了,真是明智啊。

于是尝试在之前的代码中加入selenium和PhantomJS,开始爬图片,然而遇到另一个问题:访问mm主页需要淘宝登录。同时,图片又不需要通过js加载了,oh my god,是不是我之前眼花了。

然后又看了这篇文章:Python爬虫实战五之模拟登录淘宝并获取所有订单,于是尝试将登录的代码加入到前面。现在应该可以了吧,运行看看。靠,又遇到新问题了:登录是成功的,但是访问mm主页还是要登录,这是为啥?当时是不知道,就放弃了,后来了解到应该是和cookie有关。

接着,我安装了scrapy,学习了几个例子,也抓取了一些简单的网页,发现如果不需要登录,网页也没有动态js加载的问题,scrapy真是太好用了,代码量非常少就可以完成。然后还是不死心,想用scrapy尝试解决前面的问题。

items.py

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class TaobaommItem(scrapy.Item):
    # define the fields for your item here like:
    mm_name = scrapy.Field()
    image_urls = scrapy.Field()
    images = scrapy.Field()

上面的代码,是配置了需要抓取的mm姓名、图片url等,后面使用了ImagesPipeline来下载图片,所以image_urlsimages是必须的。

settings.py

# -*- coding: utf-8 -*-
BOT_NAME = 'taobaomm'

SPIDER_MODULES = ['taobaomm.spiders']
NEWSPIDER_MODULE = 'taobaomm.spiders'
ITEM_PIPELINES = {'taobaomm.tb_pipelines.getmmimgPipeline': 1}
IMAGES_STORE = '/vagrant/taobaomm/images'
USER_AGENT_LIST = [
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
    "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
    "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
    "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
    "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
    "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
    "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
    "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
    "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
    "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
    "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
    "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
    "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
    "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
    "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]
HTTP_PROXY = 'http://127.0.0.1:8123'
DOWNLOADER_MIDDLEWARES = {
    'taobaomm.middlewares.RandomUserAgentMiddleware': 400,
    'taobaomm.middlewares.ProxyMiddleware': 410,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None
    # Disable compression middleware, so the actual HTML pages are cached
}

在上面的代码,配置了自定义的getmmimgPipeline,继承自ImagesPipeline;另外,使用了下载中间件,主要作用是使用随机的ua和ip去访问,后面会提到。

tb_pipelines

# -*- coding: utf-8 -*-

from scrapy.pipelines.images import ImagesPipeline
from scrapy import Request
import hashlib
# import sys

# reload(sys)
# sys.setdefaultencoding('gbk')


class getmmimgPipeline(ImagesPipeline):

    def get_media_requests(self, item, info):
        print item['mm_name'] + '*'*50
        # 因为在下面的file_path方法中获得不到mm的姓名,所以在这里把mm的姓名作为meta传过去
        return [Request(image_url, meta={'mm_name': item['mm_name']}) for image_url in item['image_urls']]

    def file_path(self, request, response=None, info=None):
        image_guid = hashlib.sha1(request.url).hexdigest()
        path = 'full/%s/%s.jpg' % (request.meta['mm_name'], image_guid)
        return path

在以上代码,重写了get_media_requests()file_path()方法,作用是获取mm_name,然后保存图片的时候将保存在各自的子目录下。

example.py

# -*- coding: utf-8 -*-
import scrapy
from taobaomm.items import TaobaommItem
import time
import os
from selenium import webdriver
from taobaomm.logintotaobao import loginToTaobao
# import sys

# reload(sys)
# sys.setdefaultencoding('gbk')

class ExampleSpider(scrapy.Spider):
    name = "example"
    #allowed_domains = ["mm.taobao.com"]
    start_urls = (
        "https://mm.taobao.com/json/request_top_list.htm",
    )

    pageindex = 1
    #taobao = loginToTaobao()
    #cookiejar = taobao.loginToTaobao()

    def parse(self, response):

        for sel in response.xpath('//div[@class="list-item"]'):

            name = sel.xpath(
                './/a[@class="lady-name"]/text()').extract()[0]
            print u'美眉姓名:', name
            self.mkdir('images/full/%s' % (name))
            item = TaobaommItem()
            item['mm_name'] = name
            href = sel.xpath('.//a[@class="lady-avatar"]/@href').extract()
            url = response.urljoin(href[0])
            yield scrapy.Request(url,
                                 meta={
                                 #'cookiejar': response.meta['cookiejar'],
                                 'item': item},
                                 callback=self.parse_mm_page)
            print u'去美眉图片页抓图:', url
            #time.sleep(10)
        self.pageindex += 1
        next_page = self.start_urls[0] + '?page=' + str(self.pageindex)
        yield scrapy.Request(next_page,
                             #meta={'cookiejar': response.meta['cookiejar']},
                             callback=self.parse)
        #time.sleep(60)

    def parse_mm_page(self, response):

        print u'开始下载图片'
        item = response.meta['item']
        sel = response.xpath('//div[@class="mm-aixiu-content"]')
        items = []
        for href in sel.xpath('.//img/@src').extract():
            items.append('http:' + href)
        item['image_urls'] = items
        return item

    # 创建新目录
    def mkdir(self, path):
        path = path.strip()
        # 判断路径是否存在
        # 存在     True
        # 不存在   False
        isExists = os.path.exists(path)
        # 判断结果
        if not isExists:
            # 如果不存在则创建目录
            print u"偷偷新建了名字叫做", path, u'的文件夹'
            # 创建目录操作函数
            os.makedirs(path)
            return True
        else:
            # 如果目录存在则不创建,并提示目录已存在
            print u"名为", path, '的文件夹已经创建成功'
            return False

关于淘宝登录的代码,这里就不贴出来了,采用了别人的,主要是这两篇:

执行scrapy,先登录淘宝

taobao = loginToTaobao()
cookiejar = taobao.loginToTaobao()

以上example.py代码是爬虫的实现,因为创建项目的时候用了scrapy startproject example,所以文件名叫example.py,呵呵。

我的想法是这样的:先请求一级页面,然后抓取里面的第一个mm姓名和主页url,主页url就是二级页面,然后请求二级页面,抓取里面的所有图片,然后下载。然后再在一级页面获取第二个mm姓名和主页url,继续。。。

我发现执行的时候是这样的:scrapy不会获取了第一个mm,就马上去请求她的主页,而是会阻塞住,接着获取第二个mm,在一级页面循环获取到所有mm姓名和主页url后,然后统一交给scrapy调度,接着却不一定从第一个mm开始访问二级页面。这样就存在一个问题:scrapy的调度会很快触发淘宝反爬虫策略,会302重定向到sec.taobao.com的页面去,要求输入验证码,或者302重定向到login.taobao.com,提示输入登录密码。

另一个要说的,是cookie传递的问题,登录淘宝后,我使用cookiejar = taobao.loginToTaobao()将cookie传入scrapy,一定要从第一个请求:start_requests()方法产生的一级页面请求时就加入cookie(虽然一级页面不需要cookie),比如是这样:

def start_requests(self):
    taobao = loginToTaobao()
    cookiejar = taobao.loginToTaobao()
    yield scrapy.Request(self.url,
                         meta={'cookiejar': cookiejar},
                         callback=self.parse_list
                         )

这里和example.py里面的代码不同,只是说明我曾经尝试过。

在请求二级页面时,需要接收前面的cookie,用meta={'cookiejar': response.meta['cookiejar']}。如果不这样的话,会要求登录。

其实我也不确定是对的,因为在抓取图片时,是另一个域名,cookie带不过去,也不用传。

middlewares.py

# -*- coding: utf-8 -*-

import os
import random
from scrapy.conf import settings


class RandomUserAgentMiddleware(object):

    def process_request(self, request, spider):
        ua = random.choice(settings.get('USER_AGENT_LIST'))
        if ua:
            request.headers.setdefault('User-Agent', ua)


class ProxyMiddleware(object):

    def process_request(self, request, spider):
        request.meta['proxy'] = settings.get('HTTP_PROXY')

以上代码是取自这篇文章:

其实,scrapy本身也有模拟登录的方法,叫scrapy.FormRequest.from_response(),我也做过尝试,还未成功。

综上所述,主要问题还是集中在:登录、触发反爬虫策略、验证码(目前只能手动输入)、302重定向如何处理。我还在尝试中。

by zhuhangyu Aug 23, 2015