0%

爬取58同城厦门二手房数据进行数据分析(一)

最近发现自己的输出能力实在太菜了,写东西经常要憋很久才憋出来,而且写的东西逻辑也不太清楚,所以就想着多写点东西来提升自己的写作能力,同时也加深下自己对于一些问题的理解吧,另外一个原因就是发现自己好多东西都是学完就忘,必须找个平台记录下自己学过的东西!刚好最近有个课程作业,要求爬取58同城上面的厦门二手房信息进行数据分析,就拿这个来小试牛刀吧,预计写两篇,第一篇是爬虫,第二篇是数据分析。

网页分析

首先,点进去首页,是一行行的信息,一页有120条: image.png 点进去链接后是详情页的信息,主要分为两部分信息:房子属性和小区信息 image.png image.png image.png 这里需要注意的是,虽然这些信息都可以直接用xpath获取,但是详情页里面的价格居然用了字体加密!!! image.png 虽然网上有很多这种破解办法,但其实根本不需要那么复杂,回到我们的首页,那边不也有价格嘛,而且,这个价格是没有加密的哦!(58同城程序员故意留的后门???),所以我们爬虫思路大概就分三步走:

  1. 先爬取首页一行行的信息,用xpath获取标题,价格,链接,一条信息用一个字典存储,然后将这条字典并存储在一个列表里面
  2. 第一步爬完后,然后开始遍历我们的列表,进入详情页的链接,把详情页的信息也用xpath一个个扒到我们的字典里,最终返回一个具有完整信息的列表
  3. 使用pandas的Dataframe函数就可以将第二部返回的列表直接转化为一个Dataframe,就能直接导出csv了。 原以为这样就能直接收工了,没想到小看58同城了,还没爬几条就给你来了个人机验证,访问再频繁点就需要登录了,然后就啥信息都爬不到了,因为自己之前爬的都是比较简单的网站,从来就没出现过这个问题,看来还是自己naive了啊,所以前几天也花了一整天的时间来解决这个问题,接下来就看看如何解决这个问题吧!

ip访问频繁问题

一般这种问题的解决方案有两种:

  1. 使用代理ip访问。
  2. 设置程序休眠时间和断点续传机制。

第一种解决方案的话,一般来说是需要自己花钱去买付费ip的,因为大多数免费ip都被人用烂了,考虑到穷和需要花时间去学习怎么构建代理池的问题,于是第一种方案就被我放弃了。 第二种方案算是一种土办法吧,只要我访问足够慢,跟正常用户一样,那么它就不会封我ip(所以这次我爬500条数据都用了两个小时),但是,为了避免还是弹出人机验证的问题,我们需要一个断点续传机制,即当我知道自己的ip被封了的时候,就马上停止访问,并且把已经爬取的数据全部保存下来,然后手动用浏览器去网站上点一下验证码,然后再从之前停止的地方继续开始,这就需要我们给每条信息编个号,当检测到被封ip的时候输出这个编号,手动验证之后继续从这个编号处开始。 说了这么多都是废话,接下来直接上代码吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import requests
from lxml import etree
import pandas as pd
import numpy as np
import time
import random

def parse_url(url):
# 输入链接,返回解析后的html
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36 Edg/86.0.622.63"}
response = requests.get(url=url, headers=headers)
content = response.content.decode('utf-8', 'ignore')
html = etree.HTML(content)
return html

def get_base_info(page_url):
# 获取基础信息
html = parse_url(page_url)
titles = html.xpath(
'//div[@class="list-info"]/h2[@class="title"]/a/text()') # 标题
urls = html.xpath(
'//div[@class="list-info"]/h2[@class="title"]/a/@href') # 链接
total_prices = html.xpath(
'//div[@class="price"]/p[@class="sum"]/b/text()') # 总价
unit_prices = html.xpath(
'//div[@class="price"]/p[@class="unit"]/text()') # 均价
base_infos = [] # 使用一个列表存储所有信息
for title, url, total_price, unit_price in zip(titles, urls, total_prices, unit_prices):
# 将信息写入一个字典中
info = {}
info['title'] = title
if url[0:5] != 'https': # 有的链接不是https开头的,手动加上
url = 'https:'+url
info['url'] = url.split('?')[0] # 删掉链接后面跟的cookie参数什么乱七八糟的东西
info['total_price'] = total_price
info['unit_price'] = unit_price
base_infos.append(info)
return base_infos

def get_extra_info(info):
# 进入详情页获取更多信息
info_url = info['url']
html = parse_url(info_url)
try:
info['location1'] = html.xpath(
'/html/body/div[4]/div[2]/div[2]/ul/li[2]/span[2]/a[1]/text()')[0].strip()
except:
info['location1'] = ''
try:
info['location2'] = html.xpath(
'/html/body/div[4]/div[2]/div[2]/ul/li[2]/span[2]/a[2]/text()')[0].replace('-', '').strip()
except:
info['location2'] = ''
# 获取详情页表格中的信息
info_keys = html.xpath(
'//*[@id="generalSituation"]//span[@class="mr_25 c_999"]/text()')[1:]
info_values = html.xpath(
'//*[@id="generalSituation"]//span[@class="c_000"]')
info_values = [v.text for v in info_values]
for key, value in zip(info_keys, info_values):
info[key] = value

# 获取小区及周边信息
try:
info['xiaoqu_name'] = html.xpath(
'//*[@id="xiaoWrap"]/div/div[2]/h3/a/text()')[0].strip()
except:
info['xiaoqu_name'] = ''
try:
info['xiaoqu_price'] = html.xpath(
'//*[@id="xiaoWrap"]/div/div[2]/ul/li[1]/span[2]/text()')[0]
except:
info['xiaoqu_price'] = ''
try:
info['property_costs'] = html.xpath(
'//*[@id="xiaoWrap"]/div/div[2]/ul/li[3]/span[2]/text()')[0]
except:
info['property_costs'] = ''
try:
info['area_ratio'] = html.xpath(
'//*[@id="xiaoWrap"]/div/div[2]/ul/li[4]/span[2]/text()')[0]
except:
info['area_ratio'] = ''
try:
info['green_ratio'] = html.xpath(
'//*[@id="xiaoWrap"]/div/div[2]/ul/li[5]/span[2]/text()')[0]
except:
info['green_ratio'] = ''
try:
info['车位信息'] = html.xpath(
'//*[@id="xiaoWrap"]/div/div[2]/ul/li[6]/span[2]/text()')[0]
except:
info['车位信息'] = ''
return info

base_url = 'https://xm.58.com/ershoufang/pn'
infos = []
for i in range(1, 7):
time.sleep(random.randint(10, 20)) # 设置休息时间应对反爬
page_url = base_url+str(i)
results = get_base_info(page_url)
infos.extend(results)
print(f'爬取页面{i}的基础信息成功!')

for i in range(1, len(infos)):
time.sleep(random.randint(10, 20))
infos[i] = get_extra_info(infos[i])
if infos[i]['location1'] == '' and infos[i]['xiaoqu_name'] == '': # 如果这两个值都为空值,说明开始人机验证了
print(f'爬取第{i}条信息失败,请进行人机验证! ')
print(infos[i]['url'])
# 及时保存数据
data = pd.DataFrame(infos)
data.to_csv('data.csv')
break
else:
print("爬取第{}条信息成功:{}".format(i, infos[i]['title']))

data = pd.DataFrame(infos)
data.to_csv('data.csv') # 导出到csv文件

总结

这次爬虫主要学了三个东西吧:

  1. 字典是个很有用的数据类型,不仅存储效率高,而且将多个字典放在列表里可以直接转化为pandas的Dataframe,还就能直接导出,不需要读写文件那么麻烦。
  2. xpath的异常处理机制很重要。有些信息可能会在某个页面上神奇的消失了,所以最好事先做好异常处理机制,爬不到就置为空值,不然程序一报错之前爬的数据全没掉了。
  3. 断点续传机制也很重要。天知道网站会在什么时候给你跳出验证码,所以最好有断点续传机制,防止你要从头来过。

好了,第一篇技术性的文章就这样水成了,第二篇过几天有空写。

欢迎关注我的其它发布渠道