从爬虫到机器学习预测,我是如何一步一步做到的?
作者:xiaoyu
微信公众号:Python数据科学
知乎:python数据分析师
前情回顾
前一段时间与大家分享了北京二手房房价分析的实战项目,分为分析和建模两篇。文章发出后,得到了大家的肯定和支持,在此表示感谢。
除了数据分析,好多朋友也对爬虫特别感兴趣,想知道爬虫部分是如何实现的。本篇将分享这个项目的爬虫部分,算是数据分析的一个 前传篇。
爬虫前的思考
爬虫部分主要是通过爬取链x和安x客来获取二手房住房信息,因为考虑到不同网站的房源信息可以互补,所以选择了两个网站。
爬取目标是北京二手房,仅针对一个城市而言,数据量并不大。所以直接采用Scrapy
来完成爬取工作,然后将数据存储在csv格式的文件中。最终爬取结果是这样的,链x的爬虫爬取了 30000+
条数据,安x客的爬虫爬取了 3000+
条数据。不得不说链x的房源相对来讲还是比较全的。
scrapy爬取链x
写一个爬虫最开始当然要想清楚需要获取什么样的数据了。本次项目对与二手房相关的数据都比较感兴趣,可以自然的想到,每个房源链接的具体详细信息是最全的。但考虑到爬虫深度影响整体爬虫效率问题,并且房源列表中数据已经能够满足基本的要求,并没有必要对每个详细链接进行深入的爬取,因此最终选择爬取房源列表。以下是房源列表(部分截图)中的房源信息:
确定以上爬取内容后,就开始爬虫部分的工作。首先在item.py
文件中定义一个子类,该子类继承了父类scrapy.Item
,然后在子类中用scrapy.Field()
定义以上信息的字段。如下代码,将所有需要的字段信息都设置好。
import scrapy class LianjiaSpiderItem(scrapy.Item): # define the fields for your item here like: Id = scrapy.Field() Region = scrapy.Field() Garden = scrapy.Field() Layout = scrapy.Field() Size = scrapy.Field() Direction = scrapy.Field() Renovation = scrapy.Field() Elevator = scrapy.Field() Floor = scrapy.Field() Year = scrapy.Field() Price = scrapy.Field() District = scrapy.Field() pass
在spider文件夹下的爬取文件(自定义)中导入所需库,如下代码:
- json:json格式的转换;
- scrapy:scrapy库;
- logging:日志;
- BeautifulSoup:使用bs4提取网页信息;
- table:settings中自设的一个字典;
- LianjiaSpiderItem:字段Field;
# -*- coding:utf-8 -*- import json import scrapy import logging from bs4 import BeautifulSoup from lianjia_spider.settings import table from lianjia_spider.items import LianjiaSpiderItem
下面进入关键部分,即爬虫部分。这部分主要需要自己做的就是如何解析,而对于爬虫是如何爬取的我们不用关心,因为它是框架已经在底层完成调度和爬取的实现,我们只要简单调用即可。
具体详细框架结构可参见:Python爬虫之Scrapy学习(基础篇)
爬虫解析部分,是在继承scrapy.Spider
父类的子类LianjiaSpider
中完成的。子类中设有三个函数,并通过callback
回调逐层实现解析功能,这三个函数是:
- start_requests:覆盖父类中原有函数,爬取初始url并存入消息队列中;
- page_navigate:解析初始url页面,循环爬取各初始url页面下的所有页码链接;
- parse:爬取每个页码下的所有详细房源链接,提取相应的字段信息,并储存至items中;
下面是三个函数的功能描述,以及代码实现。
start_requests
任何爬虫都需要有初始url,然后由初始url继续深入爬取进一步的url,直到爬取到所需数据。由于链家二手房url的特征是,由一个基础url和各大区拼音拼接组成,因此在start_requests
函数中定义了base_url
的基础url,和需要拼接的北京各大区的拼音列表。
然后由这些拼接的各大区url作为所有的初始url链接,并由scrapy.Request
方法对每个链接发出异步请求,代码如下:
class LianjiaSpider(scrapy.Spider): name = 'lianjia' base_url = 'https://bj.lianjia.com/ershoufang/' def start_requests(self): district = ['dongcheng', 'xicheng', 'chaoyang', 'haidian', 'fengtai', 'shijingshan', 'tongzhou', 'changping', 'daxing', 'yizhuangkaifaqu', 'shunyi', 'fangshan', 'mentougou', 'pinggu', 'huairou', 'miyun', 'yanqing', 'yanjiao', 'xianghe'] for elem in district: region_url = self.base_url + elem yield scrapy.Request(url=region_url, callback=self.page_navigate)
page_navigate
对每个大区url发出异步请求后,我们需要对各大区内的所有房源列表url进行进一步的爬取,而为了能够顺利的将全部内容爬取,我们就要解决页码循环的问题。在page_navigate
函数中,使用BeautifulSoup
解析html,提取页面中的pages数据。
BeautifulSoup的具体使用方法参见:Python爬虫之BeautifulSoup解析之路
爬取获得的pages
数据是json
字符串,所以需要使用json.loads
将其转换为字典格式,然后得到max_number
。最后通过for循环不断发送每个页码url的链接完成异步请求,并使用callback调用进入下一步的函数中,代码如下:
def page_navigate(self, response): soup = BeautifulSoup(response.body, "html.parser") try: pages = soup.find_all("div", class_="house-lst-page-box")[0] if pages: dict_number = json.loads(pages["page-data"]) max_number = dict_number['totalPage'] for num in range(1, max_number + 1): url = response.url + 'pg' + str(num) + '/' yield scrapy.Request(url=url, callback=self.parse) except: logging.info("*******该地区没有二手房信息********")
parse
parse函数中,首先通过BeautifulSoup解析每个页码下的所有房源列表信息,得到house_info_list
。链x房源列表中没有所在大区信息,但是房源所在区域对于后续数据分析是很重要的,而仅通过页面解析我们没办法获取。为了获得这个字段该如何实现呢?
我们可以通过response.url
来判断,因为url正好是我们开始用所在区域拼接而成的,我们构造url的时候已经包含了大区信息。那么简单的通过辨识url中的大区拼音,就可以解决该问题了。然后使用字典table将对应的中文所在区名映射到Region字段中。
接下来开始对房源列表 house_info_list
中的每个房源信息info
进行解析。根据链x的页面结构,可以看到,每个info下有三个不同位置的信息组,可通过class_
参数进行定位。这三个位置信息分别是house_info,position_info,price_info,每组位置下包含相关字段信息。
- house_info:如图包含Garden,Size,Layout,Direction,Renovation,Elevator房屋构造等字段信息;
- position_info:如图包含Floor,Year,District等位置年限字段信息;
- price_info:如图包含Total_price,price等字段信息;
这里说的位置不同是在前端html页面中的标签位置不同。
具体操作方法参见下面代码:
def parse(self, response): item = LianjiaSpiderItem() soup = BeautifulSoup(response.body, "html.parser") #获取到所有子列表的信息 house_info_list = soup.find_all(name="li", class_="clear") # 通过url辨认所在区域 url = response.url url = url.split('/') item['Region'] = table[url[-3]] for info in house_info_list: item['Id'] = info.a['data-housecode'] house_info = info.find_all(name="div", class_="houseInfo")[0] house_info = house_info.get_text() house_info = house_info.replace(' ', '') house_info = house_info.split('/') # print(house_info) try: item['Garden'] = house_info[0] item['Layout'] = house_info[1] item['Size'] = house_info[2] item['Direction'] = house_info[3] item['Renovation'] = house_info[4] if len(house_info) > 5: item['Elevator'] = house_info[5] else: item['Elevator'] = '' except: print("数据保存错误") position_info = info.find_all(name='div', class_='positionInfo')[0] position_info = position_info.get_text() position_info = position_info.replace(' ', '') position_info = position_info.split('/') # print(position_info) try: item['Floor'] = position_info[0] item['Year'] = position_info[1] item['District'] = position_info[2] except: print("数据保存错误") price_info = info.find_all("div", class_="totalPrice")[0] item['Price'] = price_info.span.get_text() yield item
对于链x的爬取,没用xpath的原因是提取一些标签实在不是很方便(只是针对于链x),因此博主采用了beautifulSoup。
scrapy爬取安x客
这部分之前就有分享过,可以参见:Scrapy爬取二手房信息+可视化数据分析
以下是核心的爬虫部分,与链x爬取部分的思想一致,不同的是使用了xpath
进行解析和ItemLoader
对item加载储存。
# -*- coding:utf-8 -*- import scrapy from scrapy.loader import ItemLoader from anjuke.items import AnjukeItem class AnjukeSpider(scrapy.Spider): name = 'anjuke' custom_settings = { 'REDIRECT_ENABLED': False } start_urls = ['https://beijing.anjuke.com/sale/'] def start_requests(self): base_url = 'https://beijing.anjuke.com/sale/' for page in range(1, 51): url = base_url + 'p' + str(page) + '/' yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): num = len(response.xpath('//*[@id="houselist-mod-new"]/li').extract()) house_info = response.xpath('//*[@id="houselist-mod-new"]') print(house_info) for i in range(1, num + 1): l = ItemLoader(AnjukeItem(), house_info) l.add_xpath('Layout', '//li[{}]/div[2]/div[2]/span[1]/text()'.format(i)) l.add_xpath('Size', '//li[{}]/div[2]/div[2]/span[2]/text()'.format(i)) l.add_xpath('Floor', '//li[{}]/div[2]/div[2]/span[3]/text()'.format(i)) l.add_xpath('Year', '//li[{}]/div[2]/div[2]/span[4]/text()'.format(i)) l.add_xpath('Garden', '//li[{}]/div[2]/div[3]/span/text()'.format(i)) l.add_xpath('Region', '//li[{}]/div[2]/div[3]/span/text()'.format(i)) l.add_xpath('Price', '//li[{}]/div[3]/span[1]/strong/text()'.format(i)) yield l.load_item()
安x客的反爬比较严重,如果不使用代理ip池,速度过快非常容易挂掉。而链x的反爬相对没那么严格,速度可以很快。
总结
以上是对本项目爬虫部分核心内容的分享,至此这个项目完成了从爬虫到数据分析,再到数据挖掘预测的 "三部曲"完整过程。虽然这个项目比较简单,仍有很多地方需要完善,但是希望通过这个项目能让大家对整个过程有个很好的认识和了解。
关注微信公众号:Python数据科学,发现更多精彩内容。