领域驱动设计在马蜂窝优惠中心重构中的实践

前言

正如领域驱动设计之父 Eric Evans 所著一书的书名所述,领域驱动设计(Domain Driven Design)是一种软件核心复杂性应对之道。

在我们解决现实业务问题时,会面对非常复杂的业务逻辑。即使是同一个事物,在多个子业务单元下代表的意思也是不完全一样的。比如「商品」这个词,在商品详情页语境中,是指「商品基本信息」;在下单页语境中,是指「购买项」;而在物流页面语境中,又变成了「被运送的货物」。

DDD 的核心思想就是让正确的领域模型发挥作用。所谓「术业有专攻」,DDD 指导软件开发人员将不同的子业务单元划分为不同的子领域,在各个子领域内部分别对事物进行建模,来应对业务的复杂性。

Part 1

重构优惠中心的背景

我们在实际的开发过程中都遇到过这种情况,最初因为业务逻辑比较单一,为了快速实现功能, 以及对成本、风险等因素的综合考虑,我们会为业务统一创建一个大的模型,各个模块都使用这同一个模型。但随着业务的发展,各子领域的逻辑越来越复杂,对这个大模型的修改就会变成一种灾难,有时明明是要改一个 A 子领域的逻辑,却莫名其妙影响到了 B 或者 C 子领域的线上功能。

优惠中心就是一个例子。优惠中心主要负责马蜂窝各业务线商品的优惠活动管理,以及计算不同用户的优惠结果。「商品管理」和「优惠管理」作为两个不同的业务单元,在初期被设计为共用一个商品模型,由商品模块统一管理。

图1 :初期商品模型

出现的问题

随着业务的发展,优惠的形式不断推陈出新,业务形态逐渐多样,业务方的需求也越来越个性化,导致后期的优惠中心无论从功能上还是系统上都出现了一些具体的问题:

1. 功能上来说,不够灵活

优惠信息是作为商品信息的一个属性在商品管理模块配置的。比如为了引导用户使用 App 需要设置 A 类型优惠,就通过在商品信息的编辑页面增加一个 A 类型优惠配置项实现;如果某个商品的 A 类型优惠需要在 0:00 分生效,业务同学就必须在电脑前等到 0:00 更新商品信息来上线优惠活动。

另外,如果想要创建针对所有商品都适用的优惠,按照之前的模式,所有的商品都要设置一遍,这几乎是不可接受的。

2. 从系统层面看,不易扩展

优惠信息存储在商品信息中,优惠信息是通过商品管理模块的接口输出的。如果要新增一种优惠类型,商品信息相关的表就要增加字段,商品的表会越来越大;如果要迭代一个优惠的逻辑,就有可能影响到商品管理模块的功能。

3. 不利于迭代

由于优惠信息仅仅作为商品的一个属性,没有自己的生命周期,所以很难去统计某一次设置的优惠的投入产出比,从而指导后续的功能优化。

重构优惠中心的预期

  • 系统层面上,要把优惠相关的业务逻辑独立出来,单独设计和实现;
  • 应用层面上,优惠中心会有自己的独立后台,负责管理优惠活动;也会有独立的优惠计算接口,负责 C 端用户使用优惠时的计算。

Part 2

为什么选择 DDD

避免贫血模型

基于传统的 MVC 架构开发功能的时候,Model 层本质上是一个 DAO 层,业务逻辑通常会封装在 Service 层,然后 Controller 通过调用 Service 层来完成对外的功能。这种模式下,数据和行为分别被割裂到了 Model 和 Service 两层。我们把这种只承载数据,但没有业务行为的 Model 称为「贫血模型」。

我们在和业务方了解需求的过程中,使用到的对象都是现实业务的映射,是行为和属性的综合体。需求确定好之后,我们开发的过程中,人为把行为和数据拆分成了两部分,做了一次转换。随着需求的迭代,人员的更迭,开发看到的代码和业务方的需求越来越对应不上,导致很多代码谁也不知道对应的是什么业务逻辑,这种现象被称为由贫血模型带来的「失忆症」,最终导致的是一个维护成本极高的大泥潭系统。

领域驱动设计的核心就是基于业务逻辑去建模,避免贫血模型,减少设计和开发过程中对业务信息的丢失和转换。在业务逻辑迭代的过程中,系统通过调整对应的业务模型就可以完成迭代。

Part 3

落地过程

关键点:业务逻辑抽象

要做到基于业务逻辑建模,就要合理地抽象。因为业务表象千差万别,产品经理和软件设计人员需要和业务专家深入交流,并且从离散的信息中抽象出业务内在的逻辑。

比如旅游业务售卖的商品和标品不同,有些优惠是不考虑人群的,比如使用优惠券,所有类型的库存都可以享受;但如 N 人 N 折这类优惠,成人价可以享受,儿童价和单房差就不可以。基于这个特点,我们对优惠中心的商品模型做了抽象,抽象出来「是否可以参与件数计算」和 「是否可以参与价格计算」两个通用属性。这样既实现了基于业务逻辑建模,又不会陷入业务逻辑千差万别的表象中。

3.1 战术设计

第一步:统一语言,提炼关键词

准确的语言对于产品、运营、开发等各方对齐需求非常重要,我们需要将优惠逻辑当中的概念抽象为各方都能理解的词语,以达成共识。作为开发人员来说,对领域的理解一般来说是比较少的,为了抽象出合理的语言让产品和业务方都能理解,就需要充分理解业务背景和需求。在熟悉业务和需求的过程中,提炼出若干关键字,这些关键词就是最初产生的领域概念和通用语言。比如:

  • 优惠类型:表示一种优惠规则和对应的优惠方案。比如早鸟优惠,就是早多少钱买(优惠规则),减多少钱/打几折(优惠方案);
  • 优惠活动:拥有完整的生命周期,需要包含时间、平台、人员、商品等(限制维度)的某种优惠类型的使用过程信息;
  • 优惠发现:根据指定的商品、人员和平台,找出可以使用的优惠活动列表服务;
  • 优惠计算:根据指定的商品、人员、平台以及购买数量,计算出这一次购买行为可以享受的优惠金额及优惠明细;
  • 优惠排序:各种优惠类型在计算的时候是有先后顺序的,如果有打折的优惠存在,那顺序不同,计算的结果也会不同;
  • 优惠互斥:某些优惠之间存在互斥的关系,比如使用了金卡 96 折优惠,就不能使用马蜂窝优惠券。

第二步:抽象领域模型

根据单一职责的原则,一个领域概念对应一个领域对象。领域对象有实体值对象之分:

  • 实体:实体是有状态的和唯一标识的,包含属性和行为;
  • 值对象:值对象是无状态的,是只读的,包含属性和行为。

区分实体和值对象对系统设计有很大意义,实体是我们需要重点关注和设计的,而值对象则只使用它的「值」就可以了。这样可以简化系统的复杂度,将精力聚焦在核心领域对象。不难理解,优惠活动毋庸置疑是一个实体,优惠类型就是一个值对象。

但也存在某些业务行为是不能归于某个实体或值对象的,可以将它们归为领域服务:

  • 领域服务:领域服务本质上就是一些操作,不包含状态,通常用于协调多个实体。实体和值都属于领域对象,领域对象之间的交互逻辑不能放在领域对象内部,必须由服务来实现,从而有效地保护领域模型。

有一些领域逻辑,比如「优惠排序」和「优惠互斥」,他们涉及到多个优惠类型,也就是多个领域对象。如果也被设计为领域对象,就打破了单一职责的原则,所以我们把这部分跨多个领域对象的业务逻辑放到「领域服务」层。

第三步:抽象领域对象之间的关联关系

将相关联的领域对象进行显式分组,来表达整体的概念(也可以是单一的领域对象),也就是「聚合」

比如优惠活动是优惠类型、优惠范围等的聚合;优惠类型是优惠规则和优惠方案的聚合;优惠规则是限制维度的聚合;优惠方案是优惠手段的聚合:

图2 :关联关系示意

聚合的主要功能是把领域对象分组,外部的唯一访问点就是聚合根,这样可以避免处理领域对象间的一一对应关系,只需要处理聚合和聚合之间的关系就行了。

第四步:走查场景,调整领域模型

领域模型的调整是贯穿整个设计和开发过程的,随着业务的调整,领域模型也需要调整。比如优惠中心后期引入了会员卡的优惠类型,那么就需要把优惠券这个优惠类型的显示,调整为与会员卡互斥的优惠券和与会员卡不互斥的两种。

第五步:简化设计,降低系统复杂度

建模的本质是对现实事物的一种简化和抽象,指导我们忽略和问题域无关的事实,提取和问题域息息相关的信息。以优惠中心为例,最初的方案里我们设计了优惠类型管理的功能,根据不同的优惠规则和优惠方案自动组合成不同类型的优惠类型。但是可以预见,未来的优惠类型是有限的,并且每个优惠类型都有会自己的特殊配置,比如 N 人优惠里的 每 N 人/第 N 人;早鸟中的提前 N 天等。也就是说,根据优惠规则和优惠方案自动生成优惠类型基本是没有使用场景的,因此也就去掉了这个设计。

再如,对优惠的限制我们最初是设计在优惠活动维度,经过权衡,为了降低系统复杂度,最后实现在了优惠类型层面。以「蜂抢」优惠类型为例,它的规则是所有的蜂抢活动都是 1 个用户只能抢一次,没有必要把这个限制放在优惠活动维度,在优惠类型层面控制就可以了。

3.2 战略设计

战略设计处理的是不同限界上下文之间的拆分和集成逻辑。限界上下文比较抽象,结合我们在文章开始提到的不同语境中的「商品」例子来理解,同一个词如果不说明白所处的语境,是无法准确描述清楚其表达的含义的。「语境」其实就是「上下文」,对应不同「子领语」。同理,如果不在一个限定好的上下文中去设计领域模型,设计出的领域模型是不清晰的,它就会同时支持多个上下文。

这里需要说明一点,如果是从零搭建一个全新的电商系统,首先需要做的应该是战略设计。而优惠中心是建立在现有大的电商系统基础上,相当于作为其中一个子领域进行重构,所以我们才会先来做战术设计,再考虑在完整的电商系统下它与外部其他环境之间的关系,也就是战略设计。

优惠中心内部场景区分

优惠中心包括了服务于 B 端用户的优惠活动管理和服务于 C 端用户的优惠计算这两个不同的子业务单元:

图3 :优惠中心内部场景区分

  • 优惠活动处理的是优惠活动的增删改查,以及配套的统计等业务;优惠活动在这里是一个实体,有完整的生命周期,有上线、下线等状态,可以被创建和删除;
  • 优惠计算处理的是一个订单能享受哪些优惠,并减多少钱的问题;在这个场景里,优惠活动是一个值对象,只提供优惠计算需要的必要参数即可。

优惠中心与外部系统集成

在整个电商系统的环境下,优惠中心作为一个子域,处于自己的限界上下文当中。使用优惠中心服务的详情页、下单页都处于自己各自的限界上下文,所以调用优惠中心的时候就需要设计它们之间的上下文映射方式。

调用和被调用方使用的战略设计方法通常有以下几种:

  • 客户方-供应方:适用于同一个团队之间的协作,上游会有严格的自动化测试,来保证给到下游的数据是一定符合约定的;
  • 遵奉者:适用于不同团队协作,且上游不关心下游的标准,下游又完全「逆来顺受」地接受了上游给的数据的场景;
  • 防腐层:适用于上游不关心下游的标准,但是下游不甘心「逆来顺受」,就增加一层,来做转换处理,保持下游系统的独立性;
  • 开放主机服务:适用于中台(通用能力平台),对接方非常多,业务重复度高,并且已经有完善的测试机制和通用的模型。

结合我们的实际情况来看,调用优惠中心的可能会是不同团队的开发人员,而优惠中心又不想被不同的上游侵入内部设计中,所以「客户方-供应方」和「遵奉者」模型都不适合;另外优惠中心前期接入方会比较少,而且会不断迭代,使用「开放主机服务」也不太合适。综合考虑下,防腐层的设计比较适合优惠中心。

下图是优惠中心的业务架构示意,中间的应用服务层采用的就是防腐层的设计,反映优惠中心与外部系统集成时的上下文映射关系:

图4 :优惠中心业务架构

3.3 架构实现

优惠中心选择的是经典的分层架构。从上到下为用户接口层、应用服务层、领域层和仓储层。图中不同的颜色块分别对映外部服务、应用服务、领域服务、聚合根、实体、值对象和仓储。

图5 :优惠中心分层领域模型

  • 用户接口层:处理和终端用户的交互逻辑;
  • 应用服务层:负责封装和转换领域层的返回数据给用户接口层;
  • 领域层:优惠中心的核心逻辑都在这一层,包括领域对象和领域服务。
  • 仓储层:仓储层负责把内存中的领域对象落地到存储介质,也负责从存储介质拿到原始数据后构造领域对象给领域层使用;这一层对领域层隐藏了底层的存储细节。虽然仓储层处在领域层下方,但是我们实现过程中采用了依赖注入的方式,将仓储层的具体实现注入到领域层中。

Part 4

问题及近期规划

1. 价格层优惠

现在公司面没有一个统一的商品中心,并且各业务线对商品的定义差别很大。比如自由行的商品包括出行日期、价格类别(成人价、儿童价)和套餐类别等层级;而火车票的商品包含座次、席别、目的地和出发地等层级。

如果优惠中心抽象出一种通用的商品层级来适配各个业务线,那实际上就是优惠中心要对商品进行标准定义,但是这个标准与后续商品中心的标准定义很有可能是不一致的,如果不一致优惠中心就要做大的改版。所以最终的解决方案可能还要通过推进统一商品中心的建立来解决。

2. 性能问题

领域驱动设计带来的弊端就是类的增多。目前优惠中心的技术栈基于 PHP, PHP 是一种解释型语言,在DDD 模式下即使有了 OPCode 等缓存技术,执行阶段的耗时相对其他静态数据类型的语言还是较大。所以后面计划将优惠中心使用 Java 技术栈重构,来进行性能上的优化。

Part 5

小结

本文介绍了马蜂窝电商优惠中心基于 DDD 进行重构的一些实践经验。DDD 的思想也帮助我们在业务迭代的过程中将架构设计得更加合理。

当然,是否采用业务驱动设计的思想,需要取决于业务和团队的实际情况。在马蜂窝业务的快速发展下,我们在架构设计上还将做更多的探索,也将持续与大家交流。

天亮以前说再见 – 康梓峰

水花只能开在雨天

烟花要绽放在黑夜

雪花多舍不得冬天

像我舍不得和说你再见

谎言并不代表欺骗

诺言也不一定兑现

誓言就都留给时间

就请把从前留在今天

天亮以前说再见

笑着泪流满面

去迎接

应该你的

更好的明天

昙花若只一现

更要开的耀眼

别回头去拥有

属于你更好的世界

天亮以前说再见

让我留在今天

去保护

我和你的

最好的夏天

情丝若水三千

只取一瓢眷恋

当你来过的纪念

水花只能开在雨天

烟花要绽放在黑夜

雪花多舍不得冬天

像我舍不得和说你再见

谎言并不代表欺骗

诺言也不见得兑现

誓言就都留给时间

就请把从前留在今天

天亮以前说再见

笑着泪流满面

去迎接

应该你的

更好的明天

昙花若只一现

更要开的耀眼

别回头去拥抱

属于你更好的世界

天亮以前说再见

让我留在今天

去保护

我们最美

最好的今夜

情丝若水三千

只取一瓢眷恋

当你来过的纪念

红颜弹指老,刹那芳华-程灵素

我走过山的时候山不说话,我路过海的时候海不说话;

我坐着的毛驴一步一步滴滴答答,我带着的倚天喑哑。

大家说我因为爱着杨过大侠,找不到所以在峨嵋安家;

其实我只是喜欢峨嵋的雾,像十六岁那年绽放的烟花。

我路过海的时候海不说话,我走过山的时候也听不到回答;

我骑的毛驴步步滴滴答答,悠悠飘向远处可从不想要回家。

正当喜乐无忧年韶华如花,远游风尘之色却不似十九风华;

愁思袭人无计回避真牵挂,不知天涯何处有那我思念的他。

没半点音讯怎续风陵夜话,见不到大哥哥愿知他如何行侠;

上少室山想找无色问一下,老禅师亦不知他是在何处为家。

闻琴声似和鸟语交相应答,百鸟朝凤曲难道还有人能抚它?

白衣男子划了一画又一画,不是剑法是痴迷在那棋子围杀。

旁观者清一语道破危难局,我弹奏古曲留下了他独自惊讶;

高歌一曲轻身去不想其它,屈指昆仑三圣闯少林又有何法?

三个老者连骑而来又回转,以为是留书之人却是少林俗家;

他从石亭顶抱着瑶琴落下,教训了青脸人只为他将我威吓;

在亭上听了我和三人对答,其实他也不知名满天下的爹妈;

这人姓何,双名叫作足道,名字谦逊得哪有半点狂妄自大?

琴剑棋三绝技艺冠绝西域,昆仑三圣并非三人就是一个他;

抱着瑶琴到处找我为新曲,丰瞻华美奇妙调和考盘与蒹葭;

伊人难道是我,思慕如霞,右手弹琴左手使剑退敌亦惆杂。

从万里远赶来原为送句话,什么经书是在油中好让觉远拿;

平生足迹未履中土乘此游,路上碰到三个西域少林要比划;

非逼他去了剑圣名头不可,反正上少林寺一番做教我猜岔。

萍聚缘和山花与风的烂漫,我怎会不知自己是他心中的她?

若是真能为我再重弹一曲,或许我大哥哥找不到还有个他;

他那张嘴会说话可真不假,凭片言折服老和尚请我进奉茶;

为我一句话愿意不来打架,比剑嫌霸道青石板上把棋盘划。

觉远擦掉棋盘又将他剑夹,君宝斜击一掌他没能分身招架;

他誓用十招将这少年打发,虽取巧让君宝倒下也算输此架。

足尖一点身已在数丈之外,传完话就走人怎么忘了叫上咱?

挑着张君宝和我觉远迅跑,到深山里面疲累过度圆寂坐化;

让伤心的少年去找我爹妈,自己却不知何去何从心乱如麻。

不是为杨过才在峨眉住下,我喜欢峨眉的雾像那年的烟花;

我身上佩的倚天宝剑暗哑,昆仑何足道心头定未将我丢下;

自少林飘然远去不再回头,他潇洒的身影为何却泪如雨下?

方法内统计耗时的几种方法

1、自己写,计时开始结束使用System.currentTimeMillis()

long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(3L);
System.out.println(“method finish , use time ” +(System.currentTimeMillis() – start) + “ms”);

2、使用StopWatch类来实现

StopWatch类有不同的实现,spring有一个,guava也有一个,具体使用根据自己项目的依赖情况。真正使用上基本一致。

使用方式:

System.out.println(“SLAMonitorThread.main() start”);
StopWatch sw = new StopWatch();
sw.start();
TimeUnit.SECONDS.sleep(1L);
sw.split();
System.out.println(
“SLAMonitorThread.main() end. split:” + sw.getSplitTime() + “, ” + sw.toSplitString());
TimeUnit.SECONDS.sleep(1L);
sw.split();
System.out.println(
“SLAMonitorThread.main() end. split:” + sw.getSplitTime() + “, ” + sw.toSplitString());
TimeUnit.SECONDS.sleep(1L);
sw.split();
System.out.println(
“SLAMonitorThread.main() end. split:” + sw.getSplitTime() + “, ” + sw.toSplitString());
TimeUnit.SECONDS.sleep(1L);
System.out.println(“SLAMonitorThread.main() end. end:” + sw.getTime() + “, ” + sw.toString());
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(1L);

两种方式的优缺点:
方式1是不用任何外部依赖就可以实现的,但是缺点也同样明显,每个位置要打印一次的时候,都需要自己计算,并且还要重新拿到开始时间。
方式2则正好相反,需要guava的依赖,但是好处是提供了比较常用的接口支持,在不同的位置统计,并且计算一些东西,比如某一段耗时占比之类的,都可以友好支持。

具体使用时根据自己情况即可。

链家小区坐标爬取

之前已经爬取了链家小区的一些基本信息,主要是列表里的内容。但是这里面没有我最需要的数据之一:小区坐标,于是开始重新找。 首先发现小区详情页面是有地图的,根据经验,如果有地图的定位,在页面上肯定有坐标。地图是百度地图,正好是我需要的百度经纬度坐标体系。 自己找的时候走了一些弯路,但是我觉得这也应该是没有经验的情况下的正确查找路子。 我是首先打开开发者页面,然后看点击地图的时候发起的请求,点击地图,放大缩小,看发起的请求里有没有坐标。遗憾的是没能找到,也许是我漏掉了。但是我知道页面一定有,否则这里调用地图的api进行定位的时候显然是不行的。 在我查看源码,从详情页整理小区数据的时候,偶然看到,我不需要的一部分数据里:附近门店 的信息里,隐藏着小区的坐标。如下图所示

很开心,这么容易就找到了。 于是写代码,将这部分数据洗出来,代码如下: countStr = locationInfoItem.selectFirst(“span[class=xiaoquInfoContent]”).text();String locationInfo = locationInfoItem.selectFirst(“span[class=xiaoquInfoContent]”).selectFirst(“span[class=actshowMap]”).attr(“xiaoqu”); locationInfo = locationInfo.replace(“[“, “”).replace(“]”, “”); entity.setLocationJingDu(locationInfo.split(“,”)[0]);entity.setLocationWeiDu(locationInfo.split(“,”)[1]);由于前面已经跑完小区的基本信息了,小区详情页面的地址也已经保存下来了,于是直接把这些数据重新跑一遍,根据详情页面爬取坐标数据,以为于是就完事儿了 然而,到最后发现,总是有那么百十个小区,解析总是失败的。观察日志,发现请求是正常的并且成功的,如果是被反爬虫了按照我这换ip的频率,也不应该一直失败的,肯定代码原因咯。 没办法,debug大法。调试到获取坐标这块的时候,异常了,仔细观察发现,有的小区是没有附近的门店的!没有的时候,我的解析坐标的部分就会异常。 那咋办?找到一个正常的页面,然后找到附近门店那里的坐标数字,然后全页面搜索,果然不服所望,还有个地方存着坐标!如图所示

然后到没有附近门店的页面去验证,果然也有!这下好了。 改代码对 附近门店 这里做个判断,如果是  暂无门店信息 ,那就去找js标签,然后从中提取坐标。因为比较好找,我的数据又比较少,懒得用正则了,直接split完事儿。上代码 Elements scriptElements = doc.select(“script”); String shopLatLng = “”; for (Element element : scriptElements) {    shopLatLng = element.html();    if (shopLatLng.contains(“resblockPosition”)) {       break;    } }shopLatLng = shopLatLng.split(“require\\(\\[‘ershoufang/xiaoquDetail/index’], function\\(main\\) \\{\n” +”  main\\(“)[1].split(“\\);\n” +”\\}\\);”)[0]; ResBlock resBlock = JsonUtil.of(shopLatLng,ResBlock.class); Optional.ofNullable(resBlock).ifPresent(x->{   entity.setLocationJingDu(x.getResblockPosition().split(“,”)[0]);    entity.setLocationWeiDu(x.getResblockPosition().split(“,”)[1]); });至此,获取小区坐标就完成了,然后顺利跑完交差。

链家小区数据爬取

这次爬取链家数据时爬取的链家北京区域的所有小区数据。
区域比较少,手动去把每个区的列表页面罗列了出来(所有分区全家起来288个)
没有去处理当前总共多少页,之类的数据,而是把这个数据导入到一张表里,表字段设计了页数和当前爬取的页码,这样方便重试,也不做无用功跑已经跑过的数据。
由于一开始只需要小区的名称,地址,当前均价等,这些信息都有了,所以也没有去爬详情。
列表页面的URL格式为:https://bj.lianjia.com/xiaoqu/guangqumen/
默认为第一页,首页之后的格式均为在当前URL后面添加 pg+pageNo+/ 。
String realUrl = baseUrl + “pg” + pageNo + “/”;
通过写好的可以使用代理的HttpClient发起请求,列表页还是很容易请求成功的。
然后使用Jsoup解析请求到的网页字符串(据大神说,用XPath更高端更牛皮,不过我用着Jsoup还很顺手,就暂时不换了 – 主要以前用jquery习惯,所以找起来也方便)
import org.jsoup.Jsoup;
Document doc = Jsoup.parse(html);
打开网页调试工具,找到翻页位置的元素,发现:翻页的所有链接都在li[class=house-lst-page-box]元素内的超链接标签<a>上,并且最后一个超链接标签就是最大页码的标签,标签上的属性 data-page 就是页码,也就是最大页数,正好提取出来
代码:
Elements pageList = doc.select(“li[class=house-lst-page-box]”).select(“a”);
Element a = Safes.first(Lists.reverse(pageList));
Integer pageCount = Integer.valueOf(a.attr(“data-page”));
至此最大页数和当前页都已经拿到了。接着是收集小区信息。
小区列表中每个小区的信息就在一堆class=xiaoquListItem的li元素里,包含了列表里需要的所有小区的信息,先把每个小区的块都拿到。(当时爬的时候,下面填充用的标签还用的是a标签,写文章时候已经改成div和a标签交叉的了,感觉链家也不是没有做反爬虫,只是做的比较简单,下面呈上原来的老代码)
ElementsxiaoquList=doc.select(“li[class=xiaoquListItem]”).select(“a”).select(“a[class=PageLink]”)
接下来看源码就比较清楚了,里面分了三大块,左侧是图片,中间是小区名称、最近的成交信息、地址信息、代理人信息和标签信息,放在class=info的div里,每一块信息是一个div,右侧是价格、在售信息等内容,放在class=xiaoquListItemRight的div里。
从里面分别取出来这些信息,并放到对应对象里
List<LianJiaXiaoquEntity> entityList = Lists.newArrayList();
Safes.of(xiaoquList).forEach(xiaoqu -> {
    LianJiaXiaoquEntity entity = new LianJiaXiaoquEntity();
    Element title = Safes.first(xiaoqu.select(“div[class=title]”)).selectFirst(“a”);
    entity.setName(title.text());
    entity.setUrl(title.attr(“href”));
    Optional.ofNullable(Safes.first(xiaoqu.select(“div[positionInfo]”))).ifPresent(positionInfo -> {
        positionInfo.text();// \r\n&nbsp;\r\n&nbsp;/板楼/塔板结合/r/n&nbsp;2002年建成
        entity.setNameDetail(StringUtils.join(positionInfo.select(“a”).stream().map(Element::text).collect(Collectors.toList()), “-“) + entity.getName());
    });
    Optional.ofNullable(Safes.first(xiaoqu.select(“div[class=xiaoquListItemRight]”))).ifPresent(price -> {
        entity.setAveaPrice(Safes.first(price.select(“span”)).text());
        entity.setPriceTime(Safes.first(price.select(“div[class=priceDesc]”)).text());
    });
    entityList.add(entity);
这样,就把列表里一些小区的基本信息保存下来了。
但是我需要的不只是这些,主要还需要小区的坐标,这个在列表里没有。找小区坐标的过程也是一波三折,接下来说。

爬取大众点评商家信息

老板安排了新的任务,需要爬取一些数据,然后进行机器学习,来给决策提供依据或者建议。

当前这一步,爬取相关的大众点评商家信息,并建立关系。

当前已有:选址名称,腹地名称,选址经纬度,腹地经纬度。

思路1:筛选点评上符合要求的商铺,然后全部爬下来,再与当前已有的数据建立联系(通过经纬度,地址等方式,需要商铺的经纬度、地址等信息)

思路2:根据当前已有信息,如地址,经纬度,去点评上搜索附近的商铺,然后爬下来,这要求知道点评根据经纬度筛选商铺的接口,经过观察发现点评的APP上有这个功能,但是抓包失败。

思路3:从点评的m站上搜索,关键词为地址,然后过滤美食。结果并不好用,经常查询的是具体地址,而非商铺。

思路4:直接从爬虫群里购买数据和代码,但是可能不靠谱。

思路5:从网上搜索爬取商铺的代码,改改自己用。这种方式多数都是从web版的网页上进行爬取的,自己执行爬取的时候,得注意使用代理切换ip,不然的话,可能数据会有问题。

尝试了一番之后,大概只有第五种方法比较可行。

从百度直接搜索点评爬虫,和在gitee上搜索点评的爬虫,发现绝大多数都是爬取的都是去爬的商铺的评论,但是这顺便会爬商铺。

从中甄别一下看是不是有坐标经纬度,如果有的话最好,可以通过经纬度的计算来筛选某些点附近的商铺,然后计算。

总结下来基本还是思路1的路子。OK,开始执行。

关于寻找目标市场的思考

最初是什么想法呢。。。。。。。。

多级市场划分。

划分的维度是多方面的,比如城市等级,大小,经济,人口等。任何一项事务,扩散总是有一个过程的。多数情况下,都是由大城市发起或者由国外流入国内一线大城市,然后逐渐再扩散到二线三线直至到四五线城市。

由于身处大城市,虽然不是说随时随地都是在关注最前沿的流行的东西,但是毕竟耳濡目染,身边环境所致,还是能够意识到一些东西。尤其是逢年过节什么的,从一线城市回到家里的时候,你会觉得家里的这份“土”,其实就是潮流扩展的过程所体现出来的现象之一。

所以节假日什么的,也是二三四线城市与大城市交流沟通和打通的手段了。通过这时候从大城市到小城市的人员流动,从而促进了二三四线城市的潮流推进。

反过来说,经常在一二线城市与三四五线城市之间走动,也应该会更容易发现不同级别城市之间的差异,发现二三四五线城市的发展趋向,预见其发展潮流,从而发现其中的商机。

想要利用这个过程,就是主动去发现,然后主动去学习,效仿大城市的内容、潮流,然后带到二三线城市里去。从中应该能赚到一些潮流红利。

降维打击

对,这就是降维打击。用一种几乎已知未来的方式来操控更低维度城市的发展过程。就跟使用互联网打击传统行业一样。

三四五线城市居民的特点

根据一些已经在这个市场上火起来的应用来分析一下吧。目前已经火到大城市也能看到、知道的产品和市场,有这么几个:

  • 抖音,快手
  • 拼多多
  • 趣头条
  • 今日头条

这几个是已经十分火爆的,并且在一线城市都已经有了相当知名度的产品。他们瞄准的基本都是二三四五线的用户。甚至比较火的一些小程序,公众号等,做的也是这方面的内容,但是因为整体平台并不是这个定位,所以先不放这部分。对于今日头条是否列入这里也稍微存在争议,不过因为头条使用的推荐算法,是你越看什么,就越给你推荐什么的。所以针对二三四线城市,就会针对他们推荐他们这个市场自己关注的内容,也算是一种吧。

其他几款就不一样了,基本就是专门针对这个市场做的。

大致总结一下面对人群的特点:

  • 空闲时间多
  • 看中小利益,容易被低额小利吸引
  • 传播成本低,喜欢噱头
  • 乐于扩散

目前还有哪些东西可以在这样的市场上搬运和投放呢?思考一下,回头写。

无题

又是在咖啡厅呆了挺长的时间。什么都做了,比如看书,看小说,阅读一些文章,emmmmmm…..甚至睡觉。感觉生活又失去了方向感。

之前一段时间对区块链比较看好,自己也有意想要深入了解,因此投入相当的精力。但是回过头来看,自己投入精力的地方,其实主要并不在学习上,而是在如何参与上。自己并没有真正将这个东西了解透彻(当然自己是想了解透彻的),就已经持续参与到一些项目中了。在学习的时候,主要也围绕着这里可能用到的东西,但是更多的时候,是使用自己一知半解的知识,去强行依附正在操作的新概念项目。从概念上,其实也还好,对自己的理解力和悟性还是有一些信心的,这方面并没怎么跑偏。但是对于真正技术上的实现,自己就差的有点远了,可以说最基础的知识还没有完整的看完过。对于整个系统的理解,也没有到位,比如,到现在为止,自己对于问题:token是如何值这么多钱的 这个问题,有一个最起码看起来合理的解释。

加上正好凑这段时间,自己也换了新工作,新工作这边压力还是挺大的,要求高是一方面,大家水平都比较高,压力比较大也是一方面。上个月吧,公司经历了一番调整,而正好我也有机会单独带一个需求。也算是表现了一番吧,转正时候有意给我升级别,最终结果还没出来。这次完了之后,自己调整到另外一个部门去做另外一个东西,当然其实还不是单独的。

自己在这段时间也思考了一下,感觉把精力铺的太开还是不行,需要专注某些东西,不然自己忙不过来。当然其实这不是最主要的问题。最主要的问题之一,是自己现在已经有些懒惰了,总想做一些快速享受愉悦的短视的事情,比如看部电影,看本小说。精力没有集中在要做的事情上,哪怕自己思考过了,也仍然是这样。这才是最亟待改变的地方。如果自己的注意力能十分,不,不说十分,七八分集中吧,很多事情也做下来了,能够有一些成就。

但是这个习惯很难一下就改过来,所以想通过收敛一下自己关注的东西,让自己注意力能尽量的放在一两件事情上,算是从侧面的一种方式,让自己不用“忙”这个状态给自己找借口。

自己现在也不说能力多强多差,但是,做事情总是在那个“完成”的层面上,说不上好,也说不上差,但是更远远说不上卓越。总是需要把事情做到卓越的程度,才能真的让自己在大佬们面前露脸啊。

消费层级的逻辑

任何一个行业,想要做好,想要赚钱,都得思考清楚其中真正运作的逻辑。包括工作。

工作不就是找一个公司,签一份合同,然后自己交工,到时候发工资么?

仔细想一下,就会发现远不止如此。否则,为何工作工资有不同?为何同样的工作会有话语权的不同?为何同是工作,会有自由度的不同?

但是今天想说的不只是工作上。很多事情都是同理,所以做个普遍性的思考吧。

赚钱?挣钱?这两个词,都是为了钱,但是前面的动词明显不一样。

何为挣钱?花费自己的劳动力,包括以前储备的知识,自己的时间,脑力,体力等方式,换取自己付出这些的报酬。对,就是报酬。报酬,是你付出之后,才能得到的,如果你要持续的收入,就要有持续的付出,一旦付出停止,那么就面临“没有收入”的境地,即老人们说的“停手停口”。

然而大多数人都停留在这个阶段上,或者说层面上,很难跳出这个惯性和框架。我呢?我也没有,但是我接触并意识到了这个问题,正在努力想办法跳出来,所以才写这篇文章。

并不是说“挣钱”不好。正如现在流行的,“不要问我我的理想,我的理想是不上班”,并不只是说说,我的理想,真的是不上班,或者说有比较高的自由度,还能有很多钱花。但是,不上班和有很多钱花,一定程度上,是一个鸡和蛋的问题。

好了,不说这些了,这些问题都不是核心。核心是我要认真思考,究竟怎么样才能自由度比较高。对我当前的认知而言,就是需要比较多的钱,不用为钱而担心。

那么,在“挣钱”的层面的时候,怎么样能挣到更多的钱呢?多数情况下,给别人打工,我们才称之为“挣钱”,要用自己的付出去换,那么自己的“付出”越值钱(即价值越高),自己获得的“报酬”才有可能越高。为什么说“可能”,企业的老板都是都是商人,他们做一个企业,是为了自己的收入的,从员工的付出与给予员工的报酬之间拿到差价,才是商人的收入。所以在我理解看来,在能留下一个人的情况下,肯定会尽最大可能压低给这个人的报酬,这是天性。因此企业里制定的一系列的关于报酬的制度,除了为了方便管理之外,主要的目的就是为了降低付出“报酬”的成本。

从这里来看,在打工的情况下,想要提高收入,有两个切入点。其一,提高自己的价值,提高自己能产生的价值,让企业愿意为了你个人的价值而付出贡多的薪酬;其二,在跟企业的“定价”博弈中,拿到更高的工资。如何提高自己的价值,如何跟hr博弈在谈工资的时候不吃亏等等,不分析了,我想要做的也不是这块。

我想要做的,是摆脱“打工者”的身份。

上了这些年学,工作了这么多年,曾经有数次机会(现象级别的,社会级别的)从面前走过,上学时候的个人站、SEO,淘宝个人店,11年就看过一点的区块链,之后13、14的移动端,一直火到15、16,并行的大数据,然后人工智能等。这期间,自己稍微有些想法,曾经接触过的,有淘宝开店,区块链,移动端。这些任何一样,如果能深入做下去,都会给自己带来不菲的收益。但是都死在了浅尝辄止。

想要摆脱“打工者”的身份,必须得有自己的一份事业,持续为之投入,然后这个事业能自动的,持续的为自己带来收入。自己也早有这个意识,很多事情坚持下去,做下去,“也许”就会离自己的梦想更近一步。但是自己都没坚持下去。原因是什么?

说着说着成分析自己了。打住,继续搞赚钱分析。

赚钱嘛,用“钱”赚钱,就需要这部分钱要动起来,能自己产生收益,就是资本的运作。有人说,我那点钱,算什么资本运作啊哈哈哈。不要这么想,资本有大小,无论大小,都是资本,学会资本运作,都能产生收益。否则,你的钱多钱少,存在那里,都是死的,只能用一部分少一部分。不能运作,不能增值,不能产生收益,迟早有用完的那一天。

仔细想想,其实不只是“钱”才算作资本,一个人的人脉,资源,技能,时间,都是自己的“资本”。这些都应该运作起来,让他们发挥作用,产生价值。

(20180602补充)
想要清醒一点的活着,或者明白一点的活着,就得把自己梳理明白了。正确分析解剖自己,看清楚自己的优势劣势,然后确认自己想要做什么,然后看自己缺少什么资源,然后去通过提高自己或者寻找合作伙伴的方式弥补这些短板,从而做成自己想要做的事。

目标导向有时候也许更适合去做事情,没有目标的日子都是被动的活着做事,总是感觉被动,还会感觉累,并且总是被动思考,思考的内容总是会有缺失,毕竟没有主动思考时候更活跃,更专注。

OK,回来说赚钱。

一般来说,赚钱,就是利用自己的一些资源,用较低(或者想办法降低)的成本实现满足一些(量足够大)人的需求,同时收取相应的费用,就是赚到钱了。比如自己利用自己的地理优势,从制造厂商那里以较低的价格批发衣服,然后转而加价卖出去,然后扣除掉中间运输,存储,陈设等相关的费用,就是自己赚到的钱咯。说起来似乎就是这么简单,“做生意”其实逻辑就是很简单。

逻辑简单不代表执行、实现简单。过程中肯定会遇到很多自己想象不到的问题,但是最困难的还是第一步(自己感觉,非实践的来)。

想要做,得先选领域。这时候就得要考验一个人的眼力了。你选的领域,是不是真的有需求,你选择做的东西,有没有前途,你做的附加值,有没有人买账?这些东西一方面要考验自己思维的能力,另外一方面就要考验自己调研和信息整合的能力了。

选择什么人群呢?下面就是 摘自别人家文章,没有确切研究过。

消费能力排序?

少女》儿童》少妇》老人》狗》普通男人