美团网数据采集技巧

1.数据采集工具介绍

如今的大多数动态网站,都是由浏览器端经过js发起ajax请求,拿到数据后再渲染完成页面展现。这种状况下采集数据,经过脚本发起http的get请求,拿到DOM文档页面后再解析提取有用数据的方法是行不通的。而后又有人会想到经过F12打开浏览器控制台分析服务端api,再模拟请求相应的api来拿到咱们想要的数据,这种思路在一些状况下可行,可是不少大型网站都会采起一些反爬策略,出于安全性考虑,每每对接口增长了安全验证,好比只有设置了相关的header和cookie,才能对页面进行请求;还有的对请求来源也作了限制等等,这个时候经过这种方式采集数据就更加困难了。咱们还有其余有效的方法吗?固然,python作爬虫很是的简单,咱们先来了解一下Selenium和Selectors,而后经过爬取美团网上商家信息的例子总结一下数据采集的一些技巧:css

  • Selenium 是一个开源测试框架,用来对web应用(好比网站)作自动化测试用的,由于它能够驱动浏览器,诸如Chrome,Firefox,IE等,因此能够较为真实的模拟人自动去点击网站的各个按钮,翻页,填写表单等,咱们使用python驱动Selenium的webdriver,能够驱动浏览器,直接拿到的就是渲染好的DOM文档,大量节省了时间。
  • Selectors是Scrapy(Python的一套爬虫框架)提取数据的一套机制。被称做选择器,能够经过特定的 XPath 或者 CSS 表达式来“选择” HTML文件中的某个部分。用它来分析提取DOM文档有效数据很是的方便。而且XPath是W3C标准,因此使用Selectors提取数据的方法是通用的。

2.页面抓取数据分析和数据表建立

我以家附近朝阳大悦城中的一家美食店为例进行数据采集,网址是:python

https://www.meituan.com/meishi/40453459/

源码地址mysql

2.1 抓取数据

咱们要抓取的第一部分数据是商家的基本信息,包括商家名称、地址、电话、营业时间,分析多个美食类商家咱们可知,这些商家的web界面在布局上基本是一致的,因此咱们的爬虫能够写的比较通用。为了防止对商家数据的重复抓取,咱们将商家的网址信息也存储到数据表中。
图片描述
第二部分要抓取的数据是美食店的招牌菜,每一个店铺基本都有本身的特点菜,咱们将这些数据也保存下来,用另外的一张数据表存储。
图片描述
最后一部分咱们要抓取的数据是用户的评论,这部分数据对咱们来讲是颇有价值的,未来咱们能够经过对这部分数据的分析,提取更多关于商家的信息。咱们要抓取的这部分信息有:评论者昵称、星级、评论内容、评论时间,若是有图片,咱们也要将图片的地址以列表的形式存下来。
图片描述linux

2.2 建立数据表

咱们存储数据使用的数据库是Mysql,Python有相关的ORM,项目中咱们使用peewee。可是在创建数据表时建议采用原生的sql,这样咱们能灵活的控制字段属性,设置引擎和字符编码格式等。使用Python的ORM也能够达到效果,可是ORM是对数据库层的封装,像sqlite、sqlserver数据库和Mysql仍是有些许差异的,使用ORM只能使用这些数据库共有的部分。下面是存储数据须要用到的数据表sql:git

CREATE TABLE `merchant` (  #商家表
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL COMMENT '商家名称',
  `address` varchar(255) NOT NULL COMMENT '地址',
  `website_address` varchar(255) NOT NULL  COMMENT '网址',
  `website_address_hash` varchar(32) NOT NULL COMMENT '网址hash',
  `mobile` varchar(32) NOT NULL COMMENT '电话',
  `business_hours` varchar(255) NOT NULL COMMENT '营业时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `recommended_dish` (   #推荐菜表
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `merchant_id` int(11) NOT NULL COMMENT '商家id',
  `name` varchar(255) NOT NULL COMMENT '推荐菜名称',
  PRIMARY KEY (`id`),
  KEY `recommended_dish_merchant_id` (`merchant_id`),
  CONSTRAINT `recommended_dish_ibfk_1` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=309 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `evaluate` (   #评论表
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `merchant_id` int(11) NOT NULL COMMENT '商家id',
  `user_name` varchar(255) DEFAULT '' COMMENT '评论人昵称',
  `evaluate_time` datetime NOT NULL COMMENT '评论时间',
  `content` varchar(10000) DEFAULT '' COMMENT '评论内容',
  `star` tinyint(4) DEFAULT '0' COMMENT '星级',
  `image_list` varchar(1000) DEFAULT '' COMMENT '图片列表',
  PRIMARY KEY (`id`),
  KEY `evaluate_merchant_id` (`merchant_id`),
  CONSTRAINT `evaluate_ibfk_1` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8427 DEFAULT CHARSET=utf8mb4;

相应的咱们也可使用Python的ORM建立管理数据表,后边具体分析到代码时会讲到peewee对mysql数据库的一些经常使用操作,好比查询数据,插入数据库数据并返回id;批量插入数据库等,读者可搜集相关资料系统学习。
meituan_spider/models.py代码:github

from peewee import *

# 链接数据库
db = MySQLDatabase("meituan_spider", host="127.0.0.1", port=3306, user="root", password="root", charset="utf8")


class BaseModel(Model):
    class Meta:
        database = db


# 商家表,用来存放商家信息
class Merchant(BaseModel):
    id = AutoField(primary_key=True, verbose_name="商家id")
    name = CharField(max_length=255, verbose_name="商家名称")
    address = CharField(max_length=255, verbose_name="商家地址")
    website_address = CharField(max_length=255, verbose_name="网络地址")
    website_address_hash = CharField(max_length=32, verbose_name="网络地址的md5值,为了快速索引")
    mobile = CharField(max_length=32, verbose_name="商家电话")
    business_hours = CharField(max_length=255, verbose_name="营业时间")


# 商家推荐菜表,存放菜品的推荐信息
class Recommended_dish(BaseModel):
    merchant_id = ForeignKeyField(Merchant, verbose_name="商家外键")
    name = CharField(max_length=255, verbose_name="推荐菜名称")


# 用户评价表,存放用户的评论信息
class Evaluate(BaseModel):
    id = CharField(primary_key=True)
    merchant_id = ForeignKeyField(Merchant, verbose_name="商家外键")
    user_name = CharField(verbose_name="用户名")
    evaluate_time = DateTimeField(verbose_name="评价时间")
    content = TextField(default="", verbose_name="评论内容")
    star = IntegerField(default=0, verbose_name="评分")
    image_list = TextField(default="", verbose_name="图片")


if __name__ == "__main__":
    db.create_tables([Merchant, Recommended_dish, Evaluate])

3.代码实现和详解

代码比较简单,可是让代码运行起来,须要安装前边提到的工具包:selenium、scrapy,另外使用peewee也须要安装,这些包均可以经过pip进行安装;另外selenium驱动浏览器还须要安装相应的driver,由于我本地使用的是chrome浏览器,因此我下载了相关版本的chromedriver,这个后边会使用到。请读者自行查阅python操做selenium须要作的准备工做,先手动搭建好相关环境。接下来详细分析代码;源代码以下:web

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import NoSuchElementException

from scrapy import Selector
from models import *

import hashlib
import os
import re
import time
import json

chrome_options = Options()

# 设置headless模式,这种方式下无启动界面,可以加速程序的运行
# chrome_options.add_argument("--headless")
# 禁用gpu防止渲染图片
chrome_options.add_argument('disable-gpu')
# 设置不加载图片
chrome_options.add_argument('blink-settings=imagesEnabled=false')


# 经过页面展现的像素数计算星级
def star_num(num):
    numbers = {
        "16.8": 1,
        "33.6": 2,
        "50.4": 3,
        "67.2": 4,
        "84": 5
    }

    return numbers.get(num, 0)


# 解析商家内容
def parse(merchant_id):
    weblink = "https://www.meituan.com/meishi/{}/".format(merchant_id)
    # 启动selenium
    browser = webdriver.Chrome(executable_path="/Users/guozhaoran/python/tools/chromedriver", options=chrome_options)
    browser.get(weblink)
    # 不重复爬取数据
    hash_weblink = hashlib.md5(weblink.encode(encoding='utf-8')).hexdigest()
    existed = Merchant.select().where(Merchant.website_address_hash == hash_weblink)
    if (existed):
        print("数据已经爬取")
        os._exit(0)
    time.sleep(2)
    # print(browser.page_source)  #获取到网页渲染后的内容
    sel = Selector(text=browser.page_source)

    # 提取商家的基本信息
    # 商家名称
    name = "".join(sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='name']/text()").extract()).strip()
    detail = sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='address']//p/text()").extract()
    address = "".join(detail[1].strip())
    mobile = "".join(detail[3].strip())
    business_hours = "".join(detail[5].strip())
    # 保存商家信息
    merchant_id = Merchant.insert(name=name, address=address, website_address=weblink,
                                  website_address_hash=hash_weblink, mobile=mobile, business_hours=business_hours
                                  ).execute()

    # 获取推荐菜信息
    recommended_dish_list = sel.xpath(
        "//div[@id='app']//div[@class='recommend']//div[@class='list clear']//span/text()").extract()

    # 遍历获取到的数据,批量插入数据库
    dish_data = [{
        'merchant_id': merchant_id,
        'name': i
    } for i in recommended_dish_list]

    Recommended_dish.insert_many(dish_data).execute()

    # 也能够遍历list,一条条插入数据库
    # for dish in recommended_dish_list:
    #     Recommended_dish.create(merchant_id=merchant_id, name=dish)

    # 查看连接一共有多少页的评论
    page_num = 0
    try:
        page_num = sel.xpath(
            "//div[@id='app']//div[@class='mt-pagination']//ul[@class='pagination clear']//li[last()-1]//span/text()").extract_first()
        page_num = int("".join(page_num).strip())
        # page_num = int(page_num)
    except NoSuchElementException as e:
        print("改商家没有用户评论信息")
        os._exit(0)

    # 当有用户评论数据,每页每页的读取用户数据
    if (page_num):
        i = 1
        number_pattern = re.compile(r"\d+\.?\d*")
        chinese_pattern = re.compile(u"[\u4e00-\u9fa5]+")
        illegal_str = re.compile(u'[^0-9a-zA-Z\u4e00-\u9fa5.,,。?“”]+', re.UNICODE)
        while (i <= page_num):
            # 获取评论区元素
            all_evalutes = sel.xpath(
                "//div[@id='app']//div[@class='comment']//div[@class='com-cont']//div[2]//div[@class='list clear']")
            for item in all_evalutes:
                # 获取用户昵称
                user_name = item.xpath(".//div[@class='info']//div[@class='name']/text()").extract()[0]
                # 获取用户评价星级
                star = item.xpath(
                    ".//div[@class='info']//div[@class='source']//div[@class='star-cont']//ul[@class='stars-ul stars-light']/@style").extract_first()
                starContent = "".join(star).strip()
                starPx = number_pattern.search(starContent).group()
                starNum = star_num(starPx)
                # 获取评论时间
                comment_time = "".join(
                    item.xpath(".//div[@class='info']//div[@class='date']//span/text()").extract_first()).strip()
                evaluate_time = chinese_pattern.sub('-', comment_time, 3)[:-1] + ' 00:00:00'
                # 获取评论内容
                comment_content = "".join(
                    item.xpath(".//div[@class='info']//div[@class='desc']/text()").extract_first()).strip()
                comment_filter_content = illegal_str.sub("", comment_content)
                # 若是有图片,获取图片
                image_container = item.xpath(
                    ".//div[@class='noShowBigImg']//div[@class='imgs-content']//div[contains(@class, 'thumbnail')]//img/@src").extract()
                image_list = json.dumps(image_container)

                Evaluate.insert(merchant_id=merchant_id, user_name=user_name, evaluate_time=evaluate_time,
                                content=comment_filter_content, star=starNum, image_list=image_list).execute()
            i = i + 1
            if (i < page_num):
                next_page_ele = browser.find_element_by_xpath(
                    "//div[@id='app']//div[@class='mt-pagination']//span[@class='iconfont icon-btn_right']")
                next_page_ele.click()
                time.sleep(10)
                sel = Selector(text=browser.page_source)


if __name__ == "__main__":
    parse("5451106")

3.1 启动webdriver并设置优化参数

为了让爬虫更加通用,咱们的解析函数经过接收商家"参数id"来摘取不一样商家的网页内容。selenium经过webdriver驱动web浏览器:ajax

weblink = "https://www.meituan.com/meishi/{}/".format(merchant_id)
    # 启动selenium
    browser = webdriver.Chrome(executable_path="/Users/guozhaoran/python/tools/chromedriver", options=chrome_options)
    browser.get(weblink)

其中executable_path就是以前咱们下载好的相关版本的chromedriver可执行文件,另外selenium启动web浏览器以前还能够设置一些参数:正则表达式

chrome_options = Options()

# 设置headless模式,这种方式下无启动界面,可以加速程序的运行
# chrome_options.add_argument("--headless")
# 禁用gpu防止渲染图片
chrome_options.add_argument('disable-gpu')
# 设置不加载图片
chrome_options.add_argument('blink-settings=imagesEnabled=false')

设置--headless可让chrome不启动前台界面运行,有点相似于守护进程,不过在调试代码的过程当中咱们能够不设置这个参数,这样就能看到程序对浏览器中的网页具体进行了哪些操做。另外咱们还能够经过disable-gpu、blink-settings=imagesEnabled=false使浏览器解析网页过程当中不加载图片来提升浏览器渲染网页的速度;由于咱们数据中存储的图片数据也只是路径而已。selenium作爬虫的一个缺点是效率比较低,爬取速度慢,可是经过设置这些优化参数,也是能够极大提高爬虫抓取速度的。sql

3.2 提取商家的基本信息

前边提到过,为了避免重复爬取数据,咱们会对要抓取的商家进行hash校验:

# 不重复爬取数据
    hash_weblink = hashlib.md5(weblink.encode(encoding='utf-8')).hexdigest()
    existed = Merchant.select().where(Merchant.website_address_hash == hash_weblink)
    if (existed):
        print("数据已经爬取")
        os._exit(0)

若是商家数据没有被爬取过,咱们就获取到网页数据进行解析:

time.sleep(2)
    # print(browser.page_source)  #获取到网页渲染后的内容
    sel = Selector(text=browser.page_source)

sleep两秒是由于browser对象解析网页须要时间,不过这个时间通常会很快,这里是为了使程序更加稳妥;以后构造一个选择器对页面数据进行解析:

# 提取商家的基本信息
    # 商家名称
    name = "".join(sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='name']/text()").extract()).strip()
    detail = sel.xpath("//div[@id='app']//div[@class='d-left']//div[@class='address']//p/text()").extract()
    address = "".join(detail[1].strip())
    mobile = "".join(detail[3].strip())
    business_hours = "".join(detail[5].strip())
    # 保存商家信息
    merchant_id = Merchant.insert(name=name, address=address, website_address=weblink,
                                  website_address_hash=hash_weblink, mobile=mobile, business_hours=business_hours
                                  ).execute()

解析商家基本信息是经过xpath语法定位到相关元素而后提取文本信息,为了保证提取的数据都是不为空的字符串,进行了字符串拼接;最后将解析到的数据插入到商家数据表,peewee的insert方法返回了主键id,在后边采集数据入库时会使用到。

3.3 提取商家特点菜信息

提取商家特点菜信息逻辑比较简单,提取出来的数据返回一个list,python解析数据类型很是的方便,不过数据入库时有不一样的方案,能够批量插入也能够循环遍历列表插入,这里咱们采用批量插入。这样效率会更高。

# 获取推荐菜信息
    recommended_dish_list = sel.xpath(
        "//div[@id='app']//div[@class='recommend']//div[@class='list clear']//span/text()").extract()

    # 遍历获取到的数据,批量插入数据库
    dish_data = [{
        'merchant_id': merchant_id,
        'name': i
    } for i in recommended_dish_list]

    Recommended_dish.insert_many(dish_data).execute()

    # 也能够遍历list,一条条插入数据库
    # for dish in recommended_dish_list:
    #     Recommended_dish.create(merchant_id=merchant_id, name=dish)

3.4 分页提取用户评论信息

用户信息的提取是数据抓取中最难的部分了,基本思路就是咱们首先查看有多少页的用户评论,而后再一页一页的解析用户评论信息。期间咱们能够经过selenium模拟浏览器的点击事件进行翻页,入库的时候还要注意对文本进行清洗,由于评论中不少的表情字符是不符合数据表字段设计的编码规范的,另外点击了下一页以后,程序必定要sleep一段时间,由于网站的数据发生了更新,要进行页面数据的从新获取。咱们先来看看如何获取一共有多少页的用户评论数据,网站的分页图以下:
图片描述
这里咱们重点关注两个按钮,一个是下一页,另外一个是最后一页的数字,这是咱们想要的信息,不过有些商家可能没有相关的用户评论,页面上也没有相关的元素,程序仍是要作一下兼容性处理的:

# 查看连接一共有多少页的评论
    page_num = 0
    try:
        page_num = sel.xpath(
            "//div[@id='app']//div[@class='mt-pagination']//ul[@class='pagination clear']//li[last()-1]//span/text()").extract_first()
        page_num = int("".join(page_num).strip())
        # page_num = int(page_num)
    except NoSuchElementException as e:
        print("改商家没有用户评论信息")
        os._exit(0)

接下来就是像获取商场特点菜同样获取一条条的评论数据了,只是过程比较繁琐而已,咱们的基本思路就是这样:

if (page_num):
        i = 1
          ... 
        while (i <= page_num):
           ...
            i = i + 1
            if (i < page_num):
                next_page_ele = browser.find_element_by_xpath(
                    "//div[@id='app']//div[@class='mt-pagination']//span[@class='iconfont icon-btn_right']")
                next_page_ele.click()
                time.sleep(10)
                sel = Selector(text=browser.page_source)

咱们判断程序解析是否到了最后一页,若是没有,经过模拟点击下一页得到新页面,程序sleep是为了给浏览器解析新页面数据留下时间。详细的解析过程咱们挑几个重点说一下:

  • 获取用户评论星级并非直接获取到的,而是经过获取到星级评级元素的css宽度,经过函数计算获得的:
# 经过页面展现的像素数计算星级
def star_num(num):
    numbers = {
        "16.8": 1,
        "33.6": 2,
        "50.4": 3,
        "67.2": 4,
        "84": 5
    }

    return numbers.get(num, 0)
    
    ...
    # 获取用户评价星级
                star = item.xpath(
                    ".//div[@class='info']//div[@class='source']//div[@class='star-cont']//ul[@class='stars-ul stars-light']/@style").extract_first()
                starContent = "".join(star).strip()
                starPx = number_pattern.search(starContent).group()
                starNum = star_num(starPx)
  • 用户的评论内容可能含有非法字符,程序经过正则表式来过滤,正则表达式通常使用python中的re模块,预先编译能提升性能,另外这些操做要放在while,for这些循环外边:
number_pattern = re.compile(r"\d+\.?\d*")
        chinese_pattern = re.compile(u"[\u4e00-\u9fa5]+")
        illegal_str = re.compile(u'[^0-9a-zA-Z\u4e00-\u9fa5.,,。?“”]+', re.UNICODE)
         while (i <= page_num):
         ...
        comment_content = "".join(
                    item.xpath(".//div[@class='info']//div[@class='desc']/text()").extract_first()).strip()
                comment_filter_content = illegal_str.sub("", comment_content)
  • 用户评论时图片可能有多张,咱们只得到图片路径,以json压缩的形式保存到数据表字段中:
image_container = item.xpath(
                    ".//div[@class='noShowBigImg']//div[@class='imgs-content']//div[contains(@class, 'thumbnail')]//img/@src").extract()
                image_list = json.dumps(image_container)

4.反思总结

下边是程序运行过程当中数据抓取的截图:
图片描述
图片描述程序的思路很简洁,真实的企业应用中,可能会更多的考虑爬虫的效率和稳定性。通常linux服务器下程序发生错误,都会记录有相关的日志,selenium也只能是无界面的运行,程序中没有用到太多的高级特性,其实一个爬虫架构中要包含的技术点有不少,好比多线程的数据爬取,还有针对验证码反爬的验证(本示例中第一次打开美团页面也须要验证,我手动处理了一次)等等,这里算是起一个抛砖引玉的目的吧。不过程序中使用到的文本处理技巧、数据分析提取等都是爬虫中常常会使用到的,很高兴在这里和你们一块分享。

相关文章
相关标签/搜索