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

本周爬取的网站是京东(https://search.jd.com/),这次我又使用了一些 “新技术” :

  1.  使用 Scrapy 框架来写爬虫,并将爬取结果存入 MongoDB 数据库中。
  2.  将爬虫部署到阿里云服务器上,以便将来可以每天定时自动爬取。

 

写在前面的话


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)零食的品牌

可以发现,零食的品牌是在一个 <ul> 标签下的 <li> 标签里,每个 <li> 标签表示一个品牌,而品牌的名字就存放在 <li> 标签下 <a> 标签的 title 属性里。

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

(2)零食的 ID

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

 (3)零食的名字

零食的名字存放在 <li> 标签下, class 为 p-name 的 <div> 标签里的 <a> 标签下的 <em> 标签里。

(4)零食的价格

零食的价格存放在 class 为 p-price 的 <div> 标签中,<strong> 标签之下(由于我们之后要对价格做比较,查看价格的涨跌情况,价格需要存储为 float 型的数据,所以这里我们获取 <i> 标签下的价格数字)

(5)零食的店铺

零食的店铺存放在 class 为 p-shop 的 <div> 标签下,<a> 标签的内容里。

 (6)零食的图片

零食的图片 URL 存放在 class 为 p-img 的 <div> 标签下,<img> 标签的 src 属性。不过这里的 url 是不完整的,需要对其进行处理,在前面加上 “http://”, 拼接成一个完整的 url 链接。

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

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

(7)零食详情页的 URL

零食详情页的 URL 存放在 class 为 p-img 的 <div> 标签下,<a> 标签的 href 属性值。它跟图片的 URL 一样,有的是不完整的,需要手动添加 “http://” 。

(8)零食的优惠活动

零食的优惠活动的信息存放在 class 为 p-icons 的 <div> 标签下,<i> 标签的内容中,但是不同的商品 <i> 标签的数量不同,有的甚至没有 <i> 标签,所以需要做一个判断。获取 <div> 标签下所有的 <i> 标签,如果没有 <i> 标签,则该字节为空,若有 <i> 标签,则将所有 <i> 标签的内容拼接成一个字符串。

 

三、编写代码环节

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)在商品品牌列表中,<a> 标签的 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
爬虫正在执行
爬虫执行完毕

(4)如果想删掉服务器身上部署的爬虫,可以执行下面的命令。

$ curl http://xx.xx.xx.xx:6800/delproject.json -d project=SmarCraneHub

3. 遇到的常见问题

 如果你的电脑是Windows系统,那么在部署爬虫的时候可能会出现这样的错误:

'scrapyd-deploy' 不是内部或外部命令,也不是可运行的程序或批处理文件。

请参考文章:https://mp.csdn.net/postedit/84453693 

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机灵鹤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值