自己动手搭建一个简单的基于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脚本吧。有环境问题或者其他问题可以在下方评论区提问偶,我看到的话会进行回复。