交互式Python爬虫分析实例

大数据学习路线图

先前的文章展示了爬虫分析,并使用pyecharts画图。这篇文章在先前文章的基础上,增加了在线控制模块。总体的做法就是,把爬虫分析功能放在后台,在后台开启一个服务端,接收客户端的命令后,开启爬虫分析,然后将最后的结果展示在客户端。

厦门租房信息分析展示(pycharm+python爬虫+pyspark+pyecharts)

这里先给出工程截图,后面新建文件的时候就不会再详细说明应该放在哪一个目录下了。


顺带一提,init.py这个文件是用来索引用的,文件内容为空就行,如果没有出现无法import爬虫模块代码的情况,就无需添加这个文件。

接下来文章将分为大致三个部分,第一部分是后台的爬虫分析模块,第二部分是后台的用flask构建服务端模块,以及第三部分是客户端的编写html部分。

注意,这次的工程我使用的是本地的python环境了,在pycharm新建工程的时候是可以选择本地运行环境的。python版本3.6。正常来说,python3的版本都可以使用。只是要注意安装相关的模块。可以使用pycharm的红色感叹号提示来install相关模块。也可以开启终端自行安装。使用本地的环境就可以永久使用了,这就很方便了。需要安装的模块,请参考先前的文章。此外还增加了flask模块,flask_socketio模块。

sudo pip3 install flask
sudo pip3 install flask-socketio

第一步 爬虫分析

该部分与文章开头的链接相差无几。只是少了一个程序总运行程序,因为我把总运行程序部分写到了flask后台服务里了。
首先是要创建爬去网页内容的rent_spider.py。代码如下:

# -*- coding: utf-8 -*-
import requests
from bs4 import BeautifulSoup
import csv

# num表示记录序号
Url_head = "http://fangzi.xmfish.com/web/search_hire.html?h=&hf=&ca=5920"
Url_tail = "&r=&s=&a=&rm=&f=&d=&tp=&l=0&tg=&hw=&o=&ot=0&tst=0&page="
Num = 0
Filename = "./data/rent.csv"


# 把每一页的记录写入文件中
def write_csv(msg_list):
    out = open(Filename, 'a', newline='')
    csv_write = csv.writer(out,dialect='excel')
    for msg in msg_list:
        csv_write.writerow(msg)
    out.close()


# 访问每一页
def acc_page_msg(page_url):
    web_data = requests.get(page_url).content.decode('utf8')
    soup = BeautifulSoup(web_data, 'html.parser')
    address_list = []
    area_list = []
    num_address = 0
    num_area = 0
    msg_list = []

    # 得到了地址列表,以及区域列表
    for tag in soup.find_all(attrs="list-addr"):
        for em in tag:
            count = 0
            for a in em:
                count += 1
                if count == 1 and a.string != "[":
                    address_list.append(a.string)
                elif count == 2:
                    area_list.append(a.string)
                    num_area += 1
                elif count == 4:
                    if a.string is not None:
                        address_list[num_address] = address_list[num_address] + "-" + a.string
                    else:
                        address_list[num_address] = address_list[num_address] + "-Null"
                    num_address += 1

    # 得到了价格列表
    price_list = []
    for tag in soup.find_all(attrs="list-price"):
        price_list.append(tag.b.string)

    # 组合成为一个新的tuple——list并加上序号
    for i in range(len(price_list)):
        txt = (address_list[i], area_list[i], price_list[i])
        msg_list.append(txt)

    # 写入csv
    write_csv(msg_list)


# 爬所有的页面
def get_pages_urls():
    urls = []
    # 思明可访问页数134
    for i in range(134):
        urls.append(Url_head + "1" + Url_tail + str(i+1))
    # 湖里可访问页数134
    for i in range(134):
        urls.append(Url_head + "2" + Url_tail + str(i+1))
    # 集美可访问页数27
    for i in range(27):
        urls.append(Url_head + "3" + Url_tail + str(i+1))
    # 同安可访问页数41
    for i in range(41):
        urls.append(Url_head + "4" + Url_tail + str(i+1))
    # 翔安可访问页数76
    for i in range(76):
        urls.append(Url_head + "5" + Url_tail + str(i+1))
    # 海沧可访问页数6
    for i in range(6):
        urls.append(Url_head + "6" + Url_tail + str(i+1))
    return urls


def run():
    print("开始爬虫")
    out = open(Filename, 'a', newline='')
    csv_write = csv.writer(out, dialect='excel')
    title = ("address", "area", "price")
    csv_write.writerow(title)
    out.close()
    url_list = get_pages_urls()
    for url in url_list:
        try:
            acc_page_msg(url)
        except:
            print("格式出错", url)
    print("结束爬虫")

该模块的代码不做太多介绍,详细信息可以参考,文章开头的链接。
接着创建rent_analyse.py文件。代码如下:

# -*- coding: utf-8 -*-
from pyspark.sql import SparkSession
from pyspark.sql.types import IntegerType


def spark_analyse(filename):
    print("开始spark分析")
    # 程序主入口
    spark = SparkSession.builder.master("local").appName("rent_analyse").getOrCreate()
    df = spark.read.csv(filename, header=True)

    # max_list存储各个区的最大值,0海沧,1为湖里,2为集美,3为思明,4为翔安,5为同安;同理的mean_list, 以及min_list,approxQuantile中位数
    max_list = [0 for i in range(6)]
    mean_list = [1.2 for i in range(6)]
    min_list = [0 for i in range(6)]
    mid_list = [0 for i in range(6)]
    # 类型转换,十分重要,保证了price列作为int用来比较,否则会用str比较, 同时排除掉一些奇怪的价格,比如写字楼的出租超级贵
    # 或者有人故意标签1元,其实要面议, 还有排除价格标记为面议的
    df = df.filter(df.price != '面议').withColumn("price", df.price.cast(IntegerType()))
    df = df.filter(df.price >= 50).filter(df.price <= 40000)

    mean_list[0] = df.filter(df.area == "海沧").agg({"price": "mean"}).first()['avg(price)']
    mean_list[1] = df.filter(df.area == "湖里").agg({"price": "mean"}).first()['avg(price)']
    mean_list[2] = df.filter(df.area == "集美").agg({"price": "mean"}).first()['avg(price)']
    mean_list[3] = df.filter(df.area == "思明").agg({"price": "mean"}).first()['avg(price)']
    mean_list[4] = df.filter(df.area == "翔安").agg({"price": "mean"}).first()['avg(price)']
    mean_list[5] = df.filter(df.area == "同安").agg({"price": "mean"}).first()['avg(price)']

    min_list[0] = df.filter(df.area == "海沧").agg({"price": "min"}).first()['min(price)']
    min_list[1] = df.filter(df.area == "湖里").agg({"price": "min"}).first()['min(price)']
    min_list[2] = df.filter(df.area == "集美").agg({"price": "min"}).first()['min(price)']
    min_list[3] = df.filter(df.area == "思明").agg({"price": "min"}).first()['min(price)']
    min_list[4] = df.filter(df.area == "翔安").agg({"price": "min"}).first()['min(price)']
    min_list[5] = df.filter(df.area == "同安").agg({"price": "min"}).first()['min(price)']

    max_list[0] = df.filter(df.area == "海沧").agg({"price": "max"}).first()['max(price)']
    max_list[1] = df.filter(df.area == "湖里").agg({"price": "max"}).first()['max(price)']
    max_list[2] = df.filter(df.area == "集美").agg({"price": "max"}).first()['max(price)']
    max_list[3] = df.filter(df.area == "思明").agg({"price": "max"}).first()['max(price)']
    max_list[4] = df.filter(df.area == "翔安").agg({"price": "max"}).first()['max(price)']
    max_list[5] = df.filter(df.area == "同安").agg({"price": "max"}).first()['max(price)']

    # 返回值是一个list,所以在最后加一个[0],才能取到里面的数值
    mid_list[0] = df.filter(df.area == "海沧").approxQuantile("price", [0.5], 0.01)[0]
    mid_list[1] = df.filter(df.area == "湖里").approxQuantile("price", [0.5], 0.01)[0]
    mid_list[2] = df.filter(df.area == "集美").approxQuantile("price", [0.5], 0.01)[0]
    mid_list[3] = df.filter(df.area == "思明").approxQuantile("price", [0.5], 0.01)[0]
    mid_list[4] = df.filter(df.area == "翔安").approxQuantile("price", [0.5], 0.01)[0]
    mid_list[5] = df.filter(df.area == "同安").approxQuantile("price", [0.5], 0.01)[0]

    all_list = []
    all_list.append(min_list)
    all_list.append(max_list)
    all_list.append(mean_list)
    all_list.append(mid_list)

    print("结束spark分析")
    return all_list

然后创建draw.py文件,代码如下:

# -*- coding: utf-8 -*-
from pyecharts import Bar


def draw_bar(all_list):
    print("开始绘图")
    attr = ["海沧", "湖里", "集美", "思明", "翔安", "同安"]
    v0 = all_list[0]
    v1 = all_list[1]
    v2 = all_list[2]
    v3 = all_list[3]

    bar = Bar("厦门市租房租金概况")
    bar.add("最小值", attr, v0, is_stack=True)
    bar.add("最大值", attr, v1, is_stack=True)
    bar.add("平均值", attr, v2, is_stack=True)
    bar.add("中位数", attr, v3, is_stack=True)
    bar.render('./templates/result.html')
    print("结束绘图")

这样就完成了爬虫模块的部分。

第二步 flask后台服务端

创建app.py文件。代码如下:

from flask import Flask, render_template
from flask_socketio import SocketIO
from scripts import draw
from scripts import rent_analyse
from scripts import rentspider


def run_spider():
    print("开始总程序")
    Filename = "rent.csv"
    socketio.emit('analyse', {'data':"开始运行爬虫,请等待..."})
    rentspider.run()
    socketio.emit('analyse', {'data': "结束爬虫"})
    socketio.emit('analyse', {'data': "开始分析数据,请等待..."})
    all_list = rent_analyse.spark_analyse(Filename)
    socketio.emit('analyse', {'data': "结束分析数据"})
    socketio.emit('analyse', {'data': "开始绘图,请等待..."})
    draw.draw_bar(all_list)
    socketio.emit('analyse', {'data': "绘图结束"})
    print("结束总程序")


app = Flask(__name__)
app.config['SECRET_KEY'] = 'xmudblab'
socketio = SocketIO(app)


# 客户端访问 http://127.0.0.1:5000/,可以看到index界面
@app.route("/")
def handle_mes():
    return render_template("index.html")


# 对客户端发来的start_spider事件作出相应
@socketio.on("start_spider")
def start_spider(message):
    print(message)
    run_spider()
    socketio.emit('get_result', {'data': "请获取最后结果"})


# 对客户端发来的/Get_result事件作出相应
@app.route("/Get_result")
def Get_result():
    return render_template("result.html")


if __name__ == '__main__':
    socketio.run(app, debug=True)

这里对该部分代码进行相关的解释。
该代码分为两个部分。

程序初始化部分:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'xmudblab'
socketio = SocketIO(app)
if __name__ == '__main__':
    socketio.run(app, debug=True)

该部分是flask-socketio程序的常规写法

程序的事件相应部分:

# 客户端访问 http://127.0.0.1:5000/,可以看到index界面
@app.route("/")
def handle_mes():
    return render_template("index.html")


# 对客户端发来的start_spider事件作出相应
@socketio.on("start_spider")
def start_spider(message):
    print(message)
    run_spider()
    socketio.emit('get_result', {'data': "请获取最后结果"})


# 对客户端发来的/Get_result事件作出相应
@app.route("/Get_result")
def Get_result():
    return render_template("result.html")

这个部分实现了前后端交互时候的后台逻辑。
首先是当客户端访问http://127.0.0.1:5000 时候可以看到index界面。然后通过操作index界面来发送start_spider事件给后台,后台接受到该事件后,运行第一步的爬虫部分程序,并且会利用socket实时向客户端反馈运行进度。当爬虫部分完成后,向客户端发送可以获取结果事件。而@app.route("/Get_result")就是在相应客户端要获取结果。

第三步 客户端部分

创建index.html文件,代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>FishRent</title>
    <script src="static/js/socket.io.js"></script>
    <script src="static/js/jquery-3.1.1.min.js"></script>
    <script type="text/javascript" charset="utf-8">
        $(document).ready(function() {
            var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port);
   //连接后发送日志
            socket.on('connect', function(){
              console.log('connected')
            });
                         //断开后发送日志
            socket.on('disconnect', function () {
                console.log('disconnect');
            });
    //点击开始后,执行程序
            $('#btn').click(function() {
                socket.emit('start_spider', {data: 'Start spider'});
            });

   //接收后端发送的提示消息
            socket.on('analyse', function(msg) {
                var obj = eval(msg);
                var result = obj["data"];
                $('#log').append('<br>' + result);
            });

            //收到后台可以获取最终结果的消息后,新增按钮,点击可以获取结果
            socket.on('get_result', function(msg) {
                var obj = eval(msg);
                var result = obj["data"];
                $('#log').append('<br>' + result);
                $('#cnt').append("<br> <button id=\"get_res\">获取结果</button>");
                $('#get_res').click(function() {
                    //在当前窗口打开界面
                    window.open('/Get_result')
                    //在新窗口打开界面,二者选一个就好
                    //window.location.href = '/Get_result'
                });
            });

        });
    </script>
</head>


<body>

    <div id="cnt">
        <button id="btn">开始爬虫</button>
        <h4 id="log"></h4>
    </div>

</body>
</html>

index界面开始只有一个开始爬虫按钮,点击后向后台发送start_spider事件。然后会实时展示后台的爬虫运行进度。收到爬虫结束事件后,会增加一个获取结果按钮。点击可查看最后结果。该部分会用到js文件夹下的三个文件,jquery-3.1.1.min.js,socket.io.js,以及socket.io.js.map。这些都可以从网上下载。文章的最后也会提供完整的程序工程文件压缩包,也可以从里面获得。

运行程序

在pycharm里面运行app.py,开启后台服务。然后在浏览器访问http://127.0.0.1:5000/ 可以看到index界面,只有一个开始爬虫分析按钮。点击开始运行。运行过程如图。

点击获取结果按钮,可以看到最终结果。

最后附上工程文件压缩包。
点击下载压缩包