用户头像

古才

2022-12-25

233

论如何像个程序员一样找房子 - 知乎

最近一直在找MUJI✖️UR的房子,他家的房源都是挂在网上的,没事多刷刷说不定就可以刷到自己心仪的房子,但程序员一定是不能屑于这种办法的,程序员应该想,可不可以让计算机来帮我们刷,刷到的话就发信息告诉我们

目前市面上主要有两种解决办法,一种是复刻人类看网页的动作:打开浏览器,输入网址,点击按钮,找到对应内容爬下来,这种办法的好处是比较直觉,而且也不易被服务器察觉,缺点就是比较慢,显得笨重,有一个叫UIPath的软件,针对个人用户也是免费的,用它拖拖鼠标就可快速实现这样的神奇操作,另外经常被用来做自动化测试的SeleniumHeadless Chrome也可以归在这一类

另一种办法就比较简单粗暴,直接向服务器发送请求,然后解析服务器传回的HTML文本,它的好处是较之第一种来说,单刀直入,多快好省,但毛病也有,有的时候可能会拿不到想要的数据,这种情况下可能不得不换成第一种方法才行

这次选第二种做法,和把大象装进冰箱的步骤一样,分成三步:

  1. 发送请求,取到数据
  2. 解析数据
  3. 轮寻服务器,如果发现新数据,就向微信发送消息

不少语言都可以轻松做到这些,比如C#,Python,不过这次用JavaScript试着做一做(因为其他的不会。。),所以事先需要一个nodejs环境,IDE就选VSCode

打开冰箱门

发送HTTP请求的库有很多,Request,XMLHttpRequest啥的好像之前的文章有介绍过,所以这次就用fetch,反正大同小异,用哪个都没差,在node环境下用node-fetch

在终端里打开项目目录,输入

$ npm install node-fetch --save

就安装好了,接着建一个js文件,在里面写

const fetch = require('node-fetch');test();async function test() { const url = 'https://www.ur-net.go.jp/chintai/muji/'; const res = await fetch(url); const body = await res.text(); console.log(body);}

运行一下,如果在控制台打印粗一坨翔一样的HTML文本,就说明成功了,简单解释一下:

  • 第一行导入fetch这个包,然后用fetch向服务器发起GET请求,返回的结果转换成文本后打印在控制台上
  • 用async/await的语法,主要是为了解决异步的问题,用async/await可以把异步写的和同步似的,就省去了恼人的回调函数,写法就是在需要停一停的地方写个await,这样之后的代码就会等待,直到await那行完成,不过await必须要现在一个带async的函数里面
  • 这里的URL就是MUJI✖️UR的网页,房子数量不多,全都在这一页上

把大象放进去

接下来就是解析HTML了,只需要房子信息的部分,其余的都不需要,那怎么才能提取出来呢,其实也有相应的库——cheerio,它的用法和jQuery很像,也是用CSS选择器然后对DOM进行操作,如果之前有用过jQuery,那这个一定会觉得很眼熟。。

安装cheerio,在终端输入

$ npm install cheerio --save

然后打开Chrome Devtools,先分析一哈,找到房源信息的地方,他被写在一个class是box mix的div里,房子的名字在其中的h5里,地址呀房租呀空房数量这些被写在了其中的另一个div.dtbc2里

分析好结构,就可以代码了,继续之前的,这次不要直接打印body,而是把它加载进cheerio里,这个$就已经包含了所有的页面信息,在里面找到class是box mix的div,这里房子不止一个,所以这样的div也不止一个,用each去遍历打印他

const cheerio = require('cheerio');//。。。//console.log(body);const $ = await cheerio.load(body);$('div.box.mix').each((i, ele) => { console.log($(ele).text());});

这时候就会出现一个问题。。只能取到项目名,实际的数据却没有显示,明明在画面上都有显示,为啥打印不到终端呢。。

原因在于有的网站是,向服务器发送一个请求,他会把数据写到HTML里一气发回来,但也有的网站不是,他的数据可能是异步调用,这种情况下解析HTML是没用的,那些动态数据并没有放在这里,一般是调用了其他的API,打开Chrome Devlop Tools,在Network标签下,勾选XHR,就会看到想要的数据原来在这里。。

这里主要传回来两个,第一个POST请求通过43个房子的ID取回了43件房子的信息,这些大概都是一些固定不变的信息,比如房子的地址,离车站多远之类的,暂且不做考虑,重点是看下一个GET请求

在这个请求中,返回了14条数据,基本和空房的数量相当(当前空房有13个显示,不知道一个差在哪了。。)和页面上的数据对比一下,大概也可以猜测到MIN就是每个月的房租钱,COUNT就是当前的空房数,至于前两个SCD和DCD,他们是表示区域和房子的ID,他俩的组合可以唯一确定某一个房源

找到了空房信息的所在地,就对他做一次请求,这次取回的不是HTML,而是JSON格式的,把ID拼接好,连同房租和空房数量一起放进一个数组里

getList();async function getList() { var list = []; const url = 'https://chintai.sumai.ur-net.go.jp/kanto/muji_json_index.ashx'; const res = await fetch(url); const json = await res.json(); for(var i=0; i<json.length; i++) { var house = json[i]; const id = house.SCD + '_' + house.DCD; const bk = { 'id' : id, 'count' : house.COUNT, 'price' : house.MIN            }; await list.push(bk);    } console.log(list); console.log(list.length); return list;};

运行之后如下,说明取到了所有空房子信息,现在的问题是不知道哪个ID对应哪个名字的房子

[ { id: '20_135', count: '3', price: '90200' },
{ id: '30_262', count: '1', price: '60300' },
{ id: '30_263', count: '1', price: '67600' },
{ id: '30_336', count: '1', price: '86800' },
{ id: '70_079', count: '1', price: '95800' },
{ id: '70_089', count: '1', price: '72500' },
{ id: '70_093', count: '2', price: '84700' },
{ id: '80_104', count: '10', price: '65600' },
{ id: '80_121', count: '4', price: '71000' },
{ id: '80_170', count: '1', price: '76500' },
{ id: '80_241', count: '12', price: '54300' },
{ id: '90_088', count: '1', price: '44400' },
{ id: '90_106', count: '2', price: '59900' },
{ id: '90_128', count: '2', price: '53200' } ]
14

之前解析HTML的功夫没有白费,因为HTML里有ID和房间名字的对应关系,对之前的代码稍作修改,把取到的对应关系放进字典里

getMap();async function getMap() { var myMap = new Map(); const url = 'https://www.ur-net.go.jp/chintai/muji/'; const res = await fetch(url); const body = await res.text(); const $ = await cheerio.load(body); $('a', $('h5', '.blue')).each((i, ele) => { var id = $(ele).attr('href'); id = id.substring(0, id.indexOf('.')); const name = $(ele).text(); myMap.set(id, name);    }); console.log(myMap); return myMap;};

这次只选择关东地区,关东地区是蓝色(blue),要取到在blue中的h5中的a,就用两次嵌套,他的href属性指向了房子的详细页面,截去.html的部分剩下的就是房子的ID,房子的名称是直接写在内容里的,以ID为键,名字为值,做一个字典,运行后是这样的

Map {
'30_336' => 'ハイタウン塩浜',
'20_167' => '町田山崎',
'20_207' => '多摩ニュータウン 永山(永山二丁目)',
'20_434' => '多摩ニュータウン ベルコリーヌ南大沢',
'30_265' => '高洲第二',
'20_187' => '百草',
'30_283' => '芝山',
'20_347' => '光が丘パークタウン ゆりの木通り北',
'50_200' => '若葉台',
'30_262' => '牧の原',
'20_135' => '国立富士見台',
'20_364' => '品川八潮パークタウン 潮路南第一ハイツ',
'20_225' => '高島平',
'30_258' => '真砂第一・真砂第二',
'50_105' => '武里' }

然后在上一步取到的空房数据中通过ID去找字典中的对应值,这样就可以知道空房的名字是什么了

getHouse();async function getHouse() { const names = await getMap(); const list = await getList(names); return list;}async function getList(names) { var list = []; const url = 'https://chintai.sumai.ur-net.go.jp/kanto/muji_json_index.ashx'; const res = await fetch(url); const json = await res.json(); for(var i=0; i<json.length; i++) { var house = json[i]; const id = house.SCD + '_' + house.DCD; if(names.has(id)) { const bk = { 'id' : id, 'name' : names.get(id), 'count' : house.COUNT, 'price' : house.MIN            }; await list.push(bk);        }    } console.log(list); return list;};

运行后的结果如下,我们取到了三个空房数据,和页面上所显示的对比一下,结果是一样的

[ { id: '20_135', name: '国立富士見台', count: '3', price: '90200' },
{ id: '30_262', name: '牧の原', count: '1', price: '60300' },
{ id: '30_336', name: 'ハイタウン塩浜', count: '1', price: '86800' } ]

把冰箱门关上

到目前为止,已经可以成功取到了空房信息,剩下的任务就很简单,只需要每隔一段时间向服务器发送一次请求,然后看数据有没有变化,如果有就通知我们

一个最直观的做法就是写个死循环,不停的调用getHouse()这个方法,但中间要记得留好睡眠时间,不然不停的发送请求会被看成是网络攻击。。

doLoop();async function doLoop() { var house0 = await getHouse(); console.log(house0); while(true) { await sleep(6000); var house = await getHouse(); console.log(house); if(JSON.stringify(house0) != JSON.stringify(house)) { console.log("FIND DIFF!!"); house0 = house;        }    }}function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}

这个JSON.stringify会把JSON对象转化成字符串来方便做比较,如果和之前的不同,则打印FIND DIFF,并更新比较对象,JavaScript里虽然没有现成的sleep方法,不过可以封装成如上的样子,这里6000表示每隔6秒查看一次

这些个数据通常都是不变的,为了测试代码对不对,需要手动设置断点,然后人为的修改一下之前的信息(比如把名称"牧の原"改成"牧の原2"。。)看是否能打印FIND DIFF”

Array(3) [Object, Object, Object]scrap.js:8
[[StableObjectId]]:6
length:3
__proto__:Array(0) [, …]
0:Object {id: "20_135", name: "国立富士見台", count: "3", …}
1:Object {id: "30_262", name: "牧の原2", count: "1", …}
2:Object {id: "30_336", name: "ハイタウン塩浜", count: "1", …}
Array(3) [Object, Object, Object]scrap.js:12
[[StableObjectId]]:16
length:3
__proto__:Array(0) [, …]
0:Object {id: "20_135", name: "国立富士見台", count: "3", …}
1:Object {id: "30_262", name: "牧の原", count: "1", …}
2:Object {id: "30_336", name: "ハイタウン塩浜", count: "1", …}
FIND DIFF!!

成功打印,证明测试通过,最后只需要把打印到控制台改成发送到微信即可,发送的办法也有很多,这里简单起见,用Server酱来实现,Server酱是一个用来给微信发送请求的轻量服务,选择他没有别的理由,只是因为很简单,只需要按照步骤说明用GitHub账号登录,获取KEY,然后添加微信关注就可以了,发送时只需要一行GET请求就可以了

将打印到控制台的代码修改如下即可

const url = 'https://sc.ftqq.com/这里是KEY.send?text=find new house!';const res = await fetch(url);

这样就大功告成了,启动JS后,开始轮寻服务器,如果出现了差分,便会自动发消息到微信通知,是不是很赞。。

参考资料

 
 
 
 
 

 



文章来源:https://zhuanlan.zhihu.com/p/88776692

声明:本文通过RPA之家机器人自动转载,如有侵权请联系service@rpazj.com删除

1条评论

用户头像
提交评论
王晓波: RPA之家(www.rpazj.com)—中国最大的RPA服务平台。提供RPA培训、咨询、实施、机器人定制购买、技术交流、求职招聘、外包兼职等专业服务。业务咨询请加微信18925203701交流。
回复 2022-12-28
RPA之家banner图