自己动手搭建一个简单的基于Hadoop的离线分析系统之一——网络爬虫

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_40793975/article/details/85006097

自己动手搭建一个简单的基于Hadoop的离线分析系统之一——网络爬虫

之前对大数据颇感兴趣,在学习了一个月的相关原理和应用后,感觉自己需要动手一个实战项目来巩固之前学到的东西,加之很早之前就接触过一些爬虫知识,因此利用手上现有的资源(一台笔记本电脑)来搭建一个关于房屋租赁的简单的基于Hadoop的离线分析系统,其中包含了爬虫、HDFS、MapReduce、MySQL以及hive的简单应用。
由于手上硬件资源着实有限,该系统是实际应用系统的超级简化版,旨在对大数据的一部分相关知识综合起来做一个简单应用,请大神勿喷!

项目整体框架

一、基本环境

  为了避免后面出现各种环境问题,这里首先给出我的基本环境配置信息:
1. Windows
  a. Window10 64位操作系统
  b. Python3.7
  c. jdk1.7.0_80
  d. maven3.6.0
  e. VMware Workstation 14 Pro
  f. SecureCRT 8.0
2. Linux
  a. Centos7 64位
  b. Python3.6.5
  c. jdk1.7.0_80
  d. Hadoop2.6.5
  e. hive1.2.1
  f. MySQL5.7.24

二、待爬信息

  我选择的房屋租赁信息网站是小猪短租,该网站没有使用大量的JS渲染以及异步加载方式等反爬取手段,即使IP被封也可以通过输入验证码来解封,并不影响接下来一段时间的爬取。
  待爬信息有:出租房屋所在省、市、区,起步价格,房屋面积,适宜居住的人数,出租标题信息,详细地址,如下图所示。

三、爬虫代码(For Windows)

'''
@author: Ἥλιος
@CSDN:https://blog.csdn.net/qq_40793975/article/details/82734297
Platform:Windows Python3
'''
print(__doc__)

from bs4 import BeautifulSoup
import requests
import re
import time
import random
import sys
import getopt

url = 'http://sh.xiaozhu.com/'
proxies = {"http": "123.114.202.119:8118"}  # 代理IP
header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0'
}   # 消息头
MunicList = ['sh', 'tj', 'cq', 'bj']    # 直辖市列表


def get_page_links(url=None, page_links=None, label=0):
    """
    爬取某个网页上的全部房屋租赁链接
    :param url: 网页链接
    :param page_links: 全部房屋租赁链接
    :param label: 标志位,该网页是第一页为1,否则为1
    :return: 状态码,0爬取成功,1IP被封导致爬取失败,2爬取成功且当前网页为最后一页
    """
    sec = (random.random() + 0) * 10
    time.sleep(sec)  # 请求延时

    wb_data = requests.get(url, headers=header)

    if wb_data.status_code == 202:  # 页面响应状态202,IP被封
        print("IP blocked")
        return 1

    soup = BeautifulSoup(wb_data.text, 'lxml')

    links = soup.select('.pic_list > li > a:nth-of-type(1)')

    for link in links:
        page_link = link.get('href')
        page_links.append(page_link)

    info = soup.select('a.font_st')
    if len(info) <= 1 and label == 0:  # 判断当前页是不是最后一页,不检查第一页
        print("Last page")
        return 2

    return 0


def detail2adress(str=None):
    """
    使用正则表达式提取详细地址(非直辖市)中的省或行政区、市或自治区、区或县
    :param str: 详细地址
    :return: 省或行政区、市或自治区、区或县组成的列表
    """
    result_default = [None, None, None]
    if str is None:
        return result_default
    result = re.search('(?P<province>[^省]+省|[^行政区]+行政区)(?P<city>[^市]+市|[^自治区]+自治区)(?P<county>[^县]+县|[^区]+区)', str)
    if result is None:
        return result_default
    return list(result.groups())


def detail2adress_Munic(str=None):
    """
    使用正则表达式提取详细地址(直辖市)中的省或行政区、市或自治区、区或县
    :param str: 详细地址
    :return: 省或行政区、市或自治区、区或县组成的列表
    """
    result_default = [None, None, None]
    if str is None:
        return result_default
    result = re.search('(?P<city>[^市]+市)(?P<county>[^区]+区)', str)
    if result is None:
        return result_default
    result = list(result.groups())
    result_default[0] = result[0]
    result_default[1:3] = result[:]
    return result_default


def get_rental_information(url=None, Munic=0):
    """
    根据链接爬取某个房屋租赁信息
    :param url: 待爬取房屋租赁信息的链接
    :param Munic: 标志位,1是直辖市,否则为0
    :return: 房屋租赁信息
    """
    sec = (random.random() + 0) * 10
    time.sleep(sec)  # 请求延时

    wb_data = requests.get(url, headers=header)
    if wb_data.status_code == 202:
        print("IP blocked")
        return 1
    soup = BeautifulSoup(wb_data.text, 'lxml')

    address = soup.select('.pho_info > p')[0].get('title')
    price = soup.select('.day_l > span:nth-of-type(1)')[0].text
    size = soup.select('.border_none > p')[0].text
    number = soup.select('.h_ico2')[0].text
    title = soup.select('.pho_info > h4:nth-of-type(1) > em:nth-of-type(1)')[0].text

    pattern_size = re.compile(r'\d+')   # 查找数字
    pattern_number = re.compile(r'\d+')   # 查找数字

    size = pattern_size.findall(size.split(' ')[0])[0]
    number = pattern_number.findall(number)[0]

    data = {
        'address': detail2adress_Munic(address) if Munic else detail2adress(address),
        'price': int(price),
        'size': int(size),
        'number': int(number),
        'detail_address': address,
        'title': title
    }

    return data


def get_area_page_links(area=None):
    """
    爬取某所有网页上的全部房屋租赁链接
    :param area: 这些网页所属的地区
    :return: 全部房屋租赁链接
    """
    sec = (random.random() + 1) * 10
    time.sleep(sec)

    page_links = []
    for i in range(100):
        label = 0
        if i + 1 == 1:
            label = 1
            url = 'http://{}.xiaozhu.com/'.format(area)
        else:
            url = 'http://{}.xiaozhu.com/search-duanzufang-p{}-0/'.format(area, i + 1)
        res = get_page_links(url, page_links, label)
        print("Area: " + area + " ,Page: " + str(i+1))
        print(len(page_links))
        if res != 0:
            break
    return page_links


def get_area_rental_information(area=None):
    """
    根据该地区的全部房屋租赁链接爬取房屋租赁信息
    :param area: 这些房屋租赁链接所属的地区
    :return: 状态码,0爬取成功, 1IP被封导致爬取失败
    """
    Munic = 0
    if area in MunicList:
        Munic = 1
    area_page_links = get_area_page_links(area)
    filename = 'F:\\{}_rental_information.txt'.format(area)  # 租赁信息存储路径
    try:
        fw = open(filename, 'w', encoding='utf-8')
    except IOError:
        print("Fail in open file" + filename)
    else:
        link_num = 0
        for page_link in area_page_links:
            link_num += 1
            rental_data = get_rental_information(page_link, Munic)
            if rental_data == 1:
                fw.flush()
                fw.close()
                return 1
            line = rental_data['address'][0] + '\t' + rental_data['address'][1] \
                   + '\t' + rental_data['address'][2] + '\t' + str(rental_data['price'])\
                   + '\t' + str(rental_data['size']) + '\t' + str(rental_data['number']) + '\t' \
                   + rental_data['detail_address'] + '\t' + rental_data['title'] + '\n'
            print("Line " + str(link_num) + ": " + line)
            try:
                fw.writelines(line)
            except UnicodeEncodeError:
                pass
            fw.flush()
        fw.close()
    return 0


opts, args = getopt.getopt(sys.argv[1:], "ha:")
area = None
for op, value in opts:
    if op == "-h":
        print("Usage: python 爬虫.py -a area")
        print("Optimal areas are in file: areas.txt Or You can search them on www.xiaozhu.com")
    elif op == "-a":
        area = value
        get_area_rental_information(area=area)
    else:
        print("ParameterError Usage: python 爬虫.py -a area")

四、代码详情(For Windows)

  该代码只对两种响应状态码进行处理,200代表网页信息被正常加载,202则表示IP被封,然后使用beautifulsoup对网页进行解析,提取我们所需要的信息。程序先对所给定区域的全部房屋租赁链接进行逐页面的爬取,在爬取到所有链接后,根据每条信息爬取对应的房屋租赁信息,每爬到一条信息就整合到到一个字典中,最后反序列化到一个自定的输出文件(filename)中,默认存储路径是F盘
  在命令行中直接输入“python .\爬虫_windows.py -a 区域”就开始爬取该地区的全部租赁信息,

输入“python .\爬虫_windows.py -h”查看帮助信息,

  该代码采用的反爬虫应对方法是当前线程随机等待一段时间(sys.sleep())再继续发送下一个请求,以此来模仿人的浏览方式,另外,该网站还会检查请求头中User-Agent的内容,requests中get方法默认的User-Agent是Python访问,因此这里对headers进行了替换,更多的反爬虫应对措施见下文。

五、爬虫代码(For Linux)

'''
@author: Ἥλιος
@CSDN:https://blog.csdn.net/qq_40793975/article/details/82734297
Platform:Windows Python3
'''
print(__doc__)

from bs4 import BeautifulSoup
import requests
import re
import time
import random
import sys
import getopt

url = 'http://sh.xiaozhu.com/'
proxies = {"http": "123.114.202.119:8118"}  # 代理IP
header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0'
}   # 消息头
MunicList = ['sh', 'tj', 'cq', 'bj']    # 直辖市列表


def get_page_links(url=None, page_links=None, label=0):
    """
    爬取某个网页上的全部房屋租赁链接
    :param url: 网页链接
    :param page_links: 全部房屋租赁链接
    :param label: 标志位,该网页是第一页为1,否则为1
    :return: 状态码,0爬取成功,1IP被封导致爬取失败,2爬取成功且当前网页为最后一页
    """
    sec = (random.random() + 0) * 10
    time.sleep(sec)  # 请求延时

    wb_data = requests.get(url, headers=header)

    if wb_data.status_code == 202:  # 页面响应状态202,IP被封
        print("IP blocked")
        return 1

    soup = BeautifulSoup(wb_data.text, 'lxml')

    links = soup.select('.pic_list > li > a:nth-of-type(1)')

    for link in links:
        page_link = link.get('href')
        page_links.append(page_link)

    info = soup.select('a.font_st')
    if len(info) <= 1 and label == 0:  # 判断当前页是不是最后一页,不检查第一页
        print("Last page")
        return 2

    return 0


def detail2adress(str=None):
    """
    使用正则表达式提取详细地址(非直辖市)中的省或行政区、市或自治区、区或县
    :param str: 详细地址
    :return: 省或行政区、市或自治区、区或县组成的列表
    """
    result_default = [None, None, None]
    if str is None:
        return result_default
    result = re.search('(?P<province>[^省]+省|[^行政区]+行政区)(?P<city>[^市]+市|[^自治区]+自治区)(?P<county>[^县]+县|[^区]+区)', str)
    if result is None:
        return result_default
    return list(result.groups())


def detail2adress_Munic(str=None):
    """
    使用正则表达式提取详细地址(直辖市)中的省或行政区、市或自治区、区或县
    :param str: 详细地址
    :return: 省或行政区、市或自治区、区或县组成的列表
    """
    result_default = [None, None, None]
    if str is None:
        return result_default
    result = re.search('(?P<city>[^市]+市)(?P<county>[^区]+区|[^县]+县)', str)
    if result is None:
        return result_default
    result = list(result.groups())
    result_default[0] = result[0]
    result_default[1:3] = result[:]
    return result_default


def get_rental_information(url=None, Munic=0):
    """
    根据链接爬取某个房屋租赁信息
    :param url: 待爬取房屋租赁信息的链接
    :param Munic: 标志位,1是直辖市,否则为0
    :return: 房屋租赁信息
    """
    sec = (random.random() + 0) * 10
    time.sleep(sec)  # 请求延时

    wb_data = requests.get(url, headers=header)
    print(wb_data.status_code)
    if wb_data.status_code == 202:
        print("IP blocked")
        return 1
    soup = BeautifulSoup(wb_data.text, 'lxml')

    address = soup.select('.pho_info > p')[0].get('title')
    price = soup.select('.day_l > span:nth-of-type(1)')[0].text
    size = soup.select('.border_none > p')[0].text
    number = soup.select('.h_ico2')[0].text
    title = soup.select('.pho_info > h4:nth-of-type(1) > em:nth-of-type(1)')[0].text

    pattern_size = re.compile(r'\d+')   # 查找数字
    pattern_number = re.compile(r'\d+')   # 查找数字

    size = pattern_size.findall(size.split(' ')[0])[0]
    number = pattern_number.findall(number)[0]

    data = {
        'address': detail2adress_Munic(address) if Munic else detail2adress(address),
        'price': int(price),
        'size': int(size),
        'number': int(number),
        'detail_address': address,
        'title': title
    }

    return data


def get_area_page_links(area=None):
    """
    爬取某所有网页上的全部房屋租赁链接
    :param area: 这些网页所属的地区
    :return: 全部房屋租赁链接
    """
    sec = (random.random() + 1) * 10
    time.sleep(sec)

    page_links = []
    for i in range(100):
        label = 0
        if i + 1 == 1:
            label = 1
            url = 'http://{}.xiaozhu.com/'.format(area)
        else:
            url = 'http://{}.xiaozhu.com/search-duanzufang-p{}-0/'.format(area, i + 1)
        res = get_page_links(url, page_links, label)
        print("Area: " + area + " ,Page: " + str(i+1))
        print(len(page_links))
        if res != 0:
            break
    return page_links


def get_area_rental_information(area=None, path=None):
    """
    根据该地区的全部房屋租赁链接爬取房屋租赁信息
    :param area: 这些房屋租赁链接所属的地区
    :return: 状态码,0爬取成功, 1IP被封导致爬取失败
    """
    Munic = 0
    if area in MunicList:
        Munic = 1
    area_page_links = get_area_page_links(area)
    filename = path + area + '_rental_information.txt'  # 租赁信息存储路径
    try:
        fw = open(filename, 'w', encoding='utf-8')
    except IOError:
        print("Fail in open file" + filename)
    else:
        link_num = 0
        for page_link in area_page_links:
            link_num += 1
            rental_data = get_rental_information(page_link, Munic)
            failed_time = 1
            while rental_data == 1 and failed_time <= 3:  # 失败重试
                sys.wait(10000)
                print("Retry " + failed_time + " time!")
                rental_data = get_rental_information(page_link, Munic)
                failed_time += 1
            if rental_data == 1:
                print("Retry Failed!")
                raise Exception("Crawling Failed!Next Area")
                return 1
            try:
                line = rental_data['address'][0] + '\t' + rental_data['address'][1] \
                       + '\t' + rental_data['address'][2] + '\t' + str(rental_data['price'])\
                       + '\t' + str(rental_data['size']) + '\t' + str(rental_data['number']) + '\t' \
                       + rental_data['detail_address'] + '\t' + rental_data['title'] + '\n'
            except TypeError:
                print("Error in URL: " + page_link)
                continue
            else:
                print("Line " + str(link_num) + ": " + line)
                try:
                    fw.writelines(line)
                except UnicodeEncodeError:
                    pass
            fw.flush()
        fw.close()
    return 0


opts, args = getopt.getopt(sys.argv[1:], "ha:p:")
area = None
path = None
opt_num = len(opts)
opt_id = 0
for op, value in opts:
    opt_id += 1
    if op == "-h" and opt_num == 1 and value == None:
        print("Usage: python pachong.py -a area -p path")
        print("Optimal areas are in file: areas.txt Or You can search them on www.xiaozhu.com")
    elif op == "-a" and value != None and (opt_num == 1 or opt_num == 2):
        area = value
        if opt_num == 1:
            get_area_rental_information(area=area, path='//root//simple_log_analysis//srcdata//')
        else:
            if opt_id == opt_num:
                get_area_rental_information(area=area, path=path)
    elif op == "-p" and value != None and (opt_num == 1 or opt_num == 2):
        path = value
        if opt_num == 1:
            get_area_rental_informationn(area='sh', path=path)
        else:
            if opt_id == opt_num:                                   
                get_area_rental_information(area=area, path=path)
    else:
        print("ParameterError Usage: python pachong.py -a area -p path or -h for Help")

六、代码详情(For Linux)

  代码总体相较于Windows版本没有什么较大的改动,在爬取过程中遇到IP被封的情况,该代码会重试三次,这样就有30秒的时间重新恢复访问(在页面上输入验证码,解封后建议不要关闭该页面,下次IP再被封后可以直接刷新该页面),而不需要重新从头爬取,如下图所示

另外,多加了一个命令行参数-p,来指代爬取到的信息汇总文件的存储位置,因此命令行的调用方式有:
1. “python3 .\爬虫_linux.py -a 区域 -p 存储路径”
2. “python3 .\爬虫_linux.py -a 区域”(默认路径/root/simple_log_analysis/srcdata/)
3. “python3 .\爬虫_linux.py -p 存储路径”(默认爬取区域sh)
4. “python3 .\爬虫_linux.py -h”(帮助信息)

七、后续改进

  该爬虫程序的主要问题:爬取效率太低、对其他错误状态码没有做处理,对于“爬取效率低”的问题,解决方法有:
1. 使用IP池,IP被封后可以立即使用代理IP进行爬取;
2. 使用多线程爬虫框架Scrapy和IP池结合,多线程爬取提高爬取效率;
3. 几次访问后就修改headers,造成不同用户访问的假象(网上提供的方法,不知道实际可行与否);
4. 对于JS渲染可以寻找JS的API接口,进一步解析;
5. 对于异步加载方式,可以切换浏览器的“响应设计模式”为手机访问。
  这篇代码为什么没有采用上述方法呢?代理IP池太贵,而且不稳定,即使使用Scrapy,如果单靠一个IP去访问,很容易被封IP。

爬虫就介绍到这里,下一篇一起来写一个Linux的shell脚本吧。有环境问题或者其他问题可以在下方评论区提问偶,我看到的话会进行回复。

猜你喜欢

转载自blog.csdn.net/qq_40793975/article/details/85006097