初学爬虫,写了一个获取soufun住宅信息的方法。
大致分为三步:
1、获取每个区的小区列表信息
2、获取每个小区的建筑面积、房屋户数等信息
3、处理每个区的住宅信息
一、selenium获取小区列表
首先查看soufun的网页信息,小区列表见https://wuhan.esf.fang.com/housing/,可以看到总共有7000+小区如果直接翻页最多只能翻100页,要获取全部的小区列表,需要先按区进行过滤,然后再翻页,逐页获取小区列表。
browser.find_elements_by_css_selector("div[class = 'qxName']>a") #通过CSS选择器获取每个区域的的链接TAG
totalpage = browser.find_element_by_class_name('txt') #总页数TAG
currentpage = browser.find_element_by_class_name("pageNow") #当前页TAG
nextpagebutton = browser.find_element_by_id("PageControl1_hlk_next") #下一页TAG
celllist = bs4.select("div[class = 'houseList']>div") #获取每一页的小区列表,在houseList下面分为多个DIV
for cell in celllist:
name = cell.select("a[class = 'plotTit']")[0].get_text() #获取小区名称
link = cell.select("a[class = 'plotTit']")[0].get("href") #获取小区链接
locationlist = cell.select("dd>p:nth-child(2)>a")
locationlist[0].get_text() #小区所在区域
locationlist[1].get_text() #小区所在街道
二、request获取小区列表
根据第一步获取的小区链接,获取每个小区的详细信息,详细信息需要在链接基础上加/xiangqing/如:
https://wankehongjun.fang.com/xiangqing/
bs4.select("div[class = 'inforwrap clearfix']>dl>dd")
三、pandas处理获取的数据
1、数据显示
在print查看数据时,经常显示不全,查了一下需要set_option设置显示格式。
pandas.set_option('expand_frame_repr', False) 显示DataFrame是否可以换行True就是可以换行显示。设置成False的时候不允许换行
pandas.set_option('display.max_rows',20) #显示最大行数,超过的行中间为省略号,None表示显示所有的行。
pandas.set_option('display.max_columns', 20) #显示最大列数,超过的列中间为省略号,None表示显示所有的列。
pandas.set_option('display.width', 400) #显示最大宽度,超过就会换行。
2、格式转换
在pandas打开excel文件,打开文件的数据格式是自行判断的,特别对于数据列,如果有一个数据为str则会转换为Object类型,后续计算将无法进行。
所以先要处理异常的数据,也就是常说的数据清洗,然后将格式转换过来。
数据处理参考下面的博文。只有数据清洗之后,且转换为float64或者int64才能正常进行数据处理。
https://www.cnblogs.com/onemorepoint/p/9404753.html
filedata = pandas.read_excel(filename)
filedata.info()
housedata = filedata[filedata['类型'] == '住宅'] #过滤需要处理的数据,只处理类型为住宅的行。
col_to_convert = ["建筑面积","容积率","物业费"] #读取的时候只要有异常数据,就读为了object类型,定义需要转换为numeric的列,逐列转换为numeric,对于异常数据转换为NAN
for col in col_to_convert:
filter_data1.loc[:,col] = pandas.to_numeric(filter_data1.loc[:,col],errors='coerce')
最后通过pivot_table得到数据,建筑面积总共6亿平方米,套数326万。房屋总数和建筑面积对不上,后来看了一下统计数据,住宅总共有5592条数据,其中3267条有建筑面积,4908条有房屋总数,有一部分建筑面积填的异常大导致数据异常。房屋总数较为可信。
0 类型 5592 non-null object
1 区域 5592 non-null object
7 建筑面积 3267 non-null float64
8 房屋总数 4908 non-null float64
建筑面积 | 房屋总数 | |
东湖高新区 | 48174909 | 287740 |
东西湖 | 68552783 | 251768 |
新洲 | 16304000 | 59994 |
武昌 | 69827404 | 546951 |
武汉周边 | 14313069 | 11755 |
汉南 | 9610610 | 13027 |
汉阳 | 55583398 | 286434 |
江夏 | 30744369 | 153415 |
江岸 | 57829587 | 408917 |
江汉 | 26974541 | 259508 |
沌口 | 19425930 | 110050 |
洪山 | 63125071 | 229349 |
硚口 | 23333837 | 207941 |
蔡甸 | 21302382 | 88748 |
青山 | 21726329 | 176268 |
黄陂 | 31805281 | 168196 |
总计 | 578633500 | 3260061 |
在绘制柱状图时,想同时显示具体数值,根据下面文档查看了下没有绘制的接口。在网上找了下,需要pyplot.text一个数一个数的绘在图上。
for xnum, ynum in zip( range(0,16), house['建筑面积']):
pyplot.text(x=xnum-0.03, y= ynum+0.01, s=ynum,ha='center', va= 'bottom',fontsize=11)
https://blog.csdn.net/brucewong0516/article/details/80524442
四、异常处理
1、openxl保存数据时异常
openpyxl.utils.exceptions.IllegalCharacterError
根据https://blog.csdn.net/javajiawei/article/details/97147219介绍是openxl检测cell时如果字符非法会抛出异常。
这里面的非法字符都是八进制,可以到对应的ASCII表中查看,都是不常见的不可显示字符,例如退格,响铃等,在此处被定义为excel中的非法字符。
ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]')
解决上述错误有两种方法
1,既然检测到excel中存在[\000-\010]|[\013-\014]|[\016-\037]这些非法的字符,因此可以将字符串中的非法字符替换掉即可,在重新写入excel即可。如下:
text= ILLEGAL_CHARACTERS_RE.sub(r'', text)
2,使用xlsxwriter
import xlsxwriter
outputData.to_excel(outputExcelFilePath, engine='xlsxwriter')
outputData是一个DataFrame,在 xlsxwriter应该也是替换掉非法字符,省去了我们手工操作。
2、class定位
当一个TAG有多个class属性时,如下DIV有"main current clearfix"三个class,如果用selenium的find_element_by_class_name方法查找,只能选择其中的一个class定位,还有一种方法是通过selenium的CSS定位find_element_by_css_selector可以成功。
如果使用英文点替换,则可以正常定位 find_element_by_class_name("main.current") ,这种方法没有试过。
类似的,在BeautifulSoup中,通过CSS选择器定位如果选择所有的class可以查找成功,如果选择其中一个class属性会查找失败
<div class="main current clearfix">
Selenium:
find_element_by_class_name("main current clearfix") #失败
find_element_by_class_name("clearfix") #成功
find_element_by_css_selector("div[class='main current clearfix']") #成功
BeautifulSoup:
select("div[class='main current clearfix']") #成功
select("div[class='clearfix']") #失败
3、解析乱码问题
通过requests获取网页信息,然后通过BeautifulSoup解码后获取的信息为乱码,查找到了一篇文章介绍requests和BeautifulSoup编解码方法。
https://www.jianshu.com/p/69401b84419e
requests会自动将从服务器端获取到的内容自动转换成unicode, 而beauifulsoup也会将获取到内容自动解码成unicode。既然response.text已经是unicode形式,那么再传递给beautifulsoup,是unicode->unicode之间的直接传递,应该不存在编码转换错误的情况,那么为什么最后print出来的会是乱码呢?
requests和beautifulsoup都会自行猜测原文的编码方式,然后用猜测出来的编码方式进行解码转换成unicode。大多数时候猜测出来的编码都是正确的,但也有猜错的情况,如果猜错了可以指定原文的编码。
1、requests解码
response.text
返回的类型是str
response.content
返回的类型是bytes
,可以通过decode()
方法将bytes
类型转为str
类型
传递给Beautiful Soup的时候两种方法都可以,content需要通过response.content.decode()
的方式获取相应的html页面
- response.text
解码类型:根据HTTP头部对响应的编码做出有根据的推测,推测的文本编码
如何修改编码方式:response.encoding = 'gbk'
- response.content
解码类型:没有指定
如何修改编码方式:response.content.decode('utf8')
requests的编码格式通过print(response.encoding)查询,可以通过response.encoding = 'XXX'指定编码格式。
2、Beautiful Soup解码
任何HTML或XML文档都有自己的编码方式,比如ASCII 或 UTF-8,但是使用Beautiful Soup解析后,文档都被转换成了Unicode,Beautiful Soup用了 编码自动检测 子库来识别当前文档编码并转换成Unicode编码. BeautifulSoup
对象的 .original_encoding
属性记录了自动识别编码的结果,可以通过传入 from_encoding
参数来指定编码方式:
soup.original_encoding #查询解码格式
soup = BeautifulSoup(markup, from_encoding="iso-8859-8") #指定解码格式
通过Beautiful Soup输出文档时,不管输入文档是什么编码方式,输出编码均为UTF-8编码。
如果不想用UTF-8编码输出,可以将编码方式传入 prettify()
方法,注意charset字段已经根据设置改为了latin-1。
print(soup.prettify("latin-1"))
# <html>
# <head>
# <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...
还可以调用 BeautifulSoup
对象或任意节点的 encode()
方法,就像Python的字符串调用 encode()
方法一样。
encoding是从http中的header中的charset字段中提取的编码方式,若header中没有charset字段则默认为ISO-8859-1编码模式,则无法解析中文,这是乱码的原因
apparent_encoding会从网页的内容中分析网页编码的方式,所以apparent_encoding比encoding更加准确。当网页出现乱码时可以把apparent_encoding的编码格式赋值给encoding。
response = requests.get(link, chromehead)
response.encoding = response.apparent_encoding
bs4 = BeautifulSoup(response.text, 'html.parser')
4、pyplot绘图乱码
pyplot在绘制图片时如果有中文时只会显示一个方框,产生中文乱码的原因就是字体的默认设置中并没有中文字体,所以我们只要手动添加中文字体的名称就可以了
在程序开头手动增加如下代码,就可以正常绘制中文。
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']