zoukankan      html  css  js  c++  java
  • python 爬虫 学习笔记(一)Scrapy框架入门

    沉迷于通过高效算法及经典数据结构来优化程序的时候并不理解,为什么多线程可以优化爬虫运行速度?原来是程序特性所决定的:传统算法的程序复杂度主要来源于计算,但网络程序的计算时间可以忽略不计,网络程序所面临的挑战打开很多很慢的链接,或者说,是如何有效的等待大量网络事件。

    (1)简单的socket爬虫:

    直接下载一个页面

    import socket
    
    
    def threaded_method():
        sock = socket.socket()
        sock.connect(('xkcd.com', 80))
        request = 'GET /353/ HTTP/1.0
    Host: xkcd.com
    
    '
        sock.send(request.encode('ascii'))
        response = b''
        chunk = sock.recv(4096)
        while chunk:
            response += chunk
            chunk = sock.recv(4096)
            #print(chunk)
    
        print(response)
    
    threaded_method()
    

     每次通过recv向字节流读取4096字节的数据。当然默认情况下recv和connect都是阻塞的。这样我们每下载一个页面就需要一个线程。线程开销是昂贵的,很可能在页面处理完之前就用光了线程。这是裸的套接字写法,为了接下来的编写和学习舒服一点,果然还是来用几个python包吧。

    (2)scrapy框架

    官方文档:Scrapy框架简单介绍

    这里记录下scrapy的基本操作

    创建一个scrapy项目:scrapy startproject tutorial

    在你的工作文件夹下输入此命令,tutorial文件夹就出现了。第一感觉文件很少很清爽。

    定义item:vi items

    编辑tutorial中的items文件。items是你保存爬取数据的容器。我们需要根据你程序获取的内容对item进行建模,比如我们需要title,link,desc这三个字段

    import scrapy
    
    class DmozItem(scrapy.Item):
        title = scrapy.Field()
        link = scrapy.Field()
        desc = scrapy.Field()
    

     普通爬取

    在tutorial/spiders目录下创建dmoz_spider.py,比如代码写成这样

    import scrapy
    
    class DmozSpider(scrapy.Spider):
        name = "dmoz"
        allowed_domains = ["dmoz.org"]
        start_urls = [
           "https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051"
        ]
    
        def parse(self, response):
            filename = response.url.split("/")[-2]
            with open(filename, 'wb') as f:
                f.write(response.body)

     你必须继承自scrapy.Spider,并定义如下三个属性

    • name: 用于区别Spider。 该名字必须是唯一的,您不可以为不同的Spider设定相同的名字。

    • start_urls: 包含了Spider在启动时进行爬取的url列表。 因此,第一个被获取到的页面将是其中之一。 后续的URL则从初始的URL获取到的数据中提取。

    • parse() 是spider的一个方法。 被调用时,每个初始URL完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数。 该方法负责解析返回的数据(response data),提取数据(生成item)以及生成需要进一步处理的URL的 Request 对象

      进入项目根目录执行命令启动爬虫:scripy crawl dmoz  

      你也可以加个scripy crawl dmoz  --nolog关闭日志信息,这样输出页面就变得清爽了

    (python是讲究格式的,start_url列表里格式里有坑)

    (3)爬虫、网页分析解析辅助工具 

    Xpath-helper

    谷歌浏览器插件:xpath-helper

    安装好之后,我们重新打开浏览器,按ctrl+shift+x就能调出xpath-helper框了。

    如果我们要查找某一个、或者某一块元素的xpath路径,可以按住shift,并移动到这一块中,上面的框就会显示这个元素的xpath路径,右边则会显示解析出的文本内容,并且我们可以自己改动xpath路径,程序也会自动的显示对应的位置,可以很方便的帮助我们判断我们的xpath语句是否书写正确。

    啊忘记说了scrapy的选择器是基于css+xpath的,可能在应用scrapy的时候大部分时间都用在xpath的构建上。而这个插件很有用

    虽然这个小插件使用非常方便,但它也不是万能的,有两个问题:

      1.XPath Helper 自动提取的 XPath 都是从根路径开始的,这几乎必然导致 XPath 过长,不利于维护;

      2.当提取循环的列表数据时,XPath Helper 是使用的下标来分别提取的列表中的每一条数据,这样并不适合程序批量处理,还是需要人为修改一些类似于*标记等。

     Scrapy shell

    scrapy shell “https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051”
    以上命令执行后,会使用Scrapy downloader下载指定url的页面数据,并且打印出可用的对象和函数列表

    这对于想要练习xpath使用的人来说实在是棒得不行,不需要每次浪费时间爬一次才发现xpath没写对,你可以充分的验证你xpath的正确性然后再开启爬虫显然省了很多时间

    xpath编写实例

    到底怎么编写xpath才好呢我也思索了半天。谷歌浏览器有一个copy xpath功能(在f12模式下选中元素右键可以看到),多玩了几次发现它总是优先找路径上是否有Id选择器,显然copy xpath的制作者利用了一个html页面的设计规则--id选择器的属性是唯一对应的,找指定元素优先找id选择器是非常方便的。比如

    import scrapy
    from tutorial.items import TutorialItem
    
    class DmozSpider(scrapy.Spider):
        name = "amoz"
        allowed_domains = ["amazon.cn"]
        start_urls = [
            "https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051"
        ]
    
        def parse(self, response):
            for sel in response.xpath('//*[@id="a-page"]/div[4]/div/div[2]/div/div[1]/div[1]/ul[2]'):
                item = TutorialItem()
    
                item['link'] = sel.xpath('li[2]/a/@href').extract()
                #link = sel.xpath('a/@href').extract()
                #print title,price,desc
                #print item['title'][0],item['price'][0],item['desc'][0]
                print item['link'][0]
                #yield item

     这里的path就是谷歌浏览器拷下来的一个路径,然后xpath嵌套使用从而提取路径。

    但是显然我们只抓一个指定元素这种情况用得很少,然后再尝试抓取一组相似的目标。这里选取了亚马孙文学图书页面,试图抓取标题,价格,评论数。

    然后发现这些元素的属性会随着分类变化,比如电子书跟实体书标题属性不一样,折扣商品的价格属性又和不折扣的商品不一样(我真是曰了苟了这源码这么乱的吗,似乎只有分类讨论来解决?这样的话又会写很多,暂时没有想到好方法)

    import scrapy
    from tutorial.items import TutorialItem
    from scrapy.http import Request
    
    class DmozSpider(scrapy.Spider):
        name = "amoz"
        allowed_domains = ["amazon.cn"]
        start_urls = [
            "https://www.amazon.cn/b/ref=amb_link_9?ie=UTF8&node=659379051&pf_rd_m=A1AJ19PSB66TGU&pf_rd_s=merchandised-search-leftnav&pf_rd_r=04N93678VRYT3BZZZWFA&pf_rd_r=04N93678VRYT3BZZZWFA&pf_rd_t=101&pf_rd_p=4b8b99d4-2fd8-44c5-96ff-dbc77bd19498&pf_rd_p=4b8b99d4-2fd8-44c5-96ff-dbc77bd19498&pf_rd_i=658394051"
        ]
    
        def parse(self, response):
            #item['link'] = response.xpath('//a[@target="_blank"]/@href').extract()
            url_list = response.xpath('//a[@target="_blank"]/@href').extract()
            for url in url_list:
                yield Request(url,callback=self.parse_name)
                #print title,price,desc
                #print item['title'][0],item['price'][0],item['desc'][0]
            for i in range(2,75):
                page_url = 'https://www.amazon.cn/s/ref=lp_659379051_pg_{}?rh=n%3A658390051%2Cn%3A%21658391051%2Cn%3A658394051%2Cn%3A658511051%2Cn%3A659379051&page=2&ie=UTF8&qid=1497358151&spIA=B01FTXJZV2'.format(i)
                yield Request(page_url, callback=self.parse_name)
        def parse_name(self,response):
            item = TutorialItem()
            item['title'] = response.xpath('//*[@id="productTitle" or @id="ebooksProductTitle"]/text()').extract()
            item['descnum'] = response.xpath('//*[@id="acrCustomerReviewText"]/text()').extract()
            item['price'] = response.xpath('//span[@class="a-size-medium a-color-price inlineBlock-display offer-price a-text-normal price3P" or @class="a-color-price a-size-medium a-align-bottom"]/text()').extract()
            item['link'] = response.url
            yield item
    

      

    (4)将爬取的内容存入数据库

    当Item在Spider中被收集之后,它将会被传递到Item Pipeline,一些组件会按照一定的顺序执行对Item的处理。

    每个item pipeline组件(有时称之为“Item Pipeline”)是实现了简单方法的Python类。他们接收到Item并通过它执行一些行为,同时也决定此Item是否继续通过pipeline,或是被丢弃而不再进行处理。

    以下是item pipeline的一些典型应用:

    • 清理HTML数据
    • 验证爬取的数据(检查item包含某些字段)
    • 查重(并丢弃)
    • 将爬取结果保存到数据库中

    显然在pipeline中对数据进行筛选并存储非常的稳

    出于各种考虑(安全性,维护性),我们一般不将数据库的信息写在源码里,而是像这样写在settings.py里

    #Mysql 配置
    MYSQL_HOST = '127.0.0.1'
    MYSQL_DBNAME = 'amazon'         #数据库名字,请修改
    MYSQL_USER = 'root'             #数据库账号,请修改
    MYSQL_PASSWD = '123456'         #数据库密码,请修改
    
    MYSQL_PORT = 3306

     然后从setting中加载数据库信息

    # -*- coding: utf-8 -*-
    
    # Define your item pipelines here
    #
    # Don't forget to add your pipeline to the ITEM_PIPELINES setting
    # See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
    from twisted.enterprise import adbapi
    from scrapy.exceptions import DropItem
    import MySQLdb
    import MySQLdb.cursors
    import codecs
    import json
    
    class TutorialPipeline(object):
        def __init__(self,dbpool):
            self.dbpool=dbpool
            self.ids_seen=set()
    
        def process_item(self, item, spider):
            #print '----------------'
            #print u'标题' + item['title'][0]
            #print u'评论' + item['descnum'][0]
            #print u'价格' + item['price'][0]
            if item['title'] and item ['descnum'] and item['price']:
    
                query = self.dbpool.runInteraction(self._conditional_insert, item)  # 调用插入的方法
                query.addErrback(self._handle_error, item, spider)  # 调用异常处理方法
                return item
            else:
                raise DropItem("Missing price in %s" % item)
    
        @classmethod
        def from_settings(cls, settings):
               #1、@classmethod声明一个类方法,而对于平常我们见到的则叫做实例方法。
               #2、类方法的第一个参数cls(class的缩写,指这个类本身),而实例方法的第一个参数是self,表示该类的一个实例
               #3、可以通过类来调用,就像C.f(),相当于java中的静态方法
            dbparams = dict(
                host=settings['MYSQL_HOST'],  # 读取settings中的配置
                db=settings['MYSQL_DBNAME'],
                user=settings['MYSQL_USER'],
                passwd=settings['MYSQL_PASSWD'],
                charset='utf8',  # 编码要加上,否则可能出现中文乱码问题
                cursorclass=MySQLdb.cursors.DictCursor,
                use_unicode=False,
            )
            dbpool = adbapi.ConnectionPool('MySQLdb', **dbparams)  # **表示将字典扩展为关键字参数,相当于host=xxx,db=yyy....
            return cls(dbpool)  # 相当于dbpool付给了这个类,self中可以得到
    
        def _conditional_insert(self, tx, item):
            # print item['name']
            sql = "insert into testtable(title,descnum,price) values(%s,%s,%s)"
            params = (item["title"], item["descnum"],item['price'])
            tx.execute(sql, params)
        def _handle_error(self, failue, item, spider):
            print '--------------database operation exception!!-----------------'
            print '-------------------------------------------------------------'
            print failue
    

      

    如此就成功存储了

  • 相关阅读:
    [Java]用于将链表变成字符串并在元素之间插入分隔符的有用函数“String.join”
    Sql语法树示例 select username, ismale from userinfo where age > 20 and level > 5 and 1 = 1
    [Java]一段尚未雕琢的分词代码
    day44_Oracle学习笔记_03
    day43_Oracle学习笔记_02
    WinXP系统中的Oracle数据库如何以管理员身份登录
    Oracle 10G安装指导
    20个Linux服务器性能调优技巧
    Linux 上使用 Gmail SMTP 服务器发送邮件通知
    Netdata Linux下性能实时监测工具
  • 原文地址:https://www.cnblogs.com/bitch1319453/p/6921822.html
Copyright © 2011-2022 走看看