您的位置:首页 > 其它

要租房又不想自己找怎么办?用Scrapy爬取租房信息

2019-06-29 15:45 37 查看

对于临近毕业的大学生(指自己)而言,怎样寻找便宜又实惠的房源无疑是人人都在关心的问题,今天就来看看怎样用爬虫技术快速抓取房源信息。

运行环境:

Python 3.6.3

Scrapy : 1.5.1

Twisted : 18.9.0

BeautifulSoup :4.6.3

fake_useragent :0.1.11

js2py :0.60

PyMySQL :0.9.3

难度:初级

网站总体难度较低,但联系方式的获取需要运用正则表达式/js引擎提取参数,携带参数和cookie后访问接口,因此对于爬虫初学者而言具有一定难度。

爬取到的数据:

(由于这个网站并不是把所有数据按顺序展示,而是每一页近乎随机的展示60条房源信息,且并不保证每一页数据不重复,因此一次运行无法爬取所有数据。本次运行选取的地区为广州,爬取的数据为2279条,时间为2019-05-01 23:53:13 ~ 2019-05-02 02:56:01。)

网站分析:

本次爬取的网站反爬手段相当单一,几乎所有数据都是静态加载,因此采取最初级的获取列表-获取单页-提取数据-翻页的流程即可。

在几个月前爬取这个网站时,房源联系人的联系方式都直接写在页面源码里,但几个月后再次爬取,发现联系方式变为了ajax动态加载,且ajax接口参数依赖于页面源码,导致速度被大大拖慢,目前没有找到可行的解决方法,希望有思路的朋友指点一二。

首先,创建scrapy项目:

[code]scrapy startproject zufang

在zufang/settings.py里,填写如下内容:

[code]HTTPERROR_ALLOWED_CODES = [404,400,502,503]
DOWNLOAD_TIMEOUT = 5
DOWNLOAD_DELAY = 1.5

● HTTPERROR_ALLOWED_CODES :允许被中间件处理的Response。状态码未在HTTPERROR_ALLOWED_CODES中声明的Response将被Scrapy引擎直接抛弃。

● DOWNLOAD_TIMEOUT :最大超时时间。超过DOWNLOAD_TIMEOUT值的请求将会触发twisted.internet.TimeoutError异常(可被Middleware的process_exception方法捕获)。

● DOWNLOAD_DELAY :下载延迟。Scrapy保证请求间隔不小于DOWNLOAD_DELAY。

在item.py中写入如下内容:

[code]import scrapy

class ZufangItem(scrapy.Item):
house_url = scrapy.Field()      #房屋链接
house_name = scrapy.Field()     #房源名字
price = scrapy.Field()          #房源租金
house_type = scrapy.Field()     #房屋类型
house_area = scrapy.Field()     #房屋面积
rental_method = scrapy.Field()  #出租方式
community = scrapy.Field()      #所在小区
gender = scrapy.Field()         #性别要求
deposit = scrapy.Field()        #押金方式
contact = scrapy.Field()        #联系人
phone = scrapy.Field()          #联系手机
time = scrapy.Field()           #数据获取时间

如果想要了解一个租房信息,那么价格、面积、地址等信息显然是我们关注的部分,保存房屋链接可以让我们看中某个房源时方便的找到房源页面而不需要重新搜索,数据获取时间可以一定程度上体现数据的可用性——毕竟好的房源不会在网站上挂太久(笑)

在settings/spiders中创建文件houseSpider.py,并写入如下内容:

[code]import json,time
from fake_useragent import UserAgent
import bs4
import zufang.items
import scrapy
import js2py

class houseSpider(scrapy.Spider):
name = "mainSpider"
ua = UserAgent()
urls = ["https://gz.zu.anjuke.com/fangyuan/p1/",] #起始url地址

这里要注意的是,文件名houseSpider.py和类名houseSpider都不影响爬虫调用,scrapy引擎只会根据类的name属性来查找、调用爬虫。

静态页面的分析为避免拖延篇幅就不做展开,直接贴代码:

[code]    def start_requests(self):
for url in self.urls :
yield scrapy.Request(url = url,
headers={"user-agent":self.ua.random},
callback=self.parse_house_list,
dont_filter=True)

def parse_house_list(self, response):
#解析网页。提取并请求其中所有的房源信息页。
soup = bs4.BeautifulSoup(response.body.decode("utf8"), "lxml")
info_list = soup.find_all(class_="zu-itemmod")
url_list = [url.a["href"] for url in info_list]
for url in url_list:
yield scrapy.Request(url = url,
headers={"user-agent":self.ua.random},
callback=self.parse_house,
dont_filter=True)

#请求下一页。
next_url = soup.find(class_="aNxt")
if next_url != None :
yield scrapy.Request(url=next_url["href"],
headers={"user-agent": self.ua.random},
callback=self.parse_house_list,
dont_filter=True)

def parse_house(self,response):
cookie = response.headers["Set-Cookie"].decode("utf8")
item = zufang.items.ZufangItem()
soup = bs4.BeautifulSoup(response.body.decode("utf8"),"lxml")
try :
#房屋链接
item["house_url"] = response.url
#房源名字
item["house_name"] = soup.find(class_="house-title").get_text()
#租金
item["price"] = soup.find(class_="price").em.get_text()
#房屋类型
item["house_type"] = soup.find_all(class_="house-info-item l-width")[0].find_all(name="span")[-1].get_text()
#房屋面积
item["house_area"] = soup.find(class_="info-tag no-line").em.get_text()
#出租方式
item["rental_method"] = soup.find(class_="full-line cf").find_all(name="span")[1].get_text()
#所在小区
item["community"] = soup.find_all(class_="house-info-item l-width")[2].a.get_text()
#性别要求
gender = soup.find_all(class_="house-info-item")[-1].find_all("span")[-1].get_text()
if "小区" in gender :
gender = "暂无"
item["gender"] = gender
#押金方式
item["deposit"] = soup.find(class_="full-line cf").find_all("span")[1].get_text()
#联系人
item["contact"] = soup.find(class_="broker-name").get_text()

详细讲一点:联系方式。

在房源页面点击查看电话后,在Network页面搜索对应的号码,能看到一个专门用于获取电话的请求:

查看请求链接:

[code]https://gz.zu.anjuke.com/v3/ajax/getBrokerPhone/?broker_id=5688071&token=0e03375fa6f3c05dd35e34aeb2b52590&prop_id=1320361235&prop_city_id=12&house_type=1&captcha=

忽略为空的captcha,共有五个参数,分别是broker_id,token,prop_id,prop_city_id,house_type。

初学者看到这么多参数可能就晕了,但其实只要在源码里ctrl+f就可以看到:

五个参数都在源码里写着,我们需要做得事情只是把它提取出来而已。

用正则提取显然会比较麻烦,这时候我们可以用到js2py了。使用方式非常简单,获取脚本文本,建立js2py.EvalJs对象,执行js文本后获取返回值,然后用模板替换出url,直接请求即可。

[code]phone_template = "https://gz.zu.anjuke.com/v3/ajax/getBrokerPhone/?broker_id={broker_id}&token={token}&prop_id={prop_id}&prop_city_id={prop_city_id}&house_type={house_type}"

js = soup.find_all(name="script")
context = js2py.EvalJs()
for i in js:
if "brokerPhone" in i.get_text():
context.execute(i.get_text())
data_dict = getattr(context, "__Json4fe")

broker_id = data_dict["getPhoneParam"]["broker_id"]
token = data_dict["token"]
prop_id = data_dict["prop_id"]
prop_city_id = data_dict["prop_city_id"]
house_type = data_dict["house_type"]

yield scrapy.Request(url = self.phone_template.format(
broker_id=broker_id,
token=token,
prop_id = prop_id,
prop_city_id = prop_city_id,
house_type = house_type,
),headers={"user-agent":self.ua.random,"cookie" : cookie},
meta = {"item":item,"url" :response.url,},
callback=self.parse_phone,dont_filter=True)
break

眼尖的读者应该看到了,headers里还额外提交了cookie参数,这个cookie是哪来的呢?

重新打开主页面的请求信息,可以看到Response Headers里set-cookie项:

这里设置的cookie也是获取联系号码的必备参数,实际效果类似于自己动手,用Python实现Pixiv动图下载器(附模拟登录流程)中Pixiv使用的Referer头。

在发送请求前保存cookie即可:

[code]cookie = response.headers["Set-Cookie"].decode("utf8")

获取到联系号码后,将其保存,并同时保存当前时间,最后返回处理完成的item:

[code]    def parse_phone(self,response):
item = response.meta["item"]
js = json.loads(response.text)
item["phone"] = "".join(js["val"].split())
item["time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
yield item

在项目目录下新建run.py文件,并输入以下内容:

[code]from scrapy import cmdline

if __name__ == "__main__" :
cmdline.execute("scrapy crawl mainSpider".split())

运行run.py即可直接启动Scrapy项目,而不用每次都切换到命令行界面了。

运行run.py,就可以在控制台中看到提取完成的数据了:

数据入库:

对于这种小规模的爬虫,数据入库部分非常简单,无需在意性能开销、网络传输开销等,直接在Pipelines中编写SaveDataPipeline即可。利用twisted.enterprise.adbapi实现异步插入:

[code]from twisted.enterprise import adbapi
import zufang.settings as settings

class SaveDataPipeline(object):
#SQL命令模板
insert_template = """
INSERT INTO house_table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);
"""
def __init__(self):
self.dbpool = adbapi.ConnectionPool("pymysql",
host=settings.sqlsetting["HOST"],
port=settings.sqlsetting["PORT"],
db=settings.sqlsetting["DB"],
user=settings.sqlsetting["USER"],
password=settings.sqlsetting["PASSWORD"],
charset=settings.sqlsetting["CHARSET"],
cp_reconnect=True)                        #自动检测失效连接并重连。

def process_item(self, item, spider):
query = self.dbpool.runInteraction(self.insert_data,item)
query.addErrback(self.error_hander,item)
return item

def insert_data(self,cursor,item):
house_id = item["house_url"].split("/")[-1].split("?")[0]
cursor.execute(self.insert_template,[house_id,item["house_url"],item["house_name"],
item["price"],item["house_type"],item["house_area"],
item["rental_method"],item["community"],item["gender"],
item["deposit"],item["contact"],item["phone"],item["time"]])

def error_hander(self,failure,item):
#由于网站数据的展示并非有序且唯一,所以主键重复是可能的。捕获后抛出即可。
if "for key 'PRIMARY'" in str(failure) :
print("主键重复:",item["house_url"].split("/")[-1])

其中数据库配置在settings中读取:

[code]with open("DataBaseSettings.ini","r") as fp:
sqlsetting = json.loads(fp.read())["default"]

DataBaseSettings.ini使用json格式存储配置,格式如下:

[code]{
"default":{
"HOST":"your DB host",
"PORT":port,
"DB":"house_data",
"USER":"your DB username",
"PASSWORD":"your DB password",
"CHARSET":"utf8"
}
}

(个人认为将敏感配置单独编写是非常好的习惯,可以有效避免诸如“某网站管理员将数据库密码明文上传至GITHUB”一类的惨案发生。)

你可以在我的github中下载到本篇文章中的源码:OrsPced

警告

本篇文章原定发表日期为5月2日,因延迟发表,作者对文章中爬虫规则的有效性不做保证(但你可以留言让我改)

本文首发于知乎:https://zhuanlan.zhihu.com/p/71137275

END

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: