今天跟师弟聊天说到他花钱从淘宝购买表情包,而我自身的表情包也不多,但是网上有很多表情包网站,何不自己爬取搭一个表情包搜索网站呢?因此本文以doutula为演示站点,详细说明从网上使用scrapy爬取表情包并最终搭建一个自己的表情包搜索网站的过程。主要步骤如下:

  • scrapy爬取表情包并存入mysql
  • flask搭建搜索网站
  • 每日更新保存图片到本地 准备工作,anaconda python3

    pip install scrapy
    pip install flask 
    pip install pymysql
    

scrapy爬取表情包并存入mysql

打开网站审查元素

doutula 确定css提取规则与mysql表结构。 可以看到主要字段有两个,

  • 一个为图片地址,可以看到为新浪来源,确定字段 _imageurl
  • 一个为表情包描述,在p元素里面,字段为_imagedes
  • 另外需要一个主键,设为 id 。后续搜索表情包的时候根据描述字段查出表情包地址,直接外链就行。

    创建一个表情包的数据库和表

    create dataabse bqb;
    use bqb;
    DROP TABLE IF EXISTS `bqb_scrapy`;
    CREATE TABLE `bqb_scrapy` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `image_url` varchar(100) NOT NULL,
    `image_des` varchar(100) NOT NULL,
    PRIMARY KEY (`id`),
    KEY `image_url` (`image_url`)
    ) ENGINE=InnoDB AUTO_INCREMENT=187800 DEFAULT CHARSET=utf8mb4;
    

    3个字段,自增id,图片地址,图片描述。

    编写scrapy爬虫

    class DoutulaSpider(scrapy.Spider):
    name = 'doutula'
    allowed_domains = ['doutula.com']
    start_urls = ['https://www.doutula.com/photo/list/']
    for i in range(2,2773):
        start_urls.append("https://www.doutula.com/photo/list/?page={}".format(i))
    
    def parse(self, response):
        item = {}
        item['image_url'] = response.css("div.random_picture").css("a>img::attr(data-original)").extract()
        item['image_des'] = response.css("div.random_picture").css("a>p::text").extract()
        yield item
    

表情包一共有2773页,爬取规则 _response.css(“div.randompicture”).css(“a>img::attr(data-original)“).extract() 可以获得一个表情包地址的list,而 _item[‘image_des’] = response.css(“div.randompicture”).css(“a>p::text”).extract() 可以得到表情包的描述

编写scrapy pipline存入mysql

import pymysql
class BqbPipeline(object):
    def open_spider(self,spider):
        self.mysql_con = pymysql.connect('localhost', 'youruser', 'yourpass', 'bqb',autocommit=True)
        self.cur = self.mysql_con.cursor()

    def process_item(self, item, spider):
        for i in range(len(item['image_url'])):
            query = 'insert into bqb_scrapy(`image_url`,`image_des`) values("{}","{}");'.format(item['image_url'][i],item['image_des'][i])
            self.cur.execute(query)
        return item


    def close_spider(self, spider):
        self.mysql_con.close()

pymysql的使用,在mysql数据库连接中使用autocommit使得自动确认插入操作。

item等以及scrapy开始爬取

接下来需要把setting.py的pipeline注释部分取消使得存入数据库操作可以起作用,另外就是item.py确定item中的两个元素

import scrapy
class BqbItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    image_url = scrapy.Field()

    image_des = scrapy.Field()

运行爬虫就可以了,在项目根目录运行

scrapy crawl doutula

可以看到数据在疯狂地存入数据库了 store_image 到这里,第一步scrapy爬取表情包并存入mysql数据库就已经完成了。

flask搭建搜索网站

flask app中需要三个页面,首页,搜索并展示功能,404页面。 网站主程序 bqb.py

import pymysql
from flask import Flask
from flask import request, render_template



app = Flask(__name__)
def db_execute(keyword):
     conn = pymysql.connect('localhost', 'youruser', 'yourpass', 'bqb')
     query = 'select image_url,image_des from bqb_scrapy where image_des like "%{}%" limit 1000;'.format(keyword)
     cur = conn.cursor()
     cur.execute(query)
     res = cur.fetchall()
     cur.close()
     conn.close()
     return res

@app.route('/', methods=['GET', 'POST'])
def search():
    if request.method == 'GET':
        return render_template('index.html')

    elif request.method == 'POST':
        keyword = request.form.get('keyword').strip()
        items = db_execute(keyword)
        if items != None:
            return render_template('bqb.html', list=items)
        else:
            return 'not found'

    else:
        return render_template('404.html')


if __name__ == '__main__':
    #print(db_execute(u"呵呵")) 
    app.run(host='0.0.0.0',port=555)

这样就完成了,本地444端口,使用nginx反向代理就行,域名为 bqb.bobobk.com,查看效果

search_bqb bqb_result

这样一个表情包搜索网站就搭好了,斗图的时候再也不用担心没有表情包可用了。

每日更新保存图片到本地

表情包搜索网站运行一段时间后,发现图片无法直接加载了,因此决定将图片存到本地,以后就不会出现加载不出来的情况了。由于现在数据库已经存了几十万的表情包了,那么全部放在一个文件夹明显不太合适,在网上找到了存放大量小文件的思路,就是把文件id进行32进制的转换并将其补零至6位,随后进行2个2个的拆分位3个文件夹,分别进行存储就可以了。因此首先将数据库导出到csv,然后通过python调用wget实现了,方法如下:

首先导出表情包数据库到csv

导出mysql数据库到csv,这里你可以采用phpmyadmin或者直接登陆数据库使用select into file导出

use bqb;
select * from bqb_scrapy into outfile "bqb.csv"; 

在数据库中到处csv文件到当前文件夹下

图片下载并存入本地

为了防止新浪ban了我的ip,这里就慢慢的下载,没有使用多线程了。 python 下载图片代码


### 导入包
import pandas as pd
import numpy as np
import os

### 确认文件保存文件夹的存在,linux下的mkdir -p可以很好地解决迭代创建文件夹的任务
def check_path(string1):
    if os.path.exists(string1):
        pass
    else:
        os.system("mkdir -p {}".format(string1))

### 通过数据库的自增id确定文件应该保存到的文件夹位置
def get_dir(num):
    num_hex = hex(num)[2:].zfill(6)
    return "/".join(["..",str(num_hex[:2]),str(num_hex[2:4])])
### 文件不存在或者为空时调用wget下载文件到指定文件夹
def request_file(url,filename):
    if not os.path.exists(filename) or os.path.getsize(filename) == 0:
        cmd = "wget {} -qO {}".format(url,filename)
        os.system(cmd)

### 读取前面导出的mysql数据表,read_csv中的usecols很好用,可以过滤字符中的逗号
df = pd.read_csv("bqb.csv",header=None,usecols=range(4),names=["id","url","des","bool"])


### 下载所有的表情包图片文件
for i in df.index:
    url = df["url"][i].replace("'",'') ##导出的文件字符串有引号,这里去掉
    fdir = df["id"][i]
    path_dir = get_dir(fdir)
    check_path(path_dir)
    filename = path_dir+"/"+url.split("/")[-1]
    request_file(url,filename)

这里本文采用了两级文件夹,每个文件夹保存16**2次方的文件个数。在调用wget下载文件前,首先通过check_path函数保证文件夹的存在。下载文件时确认文件不存在或者为空才下载。详细解释看代码就好了。 为了更好的展示图片,表情包站采用了瀑布流的展示方式。 查看下第二个版本的表情包网站效果吧 bqb_result_v2

总结

本文从头开始使用scrapy从表情包网站抓取表情包,并随后使用flask部署网站,在新浪图片无法加载时,将图片下载到本地生成了第二版的表情包搜索网站,由于免费cdn的流量限制问题,这里每次搜索出来的表情包限制为20个。有了表情包搜索网站,你们就是最靓的仔。