Python 网络爬虫实战:使用 Scrapy + MongoDB 爬取京东网站并部署到云服务器上

1. 京东网站的搜索页是(https://search.jd.com/),它有个比较方便的点,是它不需要用户登陆即可搜索(不像某宝网,必须要登陆后才能搜索)

2. 本文使用的服务器是阿里云服务器(前几天双十一搞活动买的,CentOS系统,1核2G,40G存储空间,自己耍足够了),使用 Putty 可以远程连接服务器。

3. 本文使用的数据库是 MongoDB 数据库,使用 Robo 3T 可以比较方便的查看管理数据库。

4. 本爬虫是学习《PeekpaHub 全栈开发》 教程而开发的,实践后收获很大。

5.  本爬虫使用到的库有 scrapy ,bs4 ,pymongo ,scrapyd-client ,请自行安装。


爬虫部分
一、创建 Scrapy 工程

1. 新建 Scrapy 项目。打开终端(Terminal 或 cmd),使用 cd 命令进入自己要创建工程的目录,然后输入以下命令,创建爬虫项目(如:我的项目名叫 SmartCraneHub )

$ scrapy startproject SmartCraneHub

2.  创建爬虫文件。使用 cd 命令进入项目文件夹中,然后执行以下命令,按照模板创建我们的爬虫文件(如:我的爬虫名叫 SmartHub )

$ cd SmartCraneHub
$ scrapy genspider SmartHub http://www.baidu.com

3. 使用 pycharm 打开 scrapy 项目( pycharm 里的 python 环境请自行设置), 查看目录结构如下。

 创建 Scrapy 工程时,会根据模板自动生成一些文件(图中框出来的),这里简单说一下这些文件时做什么用的:

  • SmartHub.py —— 这个是我们的爬虫文件,主要的爬虫代码均在这里编写。
  • items.py —— 这个是定义爬到的数据的数据结构的,在爬虫爬取到数据之后,需要把数据封装成这里预先定义好的对象。
  • middlewares.py —— 这个是 scrapy 的中间件文件,这里的代码主要是在爬虫运行期间执行的操作,比如添加表头,设置代理等。
  • pipelines.py —— 这个主要是对 item 的处理,比如将数据添加到数据库中,或者写入文件里,这些操作都在这里进行。
  • setting.py —— 这个是 scrapy 框架的配置文件。
  • scrapy.cfg —— 这个是部署 scrapy 的配置文件,和 scrapyd 和 scrapyd-client 一起使用。

4. 编辑爬虫文件,设置初始 url 。打开 SmartHub.py 文件,默认的内容大致如下:(由于我们创建爬虫文件时网址写的是 百度,这里需要自行改成自己要爬的目标 url )

import scrapy

class SmarthubSpider(scrapy.Spider):
    name = 'SmartHub'
    allowed_domains = ['search.jd.com']
    start_urls = ['https://search.jd.com/Search?keyword=%E9%9B%B6%E9%A3%9F&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=lingshi&stock=1&click=0&page=1']

    def parse(self, response):
        pass

5. 到此我们的爬虫程序框架已经完成了,在 Terminal 中执行以下命令,运行爬虫程序:

$ scrapy crawl SmartHub

这里为了方便运行,我们在 scrapy.cfg 文件同级的地方,创建一个 Run.py 文件,写入以下代码:

from scrapy import cmdline

def main():
    cmdline.execute("scrapy crawl SmartHub".split())

if __name__ == '__main__':
    main()

这样,之后每次只要运行 Run.py 文件,即可直接执行我们的爬虫了。


二、分析网页结构

我们这次要爬的是京东商城,零食的页面,网址为:https://search.jd.com/Search?keyword=%E9%9B%B6%E9%A3%9F&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=lingshi&stock=1&click=0&page=1

1. 明确数据结构

观察网站,看看上面有什么我们可以获取的数据。

分析之后,我们决定爬取以下的数据:

  • 零食的品牌
  • 零食的ID
  • 零食的名字
  • 零食的价格
  • 零食的详情页URL
  • 零食的优惠活动
  • 零食的店铺
  • 零食的图片

    2. 目标网页分析

    按 F12 召唤出开发者工具,分析 HTML 页面,定位每一项数据在网页中所在的位置,并确定查找的策略。

    (1)零食的品牌

    可以发现,零食的品牌是在一个

    标签下的标签里,每个标签表示一个品牌,而品牌的名字就存放在标签下 标签的 title 属性里。
    我们有两种思路获取品牌名,一是通过 class 属性获取到标签,然后循环获取其标签下的 标签的
    标签的 id 属性也特别相似,都是 " brand-" 加一串数字,所以可以用正则表达式 'brand-(\w+)' ,根据 id 属性来筛选标签,也可以快速地定位到
    标签。(本爬虫中我使用的是后者)

    (2)零食的 ID

    零食的详细信息放在 class 为 gl-item 的标签下,其中标签有一个叫 data-pid 的属性

    (3)零食的名字

    零食的名字存放在标签下 class 为 p-name 的
    标签里的 标签下的 标签里。

    (4)零食的价格

    零食的价格存放在 class 为 p-price 的

    标签中, 标签之下(由于我们之后要对价格做比较,查看价格的涨跌情况,价格需要存储为 float 型的数据,所以这里我们获取  标签下的价格数字)

    (5)零食的店铺

    零食的店铺存放在 class 为 p-shop 的

    标签下, 标签的内容里。

    (6)零食的图片

    零食的图片 URL 存放在 class 为 p-img 的

    标签下, 标签的 src 属性。不过这里的 url 是不完整的,需要对其进行处理,在前面加上 “http://”, 拼接成一个完整的 url 链接。

    在这里有一个小的坑,就是实际按这种方法获取的时候,程序会报错,说找不到  标签中的 src 这个属性,将整个标签打印出来后发现, 标签里确实没有 src 属性,反而是有一个 source-data-lazy-img 的属性,它的值也是一个图片的 URL,在浏览器中打开也是可以正确获取到图片的。

     查阅资料之后知道,这其实是网页的一种懒加载方式。一般来说,图片的大小都要比纯网页内容大很多,为了保证打开的流畅度,一般都是先加载网页数据,再加载媒体资源,这种方式就是懒加载。所以我们直接通过 source-data-lazy-img 属性获取图片的 URL 即可。

    (7)零食详情页的 URL

    零食详情页的 URL 存放在 class 为 p-img 的

    标签下, 标签的 href 属性值。它跟图片的 URL 一样,有的是不完整的,需要手动添加 “http://” 。

    (8)零食的优惠活动

    零食的优惠活动的信息存放在 class 为 p-icons 的

    标签下, 标签的内容中,但是不同的商品  标签的数量不同,有的甚至没有  标签,所以需要做一个判断。获取

    标签下所有的  标签,如果没有  标签,则该字节为空,若有  标签,则将所有  标签的内容拼接成一个字符串。

    三、编写代码环节

    1. 爬取品牌列表

     打开 SmartHub.py 文件,在 parse 函数下开始编写我们的爬虫。首先获取品牌的列表。

      def parse(self, response):
            content = response.body
            soup = BeautifulSoup(content, "html.parser")
            brand_temp_list = soup.find_all('li', attrs={'id': re.compile(r'brand-(\w+)')})
            brand_list = list()
    
            for item in brand_temp_list:
                brand_title = item.find('a')['title']
                brand_list.append(re.sub("[A-Za-z0-9\!\%\[\]\,\。\(\)\(\)\"\.\'\ ]", "", brand_title))
                # brand_list.append(brand_title)
    

     商品的品牌信息并不能直接获取,这里我想到两种方案(这里我采用了第二种)

    (1)在商品品牌列表中, 标签的 href 属性值,通过该 URL 得到的商品均是该品牌的商品。

    (2)将商品的名字与品牌列表中的品牌进行字符串匹配,如果商品名字中包含某品牌的名字,则认为该商品是该品牌的产品。

       def parse(self, response):
    
            # 部分代码已省略 #
    
            goods_temp_list = soup.find_all('li', attrs={'class': 'gl-item'})
            for item in goods_temp_list:
                goods = SmartcranehubItem()
    
                # 零食 title
                goods_temp_title = item.find_all('div', attrs={'class': 'p-name'})
                goods_title = goods_temp_title[0].find('em').text
    
                # 零食 brand
                goods_brand = self.getGoodsBrand(goods_title, brand_list)
                # print(goods_brand)
    
        def getGoodsBrand(self, goods_title, brand_list):
            for brand in brand_list:
                if brand in goods_title:
                    return brand
            return 'No-brand'
    

    2. 爬取商品信息

      def parse(self, response):
            content = response.body
            soup = BeautifulSoup(content, "html.parser")
            brand_temp_list = soup.find_all('li', attrs={'id': re.compile(r'brand-(\w+)')})
            brand_list = list()
    
            for item in brand_temp_list:
                brand_title = item.find('a')['title']
                brand_list.append(re.sub("[A-Za-z0-9\!\%\[\]\,\。\(\)\(\)\"\.\'\ ]", "", brand_title))
                # brand_list.append(brand_title)
    
            goods_temp_list = soup.find_all('li', attrs={'class': 'gl-item'})
            for item in goods_temp_list:
                goods = SmartcranehubItem()
    
                # 零食 id
                goods_id = item['data-pid']
    
                # 零食 title
                goods_temp_title = item.find_all('div', attrs={'class': 'p-name'})
                goods_title = goods_temp_title[0].find('em').text
    
                # 零食 img
                goods_temp_img = item.find_all('div', attrs={'class': 'p-img'})
                goods_img = 'http:' + goods_temp_img[0].find('img')['source-data-lazy-img']
                # print(goods_temp_img)
    
                # 零食 url
                goods_temp_url = goods_temp_title[0].find('a')['href']
                goods_url = goods_temp_url if 'http' in goods_temp_url else 'https:' + goods_temp_url
    
                # if 'http' in goods_temp_url:
                #    goods_url = goods_temp_url
                # else:
                #    goods_url = 'http:' + goods_temp_url
                # print(goods_url)
    
                # 零食 price
                goods_price = item.find_all('div', attrs={'class': 'p-price'})[0].find('i').text
                # print(goods_price)
    
                # 零食 shop
                goods_temp_shop = item.find_all('div', attrs={'class': 'p-shop'})[0].find('a')
                goods_shop = '' if goods_temp_shop is None else goods_temp_shop.text
                # print(goods_shop)
    
                # 零食 优惠
                goods_temp_icon = item.find_all('div', attrs={'class': 'p-icons'})[0].find_all('i')
                goods_icon  =''
                for icon in goods_temp_icon:
                    goods_icon += '/' + icon.text
                # print(goods_icon)
    
                # 零食 brand
                goods_brand = self.getGoodsBrand(goods_title, brand_list)
                # print(goods_brand)
    
                # 零食 time
                cur_time = datetime.datetime.now()
                cur_year = str(cur_time.year)
                cur_month = str(cur_time.month) if len(str(cur_time.month)) == 2 else '0' + str(cur_time.month)
                cur_day = str(cur_time.day) if len(str(cur_time.day)) == 2 else '0' + str(cur_time.day)
                goods_time = cur_year + '-' + cur_month + '-' + cur_day
                # print(goods_time)
    
                # 零食 描述
                goods_describe = ""
    
                goods['goods_id'] = goods_id
                goods['goods_title'] = goods_title
                goods['goods_url'] = goods_url
                goods['goods_img'] = goods_img
                goods['goods_price'] = goods_price
                goods['goods_shop'] = goods_shop
                goods['goods_icon'] = goods_icon
                goods['goods_time'] = goods_time
                goods['goods_brand'] = goods_brand
                goods['goods_describe'] = goods_describe
    
                yield goods
    
        def getGoodsBrand(self, goods_title, brand_list):
            for brand in brand_list:
                if brand in goods_title:
                    return brand
            return 'No-brand'
    

    在这里补充一下,爬虫中需要用到的库如下:

    import scrapy
    from scrapy import Request
    from bs4 import BeautifulSoup
    import re
    from ..items import SmartcranehubItem
    import datetime

    3. 获取下一页的商品数据

     观察初始 URL 可以发现,URL 中有个参数 page ,改变它的值即可翻页,商品页数共有100页。

    class SmarthubSpider(scrapy.Spider):
        name = 'SmartHub'
        allowed_domains = ['search.jd.com']
        max_page = 100
        start_urls = ['https://search.jd.com/Search?keyword=%E9%9B%B6%E9%A3%9F&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=lingshi&stock=1&click=0&page=1']
    
        def parse(self, response):
            # 部分代码已省略 #
            cur_page_num = int(response.url.split('&page=')[1])
            next_page_num = cur_page_num + 1
            if cur_page_num < self.max_page:
                next_url = response.url[:-len(str(cur_page_num))] + str(next_page_num)
                yield Request(url=next_url, callback=self.parse, dont_filter=True)
    

     我们通过 response.url 得到当前的 url,提取 &page= 后面的值,得到当前的页数,将这个页数加一,然后拼接成新的下一页的 url ,通过 Request 方法去访问,即可实现下一页的爬取。

    4. 数据存储到MongoDB 中

     打开 pipelines.py 文件,数据入库的代码就写在这里。

    首先添加两个函数 open_spider 和 close_spider 函数,这两个函数分别会在爬虫启动和关闭时候调用,我们把数据库链接和断开的操作写在这里。

    然后是 process_item 函数,这个是数据处理函数,爬虫爬到并打包好的数据会以 SmartcranehubItem 对象的形式发送到这里,然后我们对其进行处理,依次存入数据库即可。

    入库前先判断数据库中是否有该数据,如果有则跳过,如果没有则入库。
     

    # -*- coding: utf-8 -*-
    
    # Define your item pipelines here
    #
    # Don't forget to add your pipeline to the ITEM_PIPELINES setting
    # See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
    
    import pymongo
    from SmartCraneHub.items import SmartcranehubItem
    import logging
    
    class SmartcranehubPipeline(object):
        def open_spider(self, spider):
            self.client = pymongo.MongoClient("mongodb://localhost/", 27017)
            # 数据库和数据表,如果没有的话会自行创建
            self.db = self.client['SmartSpiderHubTest']
            self.collection = self.db['Lingshi']
    
        def close_spider(self, spider):
            self.client.close()
    
        def process_item(self, item, spider):
    
            if isinstance(item, SmartcranehubItem):
                try:
                    collection_name = self.getCollection(item['goods_brand'])
                    old_item = self.db[collection_name].find_one({'goods_id': item['goods_id']})
    
                    if old_item is None:
                        logging.info('items: ' + item['goods_id'] + ' insert in ' + collection_name + ' db.')
                        self.db[collection_name].insert(dict(item))
                    elif self.needToUpdate(old_item, item):
                        self.db[collection_name].remove({'goods_id': item['goods_id']})
                        self.db[collection_name].insert(dict(item))
                        logging.info("items: " + item['goods_id'] + " has UPDATED in " + collection_name + " db.")
                    else:
                        logging.info('items: ' + item['goods_id'] + ' has in ' + collection_name + ' db.')
    
                except Exception as e:
                    logging.error("PIPELINE EXCEPTION: " + str(e))
    
            return item
    
        '''
         brand_list =
         ['乐事', '旺旺', '三只松⿏', '卫⻰', '⼝⽔娃', '奥利奥', '良品铺⼦', '达利园', '盼盼',
         '稻⾹村', '好丽友', '徐福记', '盐津铺⼦', '港荣', '上好佳', '百草味', '雀巢', '波⼒',
         '⽢源牌', '喜之郎', '可⽐克', '康师傅', '嘉⼠利', '嘉华', '友⾂', '来伊份', '豪⼠',
         '⽶多奇', '闲趣', '稻⾹村', '', '桂发祥⼗⼋街', '趣多多', '好巴⻝', '北京稻⾹村', 
         '法丽兹', '⽆穷', '源⽒', '华美', '葡记']
        '''
    
        def getCollection(self, brand):
            if brand == '乐事':
                return 'Leshi'
            elif brand == '旺旺':
                return 'Wangwang'
            elif brand == '三只松鼠':
                return 'Sanzhisongshu'
            elif brand == '卫龙':
                return 'Weilong'
            elif brand == '口水娃':
                return 'Koushuiwa'
            elif brand == '奥利奥':
                return 'Aoliao'
            elif brand == '良品铺子':
                return 'Liangpinpuzi'
            else:
                return 'Lingshi'
    
        def needToUpdate(self, old_item, new_item):
            if old_item['goods_price'] != new_item['goods_price']:
                old_time = old_item['goods_time']
                old_price = float(old_item['goods_price'])
                new_price = float(new_item['goods_price'])
    
                minus_price = round((new_price-old_price), 2)
                logging.info('Need To Update')
    
                if minus_price >= 0:
                    new_item['goods_describe'] = '比 ' + old_time + ' 涨了 ' + str(minus_price) + ' 元。'
                else:
                    new_item['goods_describe'] = '比 ' + old_time + ' 降了 ' + str(minus_price) + ' 元。'
                return True
            return False
    

     这里需要说明两点:

    (1)由于有些大品牌的商品数量较多,所以将其拿出来单独作为一个数据表进行存储,其余品牌的商品统一存入 "Lingshi" 数据表中。这里挑选了七家品牌单独建表,一共八个数据表。

    (2)不知道大家有没有注意到,前面在 item 中,我添加了两个字段,一个是 goods_time,一个是 goods_describe(扫描时间和商品描述),这两个数据主要是为了比价用。将来爬虫部署到服务器上后,比如说每天执行一次,可以通过比较同一商品价格和扫描时间,从而发现商品价格的变动情况,并且将这个变动情况存入 goods_describe 字段。

     5. 执行结果

     运行 Run.py ,等待片刻即可得到运行结果。我们去数据库中查看。可以看到数据均被存在了数据库中。
    为了测试价格变动功能好不好使,我们将其中一个商品的价格手动改一下。然后重新运行。至此,我们的爬虫程序全部完成。下面我会将爬虫程序部署到云服务器上。

    四、部署到云服务器上

    1. 设置参数

    打开 scrapy.cfg 文件,将文件做一些小的修改。

    [settings]
    default = SmartCraneHub.settings
    
    [deploy:AliCloud]
    url = http://xx.xx.xx.xx:6800/
    project = SmartCraneHub

    第四行:在 deploy 后面加一个名字,表示我们服务器的别名

    第五行:url 后面填写自己服务器的 IP 地址,以及为爬虫程序设置的端口号。

    (PS:在pipelins.py 中,连接数据库的地方,应该将 localhost 改成云服务器的 IP 地址) 

    2. 部署并在服务器上运行爬虫

    (1)首先远程连接服务器,启动 scrapyd (后面加一个)。

    $ '/usr/local/python3/bin/scrapyd' &

    启动后,在浏览器中输入 http://xx.xx.xx.xx:6800 (就是你前面 scrapy.cfg  文件里,url 的值),出现以下界面,说明启动成功:(2)在 Terminal 中执行以下命令,将爬虫部署到云服务器上。(AliCloud 是我云服务器的别名,SmartCraneHubSpider是我项目的名称,请根据自己的项目自行更改)

    $ scrapyd-deploy AliCloud -p SmartCraneHub

    如果看到以下这样的信息,说明部署成功,如果提示 :'scrapyd-deploy' 不是内部或外部命令,也不是可运行的程序或批处理文件。那么请参考下面(遇到的常见问题)。还是刚才的网址,刷新网页,会看到我们的爬虫已经部署好了。(3)爬虫部署成功之后,执行以下命令,可以启动爬虫。

    $ curl http://xx.xx.xx.xx:6800/schedule.json -d project=SmartCraneHub -d spider=SmartHub

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值