Splash-Scrapy爬取Airbnb数据
引言
很久之前(2017年11月)写过一个用Python爬虫去Airbnb网站上抓取民宿信息,当时就觉得这个网站的信息实在是隐藏得太深了。结果最近发现,现在又不能了:Airbnb的网页用了大量的JS来动态生成,直接解析网页,拿不到什么有用的信息。
道高一尺,魔高一丈。为了解决动态网页的爬取,也有许多方法,例如我们前面针对Wunderground用Selenium解析。由于Airbnb需要爬取的网页数目比较多,为了提高稳定性,我们采用Scrapy
+Splash
的方法来解析动态网页,在这个过程中也发现,如果可以找到网页请求的API,用API实际上更加方便。
Scrapy和Splash库介绍
Scrapy的安装和使用
之前为了获取一些网上的信息,我们可能会自己写一些网站爬取和网页解析的小工具。而scrapy
库则是一个流行的用于爬取网站数据,提取结构性数据的应用框架,可以大大提高我们编写爬虫的效率。scrapy
的安装十分简单,如果是采用conda的方式,安装命令为:
1 | conda install -c conda-forge scrapy |
安装好以后,创建一个Scrapy的工程,创建的工程中会包含如下内容:
1 | ./ |
其中各个模块具体的功能,大家可以阅读Scrapy的的官方文档,也能够在网上找到很多的资料。
Splash的安装和使用
Splash是一个带有HTTP API的轻量级Web浏览器,基于Python 3使用Twisted和QT5实现异步服务,从而能够利用QT主循环充分发挥wekit的并发优势。
Splash能够并行处理多个网页,返回网页的渲染代码或者屏幕截图。为了加快渲染速度,还可以尽职下载图像或者使用Adblock进行过滤。
scrapy与Splash配合使用过程中,首先要配置Splash的环境,包括以下几步:
-
安装Docker,(Windows 10支持直接安装Docker);
-
获得splash的Docker镜像,
docker pull scrapinghub/splash
-
运行Docker
docker run -it -p 8050:8050 scrapinghub/splash
这样,我们就有了一个本地运行的splash服务器,然后还需要安装scrapy-splash库实现scrapy对Spalsh HTTP API的调用。
Airbnb结构与内容分析
我们在爬数据的时候,首先还是要明确是为了什么,然后需要什么,再看看网站能提供什么?具体到Airbnb而言,上面的数据种类繁杂,无论是空间尺度、时间尺度还是面板尺度,都可以拿来作分析,既有量化的打分、也有定性的评价,既有连续数值的价格,也有离散的分类变量,所以初步的了解是非常必要的。总体来看,我觉得可以分为以下四个部分:
根据位置构造Start URL
民宿是个典型的基于地理位置的服务(LBS),最好我们能有一个地理编码的列表:如果我们想遍历中国所有的省市,遍历列表就可以,具体而言我们采用网上提供的中国全国的5级行政区划(省、市、县、镇、村)(https://github.com/adyliu/china_area ) 。
在Aribnb的爬虫中,我们主要用这个地理编码列表来构建scrapoy
中的start_requests()
方法或者是start_urls
。我们之所以要自己构造这个start_requests
主要是出于简化爬虫的复杂性:
- 如果我们采用深度优先搜索的策略,需要控制爬虫的搜索深度,如果了解内容的层次,可以设定一个比较好的深度;
- Airbnb的搜索,一次最多提供的搜索结果,显然很多城市不止这个数量,那就需要用一个合理的方式来遍历(不遗漏的同时尽可能减少重复)。
首先对网上下载的行政区划进行简单的处理,方便下一步使用:
这样,我们得到的start_url形式就是:
http://zh.airbnb.com/s/[搜索字段]/homes?[arguments]
其中,[搜索字段]就是我们不同的行政区划组合,[arguments]是查询用的参数,例如(以字典形式):
1 | _query_parts = { |
在发送请求时,我们使用SplashRequest
代替scrapy.request()
,对于不同的Splash请求,scrapy-splash返回不同的Response子类, 如下所示,args
为请求的参数,self.parse
为获得响应后的回调函数,也就是我们下一步对查询结果的解析:
1 | def start_requests(self): |
解析查询结果
XPath解析民宿URL
利用Chrome的开发者工具,我们可以看到在一页里面,Airbnb将搜索结果保存在了itemList
里面,每一个民宿就是一个itemListElement
。所以相对而言,还是比较容易解析的。
对于简单的网页或者匹配,用正则表达式就可以解决,在这里我们用另外一种方法:XPath,即XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的计算机语言,所以很适合用作查询。我们可以发现,对于同一个内容可以采用不同的查询语句,分析网页结构,我们这里采用的具体如下:
1 | def _parse_listing_results_page(self, response): |
解析结果列表的数目
可以看到,一次查询往往有很多页,我们需要逐页去提取,所以就需要逐页获取列表的结果,类似上面的做法,我们查看网页的代码可以发现,页码是以列表的形式存储的,列表的倒数第二项就是最后页码的数目。
此外,还需要注意原来Airbnb是用section_offset=[页码]
作为查询参数,现在改成了items_offset=[页码]*[每页数目]
作为查询参数,保不齐什么时候就会又变了:
解析民宿信息(接近失败!)
接下来,就到了最为重要的一步——爬取民宿的各类信息。然而不幸的是,即使我们用splash加载动态网页,我们能得到的信息也只是页面上显示的内容,而且还需要进一步处理。而以前的时候,可以直接获取一个JSON对象,里面含有几乎所有的信息,这么一比较,现在这种做法就显得非常不划算了。
所以说,如果只能获取页面内容,及时不是完全失败,也是近乎失败了。
调用AirbnbAPI获取数据
山重水复疑无路,柳暗花明又一村。既然页面的内容是动态生成的,而住房信息又不是算出来的,那一定会向服务器请求。能不能从这里面发现点什么?我们用Chrome开发者工具查看一下页面发出的请求,果然有一些端倪:
我们用在Python中调用一下这个API,看一下结果:
1 | import requests |
dict_keys(['listing_amenities', 'root_amenity_sections', 'see_all_amenity_sections', 'additional_house_rules', 'bathroom_label', 'bed_label', 'bedroom_label', 'guest_label', 'highlights', 'id', 'listing_expectations', 'listing_rooms', 'name', 'p3_subject', 'p3_summary_address', 'p3_summary_title', 'person_capacity', 'photos', 'primary_host', 'room_and_property_type', 'room_type_category', 'sectioned_description', 'star_rating', 'tier_id', 'user', 'book_it_url', 'calendar_last_updated_at', 'guest_controls', 'min_nights', 'native_currency', 'collection_kicker', 'show_policy_details', 'additional_hosts', 'applicable_disaster', 'hometour_rooms', 'hometour_sections', 'alternate_sectioned_description_for_p3', 'initial_description_author_type', 'localized_check_in_time_window', 'localized_check_out_time', 'localized_city', 'localized_listing_expectations', 'localized_room_type', 'city_guidebook', 'country_code', 'display_exact_location', 'host_guidebook', 'lat', 'lng', 'location_title', 'neighborhood_id', 'p3_event_data_logging', 'paid_growth_remarketing_listing_ids', 'commercial_host_info', 'flag_info', 'license', 'p3_listing_flag_options', 'p3_review_flag_options', 'requires_license', 'should_hide_action_buttons', 'should_show_business_details', 'show_edit_mode', 'support_cleaner_living_wage', 'p3_display_review_summary', 'review_details_interface', 'sorted_reviews', 'visible_review_count', 'cover_photo_primary', 'host_interaction', 'host_quote', 'layout', 'nearby_airport_distance_descriptions', 'property_type_in_city', 'render_tier_id', 'select_listing_tenets', 'other_property_types', 'p3_neighborhood_breadcrumb_details', 'p3_seo_breadcrumb_details', 'p3_seo_property_search_url', 'seo_features', 'share_links', 'education_module', 'collection_promotion', 'reviews_order', 'cover_photo_vertical', 'is_hotel', 'show_review_tag', 'accessibility_module', 'is_representative_inventory', 'highlights_impression_id', 'point_of_interests', 'has_essentials_amenity', 'china_points_of_interest', 'reservation_status', 'visibility', 'categorized_preview_amenities', 'section_erf_configs', 'china_points_of_interest_matcha', 'security_deposit_details', 'page_view_type', 'preview_tags', 'see_all_hometour_sections', 'summary_section', 'education_modules', 'enable_highlights_voting', 'amenity_section', 'host_info_module', 'hometour_module', 'hero_module', 'summary_module', 'new_user_education_module', 'panorama', 'p3_impression_id', 'error_status', 'debug_output'])
我们发现,这个信息可比页面显示的内容多多了,这才是我们真正需要的~
等等!如果房间的信息可以通过API获取,搜索信息能不能呢?我们不妨试一下,果然不出所料:
1 | url = ('https://zh.airbnb.com/api/v2/explore_tabs?_format=for_explore_search_web&client_session_id=2750fd8f-8607-48dc-b625-4b0669ba0a23' |
dict_keys(['backend_search_id', 'display_type', 'experiments_metadata', 'result_type', 'search_session_id', 'section_id', 'section_type_uid', 'title', 'see_all_info', 'is_paginated', 'bankai_section_id', 'refinements', 'listings', 'review_items', 'breadcrumbs', 'section_metadata', 'nearby_locations', 'localized_listing_count'])
果然我们用这个请求,可以请求到页面的主要内容,尤其是包含房间信息的listings
,而且如果我们是第一次发起请求,然后向下滚动查看页面,页面会继续发出请求,动态加载房间信息:
对比可以发现,调用API比我们用Splash加载页面、然后用XPath去解析内容方便了许多。但是,这种好事并不是总有的。此外,请求API需要用到key
,这个还是需要在一开始要请求一下网页才能获得的。
小结
我们用Splash-Scrapy实现了对Airbnb民宿信息的爬取,其中介绍了用Splash加载动态网页和用Airbnb的API直接读取信息。实际上,这还不是一个完整的爬虫或者说Scrapy项目,还有至少一下工作需要完成:
- 使用
Item
类封装抓取的数据; - 使用
Pipeline
类保存数据; - 修改
settings.py
对Scrapy进行设置 - ……
感兴趣的可以查找相关资料。
接下来,可能会对Airbnb的数据进行分析,待续~