Bootstrap

【爬虫】应用Python爬虫爬取豆瓣租房上的帖子信息

GitHub项目地址:https://github.com/Donvink/Spider.BC
哔哩哔哩代码讲解:https://b23.tv/waSfUa
CSDN博客地址:https://blog.csdn.net/sinat_16020825/article/details/108538779

一、项目简介

1.1 简介

    本项目应用Python爬虫、Flask框架、Echarts、WordCloud等技术将豆瓣租房信息爬取出来保存于Excel和数据库中,进行数据可视化操作、制作网页展示。

    主要内容包括三部分:

  • douban_renting:Python 爬虫将 豆瓣租房上的租房信息爬取出来,解析数据后将其存储于Excel和SQLite数据库中。
  • flask_demo:测试使用Flask框架。
  • douban_flask:应用Flask框架、Echarts、WordCloud技术将数据库中的租房信息以网页的形式展示出来。

1.2 Results

1.2.1 Excel保存数据

EXCEL

1.2.2 网页展示数据

  • 首页
    首页

  • 帖子列表
    帖子列表

  • 词云
    词云


作者Donvink

二、代码分析

2.1 概述

2.1.1 robots协议

    在爬取数据之前,必须先查看目标网站的robots协议,查阅网站允许和禁止的爬取的数据,对于网站禁止爬虫访问的数据,一定不要去爬取,否则严重的话需要承担相应的法律责任。网站的robots协议地址一般是:https://网站地址/robots.txt。

    豆瓣robots协议地址为:https://www.douban.com/robots.txt。

robots协议
    原则上禁止访问robots协议上disallow的地址。

2.1.2 爬取流程

    准备工作 -> 获取数据 -> 解析内容 -> 保存数据

    调用的库:

import sys
from bs4 import BeautifulSoup   # 网页解析,获取数据
import re                       # 正则表达式,进行文件匹配
import urllib                   # 制定URL,获取网页数据
import urllib.request
import urllib.error
import xlwt                     # 进行excel操作
import sqlite3                  # 进行SQLite数据库操作

需要安装requests、urllib3、xlwt和pysqlite3库,在终端执行一下命令。

pip install requests urllib3 xlwt pysqlite3

2.2 准备工作

    URL分析:

    首页 https://www.douban.com/group/558444/ (包含50条数据,和第一第二页数据相同);

    第一页 https://www.douban.com/group/558444/discussion?start=0;

    第二页 https://www.douban.com/group/558444/discussion?start=25。

    总结:

    1)页面包含x条租房数据,每页25条,

    2)每页的URL的不同之处:最后的数值 = (页数 - 1) * 25。

    # 1.爬取网页    2、逐一解析网页数据
    baseurl = 'https://www.douban.com/group/558444/discussion?start='   # 基本URL
    pagecount = 10                                                      # 爬取的网页数量
    num = 25                                                            # 每页的帖子数
    datalist = getData(baseurl, pagecount, num)                         # 爬取网页、解析数据
    
    # 调用获取页面信息的函数pagecount次
    for i in range(0, pagecount):
        url = baseurl + str(i * num)                    # 拼接网页链接

        # 1.爬取网页
        html = askURL(url)                              # 保存获取到的网页源码

2.2.1 分析页面

    1)借助Chrome开发者工具(F12)来分析页面,在Elements下找到需要的数据位置。

    2)在页面中选择一个元素以进行检查(Ctrl+Shift+C)(开发者工具最左上方的小箭头),点击页面内容即可定位到具体标签位置。

    3)点击Network,可以查看每个时间点发送的请求和交互情况,可点击最上方小红点(停止记录网络日志Ctrl+E)停止交互。

    4)Headers查看发送给服务器的命令情况。

    5)服务器返回信息可在Response中查看。

2.2.2 编码规范

    1)一般Python程序第一行需要加入

# -*- coding = utf-8 -*-

    这样可以在代码中包含中文。

    2)使用函数实现单一功能或相关联功能的代码段,可以提高可读性和代码重复利用率。

    3)Python文件中可以加入main函数用于测试程序

if __name__ == "__main__":

    4)Python使用#添加注释,说明代码(段)的作用。

2.2.3 引入模块

    sys, bs4 -> BeautifulSoup, re, urllib, xlwt。

2.3 获取数据

    python一般使用urllib库获取页面。

    获取页面数据:

    1)对每一个页面,调用askURL函数获取页面内容

def askURL(url):
    """
    得到指定一个URL的网页信息
    :param url: 网页链接
    :return:
    """

    2)定义一个获取页面的函数askURL,传入一个url参数,表示网址,如https://www.douban.com/group/558444/discussion?start=0

# 模拟浏览器头部信息,向豆瓣服务器发送消息
    head = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37'
    }   # 用户代理,表示告诉豆瓣服务器,我们是什么类型的机器、浏览器(本质上是告诉浏览器,我们可以接收什么水平的文件内容)

    3)urllib.Request生成请求,urllib.urlopen发送请求获取响应,read获取页面内容

    request = urllib.request.Request(url, headers=head)     # 发送请求
    html = ''

    4)在访问页面时经常会出现错误,为了程序正常运行,假如异常捕获try…except…语句

    # 捕获异常
    try:
        response = urllib.request.urlopen(request)          # 取得响应
        html = response.read().decode('utf-8')              # 获取网页内容
        # print(html)
    except urllib.error.URLError as e:                      # 若发生异常,则打印相关信息
        if hasattr(e, 'code'):                              # 异常代码
            print(e.code)
        if hasattr(e, 'reason'):                            # 异常原因
            print(e.reason)

    return html

2.4 解析内容

    对爬取的html文件进行解析

    1、使用BeautifulSoup定位特定的标签位置

        # 2.逐一解析数据
        soup = BeautifulSoup(html, 'html.parser')       # 对网页源码进行解析
        # print(soup)                                   # 测试html是否能被解析

    2、使用正则表达式找到具体的内容

        for item in soup.find_all('tr', class_=''):     # 找到每一个帖子选项(查找符合要求的字符串,形成列表)
            # print(item)                               # 测试:查看item全部信息
            data = []
            item = str(item)                            # 转换成字符串
            # print(item)                               # 测试item

            # 帖子名
            title = re.findall(findTitle, item)[0]
            # print(title)                              # 测试:查看title
            data.append(title)                          # 添加帖子名

            # 帖子详情链接
            link = re.findall(findLink, item)
            # print(link)                               # 测试:查看link信息

            # 进入帖子里爬取租房信息介绍内容和照片
            info, imglink = getInfo(link[0])
            data.append(info)                           # 添加详细信息
            data.append(imglink)                        # 添加图片链接
            if (len(link) == 2):
                data.append(link[0])                    # 添加详情链接
                # alink = re.findall(findLink, item)[1]
                # print(alink)                          # 测试:查看link信息
                data.append(link[1])                    # 添加发帖人主页链接
            else:
                data.append(link[0])
                data.append(' ')                        # 留空

            # 回帖数
            rcount = re.findall(findRCount, item)[0]
            # print(rcount)                             # 测试:查看回帖数
            if rcount == '':
                # print(0)                              # 测试:查看回帖数
                data.append(0)                          # 添加回帖数
            else:
                # print(rcount)                         # 测试:查看回帖数
                data.append(int(rcount))                # 添加回帖数

            # 最后回帖时间
            rtime = re.findall(findRTime, item)[0]
            # print(rtime)
            data.append(rtime)                          # 添加最后回帖时间
            # otitle = titles[1].replace('/', '')       # 去掉无关符号,或re.sub()

            datalist.append(data)                       # 把处理好的信息放入datalist
    # print(datalist)                                   # 测试

2.5 保存数据

    Excel表格存储:利用python库xlwt将抽取的数据datalist写入Excel表格。

def saveData(datalist, pagecount, num, savepath):
    """
    将解析后的数据保存在Excel文件中
    :param datalist: 网页解析后的数据
    :param pagecount: 爬取的网页数量
    :param num: 每页的帖子数
    :param savepath: Excel文件保存路径
    :return:
    """

    # print('saving...')
    book = xlwt.Workbook(encoding='utf-8', style_compression=0)     # 新建workbook
    sheet = book.add_sheet('豆瓣租房信息', cell_overwrite_ok=True)    # 添加sheet
    # 列名
    col = ('序号', '帖子名', '详细介绍', '图片链接', '帖子详情链接', '发帖人主页链接', '回帖数', '最后回帖时间')
    # 写入列名
    for i in range(0, len(col)):
        sheet.write(0, i, col[i])
    # 将每条帖子的相关内容写入Excel对应行中
    for i in range(0, pagecount*num):
        # print('第%d条' % (i+1))
        data = datalist[i]
        sheet.write(i+1, 0, i+1)            # 写入序号
        for j in range(0, len(data)):
            sheet.write(i+1, j+1, data[j])  # 写入数据
    book.save(savepath)                     # 保存文件
    # print('Successful!')

    数据库存储

def saveData2DB(datalist, dbpath):
    """
    将解析后的数据保存在数据库文件中
    :param datalist: 网页解析后的数据
    :param dbpath: 数据库文件保存路径
    :return:
    """

    init_DB(dbpath)                                 # 初始化数据库
    conn = sqlite3.connect(dbpath)                  # 连接数据库
    cur = conn.cursor()                             # 获取游标

    # 将数据逐一保存到数据库中
    for data in datalist:
        for index in range(len(data)):
            if index != 5:                          # index为5的数据类型是int
                data[index] = '"'+data[index]+'"'   # 每项的字符串需要加上双引号或单引号
        # 插入字符串,以逗号隔开
        sql = '''
            insert into renting(
            title, introduction, img_link, title_link, person_link, re_count, re_time)
            values(%s)''' % ",".join(str(v) for v in data)
        # print(sql)
        cur.execute(sql)                            # 执行数据库操作
        conn.commit()                               # 提交
    cur.close()                                     # 关闭游标
    conn.close()                                    # 关闭连接

2.7 网页展示数据

待更新

2.8 TODO

  • 添加筛选:区域、小区名、地铁站等
  • 定义类,获取各种不同途径信息(豆瓣、自如、链家、贝壳等)

三、完整代码

# -*- coding = utf-8 -*-
# @Time: 2020/5/30 19:57
# @Author: Donvink
# @File: douban.py
# @Software: PyCharm

"""
查看robots协议:https://www.douban.com/robots.txt

零、流程
    准备工作 -> 获取数据 -> 解析内容 -> 保存数据

一、准备工作
    URL分析:
        首页  https://www.douban.com/group/558444/ (包含50条数据,和第一第二页数据相同)
        第一页 https://www.douban.com/group/558444/discussion?start=0
        第二页 https://www.douban.com/group/558444/discussion?start=25
        1)页面包含x条租房数据,每页25条
        2)每页的URL的不同之处:最后的数值 = (页数 - 1) * 25
    1、分析页面
        1)借助Chrome开发者工具(F12)来分析页面,在Elements下找到需要的数据位置。
        2)在页面中选择一个元素以进行检查(Ctrl+Shift+C)(开发者工具最左上方的小箭头),点击页面内容即可定位到具体标签位置。
        3)点击Network,可以查看每个时间点发送的请求和交互情况,可点击最上方小红点(停止记录网络日志Ctrl+E)停止交互。
        4)Headers查看发送给服务器的命令情况。
        5)服务器返回信息可在Response中查看。
    2、编码规范
        1)一般Python程序第一行需要加入
                    # -*- coding = utf-8 -*-
        这样可以在代码中包含中文。
        2)使用函数实现单一功能或相关联功能的代码段,可以提高可读性和代码重复利用率。
        3)Python文件中可以加入main函数用于测试程序
                    if __name__ == "__main__":
        4)Python使用#添加注释,说明代码(段)的作用
    3、引入模块
        sys, bs4 -> BeautifulSoup, re, urllib, xlwt

二、获取数据
    python一般使用urllib库获取页面
    获取页面数据:
        1)对每一个页面,调用askURL函数获取页面内容
        2)定义一个获取页面的函数askURL,传入一个url参数,表示网址,如https://www.douban.com/group/558444/discussion?start=0
        3)urllib.Request生成请求,urllib.urlopen发送请求获取响应,read获取页面内容
        4)在访问页面时经常会出现错误,为了程序正常运行,假如异常捕获try...except...语句

三、解析内容
    对爬取的html文件进行解析
    1、使用BeautifulSoup定位特定的标签位置
    2、使用正则表达式找到具体的内容

四、保存数据
    Excel表格存储:利用python库xlwt将抽取的数据datalist写入Excel表格

TODO:
    1、添加筛选【区域,小区名,地铁站】
    2、定义类,获取各种不同途径信息(豆瓣,自如,链家,贝壳)
"""

import sys
from bs4 import BeautifulSoup   # 网页解析,获取数据
import re                       # 正则表达式,进行文件匹配
import urllib                   # 制定URL,获取网页数据
import urllib.request
import urllib.error
import xlwt                     # 进行excel操作
import sqlite3                  # 进行SQLite数据库操作


def main():
    """
    主函数入口
    1.爬取网页
    2.逐一解析数据
    3.保存数据
    :return:
    """

    print('开始爬取······')

    # 1.爬取网页    2、逐一解析网页数据
    baseurl = 'https://www.douban.com/group/558444/discussion?start='   # 基本URL
    pagecount = 10                                                      # 爬取的网页数量
    num = 25                                                            # 每页的帖子数
    datalist = getData(baseurl, pagecount, num)                         # 爬取网页、解析数据

    # 3.保存数据
    savepath = 'douban_renting.xls'                                     # Excel文件保存路径
    saveData(datalist, pagecount, num, savepath)                        # 将数据保存在Excel中
    dbpath = 'douban_renting.db'                                        # 数据库文件保存路径
    saveData2DB(datalist, dbpath)                                       # 将数据保存在数据库中
    # askURL('https://www.douban.com/group/558444/discussion?start=0')  # 测试askURL

    print('爬取完毕!')


def getData(baseurl, pagecount, num):
    """
    爬取网页,逐一对网页数据进行分析。主要内容有:
            '帖子名', '详细介绍', '图片链接', '帖子详情链接', '发帖人主页链接', '回帖数', '最后回帖时间'
    :param baseurl: 基本URL
    :param pagecount: 爬取的网页数量
    :param num: 每页的帖子数
    :return:
    """

    datalist = []
    # 正则表达式规则
    findTitle = re.compile(r'title="(.*?)"', re.S)                  # 找到帖子名,有的帖子名带有\n
    findLink = re.compile(r'href="(.*?)"')                          # 找到帖子详情链接
    findRCount = re.compile(r'<.*class="r-count".*>(.*?)</td>')     # 找到回帖数
    findRTime = re.compile(r'<.*class="time".*">(.*?)</td>')        # 找到最后回帖时间
    # 注意:正则表达式匹配空格时,需用.*或\s匹配

    # 调用获取页面信息的函数pagecount次
    for i in range(0, pagecount):
        url = baseurl + str(i * num)                    # 拼接网页链接

        # 1.爬取网页
        html = askURL(url)                              # 保存获取到的网页源码

        # 2.逐一解析数据
        soup = BeautifulSoup(html, 'html.parser')       # 对网页源码进行解析
        # print(soup)                                   # 测试html是否能被解析
        for item in soup.find_all('tr', class_=''):     # 找到每一个帖子选项(查找符合要求的字符串,形成列表)
            # print(item)                               # 测试:查看item全部信息
            data = []
            item = str(item)                            # 转换成字符串
            # print(item)                               # 测试item

            # 帖子名
            title = re.findall(findTitle, item)[0]
            # print(title)                              # 测试:查看title
            data.append(title)                          # 添加帖子名

            # 帖子详情链接
            link = re.findall(findLink, item)
            # print(link)                               # 测试:查看link信息

            # 进入帖子里爬取租房信息介绍内容和照片
            info, imglink = getInfo(link[0])
            data.append(info)                           # 添加详细信息
            data.append(imglink)                        # 添加图片链接
            if (len(link) == 2):
                data.append(link[0])                    # 添加详情链接
                # alink = re.findall(findLink, item)[1]
                # print(alink)                          # 测试:查看link信息
                data.append(link[1])                    # 添加发帖人主页链接
            else:
                data.append(link[0])
                data.append(' ')                        # 留空

            # 回帖数
            rcount = re.findall(findRCount, item)[0]
            # print(rcount)                             # 测试:查看回帖数
            if rcount == '':
                # print(0)                              # 测试:查看回帖数
                data.append(0)                          # 添加回帖数
            else:
                # print(rcount)                         # 测试:查看回帖数
                data.append(int(rcount))                # 添加回帖数

            # 最后回帖时间
            rtime = re.findall(findRTime, item)[0]
            # print(rtime)
            data.append(rtime)                          # 添加最后回帖时间
            # otitle = titles[1].replace('/', '')       # 去掉无关符号,或re.sub()

            datalist.append(data)                       # 把处理好的信息放入datalist
    # print(datalist)                                   # 测试
    return datalist


def getInfo(url):
    """
    进入每个帖子里爬取租房信息介绍内容和照片链接
    :param url: 帖子URL
    :return:
    """

    tempinfo = []
    info = ''                                               # 租房详细信息介绍
    tempimglink = []
    imglink = ''                                            # 图片链接

    # 正则表达式规则
    findInfo = re.compile(r'<p>(.*?)</p>', re.S)            # 找到帖子详情信息,有的帖子名带有\n
    findImgLink = re.compile(r'src="(.*?)"')                # 找到帖子附带的照片链接

    html = askURL(url)                                      # 保存获取到的网页源码
    soup = BeautifulSoup(html, 'html.parser')               # 解析网页源码
    topic = soup.find_all('div', class_='topic-richtext')   # 找到介绍内容和照片所在标签
    topic = str(topic)                                      # 转换为字符串

    # 详细介绍
    tempinfo = re.findall(findInfo, topic)
    info = " , ".join(str(v) for v in tempinfo)             # 帖子里详细介绍内容会有换行,用逗号隔开

    # 照片链接
    tempimglink = re.findall(findImgLink, topic)
    imglink = " , ".join(str(v) for v in tempimglink)       # 每个帖子可能会附带多张照片,用逗号隔开

    return info, imglink


def askURL(url):
    """
    得到指定一个URL的网页信息
    :param url: 网页链接
    :return:
    """

    # 模拟浏览器头部信息,向豆瓣服务器发送消息
    head = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37'
    }   # 用户代理,表示告诉豆瓣服务器,我们是什么类型的机器、浏览器(本质上是告诉浏览器,我们可以接收什么水平的文件内容)
    request = urllib.request.Request(url, headers=head)     # 发送请求
    html = ''

    # 捕获异常
    try:
        response = urllib.request.urlopen(request)          # 取得响应
        html = response.read().decode('utf-8')              # 获取网页内容
        # print(html)
    except urllib.error.URLError as e:                      # 若发生异常,则打印相关信息
        if hasattr(e, 'code'):                              # 异常代码
            print(e.code)
        if hasattr(e, 'reason'):                            # 异常原因
            print(e.reason)

    return html


def saveData(datalist, pagecount, num, savepath):
    """
    将解析后的数据保存在Excel文件中
    :param datalist: 网页解析后的数据
    :param pagecount: 爬取的网页数量
    :param num: 每页的帖子数
    :param savepath: Excel文件保存路径
    :return:
    """

    # print('saving...')
    book = xlwt.Workbook(encoding='utf-8', style_compression=0)     # 新建workbook
    sheet = book.add_sheet('豆瓣租房信息', cell_overwrite_ok=True)    # 添加sheet
    # 列名
    col = ('序号', '帖子名', '详细介绍', '图片链接', '帖子详情链接', '发帖人主页链接', '回帖数', '最后回帖时间')
    # 写入列名
    for i in range(0, len(col)):
        sheet.write(0, i, col[i])
    # 将每条帖子的相关内容写入Excel对应行中
    for i in range(0, pagecount*num):
        # print('第%d条' % (i+1))
        data = datalist[i]
        sheet.write(i+1, 0, i+1)            # 写入序号
        for j in range(0, len(data)):
            sheet.write(i+1, j+1, data[j])  # 写入数据
    book.save(savepath)                     # 保存文件
    # print('Successful!')


def saveData2DB(datalist, dbpath):
    """
    将解析后的数据保存在数据库文件中
    :param datalist: 网页解析后的数据
    :param dbpath: 数据库文件保存路径
    :return:
    """

    init_DB(dbpath)                                 # 初始化数据库
    conn = sqlite3.connect(dbpath)                  # 连接数据库
    cur = conn.cursor()                             # 获取游标

    # 将数据逐一保存到数据库中
    for data in datalist:
        for index in range(len(data)):
            if index != 5:                          # index为5的数据类型是int
                data[index] = '"'+data[index]+'"'   # 每项的字符串需要加上双引号或单引号
        # 插入字符串,以逗号隔开
        sql = '''
            insert into renting(
            title, introduction, img_link, title_link, person_link, re_count, re_time)
            values(%s)''' % ",".join(str(v) for v in data)
        # print(sql)
        cur.execute(sql)                            # 执行数据库操作
        conn.commit()                               # 提交
    cur.close()                                     # 关闭游标
    conn.close()                                    # 关闭连接


def init_DB(dbpath):
    """
    初始化数据库
    :param dbpath: 数据库保存路径
    :return:
    """

    # create table renting
    # 若不加if not exists,则每次运行程序需要先删除database;否则不用先删除,但无法更新sql里的格式
    sql = '''
        create table if not exists renting
        (
        id integer primary key autoincrement,
        title text,
        introduction text,
        img_link text,
        title_link text,
        person_link text,
        re_count numeric,
        re_time text
        )
    '''                             # 创建数据表
    conn = sqlite3.connect(dbpath)  # 创建或连接数据库
    cursor = conn.cursor()          # 获取游标
    cursor.execute(sql)             # 执行数据库操作
    conn.commit()                   # 提交
    cursor.close()                  # 关闭游标
    conn.close()                    # 关闭


if __name__ == "__main__":          # 当程序执行时
    # 调用函数
    main()
    # init_DB('租房.db')             # 测试初始化数据库

;