一条短租房源信息,比如“碧桂园山湖城观澜1街1座2903,3室1厅1厨1卫2阳台,3床:1.8m+1.5m+1.2m,可日租/周租/月租,欢迎各位邻居咨询”,看起来只是几行文字,但在房东、平台和租客之间流动时,它要承担很多角色:房源唯一标识、展示信息、床型容量、租期模式、联系方式。非结构化文本一旦进入数据库或接口,就会立刻暴露出问题:很难查询、统计和自动计算。如果你正在做民宿短租系统、房源信息聚合平台或本地生活类应用,迟早要处理这类文本到结构化数据的转换。这篇文章以这条真实文本为样例,拆解如何把一条短租房源广告转成字段模型、数据库表、JSON 校验规则和可运行的解析脚本,并讨论日租、周租、月租场景下最容易被忽略的工程细节。
1. 原始房源文本里有价值,但要先拆成可管理字段
1.1 为什么必须做字段结构化
先理解一个事实:人是靠语义阅读文本的,程序只能靠字段、索引和逻辑来理解数据。上面这条房源文本,人一眼能看出是 3 室 1 厅 1 厨 1 卫 2 阳台,有三张床,分别接近 1.8 米、1.5 米和 1.2 米,支持日租、周租、月租。但程序拿到这段字符串时,如果只把它当作标题字段存进数据库,后续所有业务都很难做。
举个例子。租客想筛选“两居室以上”“有 1.8 米大床”“可以月租”的房源。如果原始文本只是被放进一个长文本字段,最常见的做法是使用LIKE '%月租%'模糊查询,结果可能包含“月租金”“月租价”等干扰项。更麻烦的是,日租价格、周租价格、月租价格如果都写在同一段话里,程序无法自动判断哪个价格对应哪个租期。
结构化之后,每个字段都变成独立可校验、可索引、可参与计算的数据。比如卧室数量是bedroom_count = 3,床型列表是[1.8m, 1.5m, 1.2m],租期模式是["day", "week", "month"]。这样筛选条件可以写成 SQL,价格可以通过租金策略表计算,房态可以通过日历表排期。
没有结构化的房源文本,短期看只是维护成本高;长期看会出现同一房源在不同平台展示不一致、价格计算错误、订单冲突等连锁问题。所以第一步不是急着写代码,而是把一条文本里的信息拆成完整字段清单。
1.2 字段拆分:从文本提取的信息清单
把“碧桂园山湖城观澜1街1座2903,3室1厅1厨1卫2阳台,3床:1.8m+1.5m+1.2m,可日租/周租/月租,欢迎各位邻居咨询。”按信息类别拆分,可以得到下面这些字段。
| 原始片段 | 字段名 | 字段类型 | 示例值 |
|---|---|---|---|
| 碧桂园山湖城 | project_name | string | 碧桂园山湖城 |
| 观澜1街 | block_name | string | 观澜1街 |
| 1座 | building_no | string | 1座 |
| 2903 | room_no | string | 2903 |
| 3室1厅1厨1卫2阳台 | bedroom_count / living_room_count / kitchen_count / bathroom_count / balcony_count | int | 3 / 1 / 1 / 1 / 2 |
| 1.8m+1.5m+1.2m | beds | array | [{size_m:1.8, count:1}, ...] |
| 日租/周租/月租 | rental_modes | array | [day, week, month] |
| 欢迎各位邻居咨询 | contact_note | string | 欢迎各位邻居咨询 |
这里要区分“地址字段”和“房间字段”。project_name、block_name、building_no、room_no描述的是房源位置;bedroom_count、living_room_count等描述的是户型。真实系统中,地址部分通常要接入地址库做标准化,不能只靠正则把“观澜1街”抽出来就算完成。
room_no也要注意歧义。样例中的“2903”看起来是 29 层 03 号,但在不同楼盘里,它可能表示 2 栋 903 室,也可能是 29 层 03 号。具体含义需要结合项目楼栋规则确认,不要想当然把2903拆成29和03两个字段。
1.3 文本里缺失的字段要补什么
这条原始文本并没有包含短租系统需要的全部信息。拆字段时,除了从文本中提取,还要主动补上缺失项。
至少缺少这些字段:
- 房源唯一 ID:
house_code,用于关联订单、价格、日历和平台同步。 - 价格:日租价、周租价、月租价,分别多少钱,包含哪些费用。
- 可住人数:每张床对应可住几人,客厅是否有沙发床。
- 起租天数:月租是否要求至少 30 天,日租是否至少 1 天。
- 押金和清洁费:是否需要押金,退房清洁费怎么算。
- 联系人和联系方式:由谁发布,联系方式是什么。
- 房源状态:上架、下架、停用、审核中。
- 创建时间和更新时间。
这些字段在设计数据表时都要预留。价格、押金等内容缺失时,可以先允许为空,但不能因为文本没写就不设计对应表结构。否则后续接支付和订单时,要再改表结构,代价会更大。
2. 数据模型要能表达“3室1厅1厨1卫2阳台”和“1.8m+1.5m+1.2m”
2.1 设计房源基础表
先把最核心的房源表设计出来。下面是一份 MySQL 风格的表结构,用来承载样例文本中已经提取到的基础信息。
CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_code VARCHAR(64) NOT NULL UNIQUE, project_name VARCHAR(128), block_name VARCHAR(128), building_no VARCHAR(32), room_no VARCHAR(32), bedroom_count INT DEFAULT 0, living_room_count INT DEFAULT 0, kitchen_count INT DEFAULT 0, bathroom_count INT DEFAULT 0, balcony_count INT DEFAULT 0, max_guests INT, status VARCHAR(32) DEFAULT 'OFFLINE', source_text TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );这里有几个关键点。
house_code是业务唯一键,不应该让用户直接输入“碧桂园山湖城观澜1街1座2903”作为唯一键,因为用户可能多写一个空格、少写一个“街”,又或者换一种写法。建议由系统按规则生成,比如BGYYHC-GL-01-2903,并在录入时用地址库或人工确认。
source_text字段很有必要。它保存原始文本,方便以后排查解析问题。解析器改规则后,可以回放历史文本,对比新旧解析结果,减少回归风险。
max_guests没有在原始文本中出现,但它是短租系统必须有的字段。它可以根据床型自动计算,也可以由运营人员手动校正。注意:床型可以决定理论最大人数,但实际能住多少人还会受到沙发、儿童床等因素影响,所以应该单独存储。
2.2 床型和租期模式不能塞进一个字段
床型不能放到house表的一列里。虽然可以存成"1.8m+1.5m+1.2m",但这样的字段无法参与统计。如果运营想统计全平台有大床房的房源数量,或者计算平均床尺寸,字符串字段处理起来非常痛苦。
更合适的做法是建立子表,或者使用兼容 JSON 的数据库字段。如果使用的是 MySQL 5.7 以上版本,也可以用 JSON 类型;但从扩展性和查询灵活性看,独立子表更稳。
CREATE TABLE house_bed ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL, bed_order INT NOT NULL, size_m DECIMAL(4, 2) NOT NULL, bed_count INT DEFAULT 1, bed_type VARCHAR(32) DEFAULT 'DOUBLE' ); CREATE TABLE house_rental_mode ( house_id BIGINT NOT NULL, mode VARCHAR(16) NOT NULL, price DECIMAL(10, 2), currency VARCHAR(8) DEFAULT 'CNY', min_days INT, max_days INT, is_active BOOLEAN DEFAULT TRUE, effective_date DATE, expired_date DATE, PRIMARY KEY (house_id, mode) );house_bed表里bed_order用来表示床的顺序,因为展示时通常要保持原始顺序:1.8m、1.5m、1.2m。size_m用DECIMAL(4,2),不要用浮点类型存床尺寸和价格,否则计算时容易出现精度问题。
house_rental_mode表把日租、周租、月租拆成独立记录。这样做的原因是:日租、周租、月租往往有不同的价格、折扣和起租天数。如果只在house表里放一个is_day_rental、is_week_rental、is_month_rental布尔字段,后续就没办法表达“日租 300 元/晚,月租 6000 元/月,至少租 30 天”这类完整信息。
2.3 用 JSON Schema 做数据校验
在服务端接收前端提交的房源信息时,可以使用 JSON Schema 做第一道校验。下面是一个针对本例的 JSON 表示。
{ "house": { "house_code": "BGYYHC-GL-01-2903", "project_name": "碧桂园山湖城", "block_name": "观澜1街", "building_no": "1座", "room_no": "2903", "rooms": { "bedroom": 3, "living_room": 1, "kitchen": 1, "bathroom": 1, "balcony": 2 }, "beds": [ { "size_m": 1.8, "count": 1 }, { "size_m": 1.5, "count": 1 }, { "size_m": 1.2, "count": 1 } ], "rental_modes": ["day", "week", "month"] } }JSON Schema 示例片段:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["house"], "properties": { "house": { "type": "object", "required": ["house_code", "rooms", "beds", "rental_modes"], "properties": { "house_code": { "type": "string", "pattern": "^[A-Z0-9\\-]+$" }, "rooms": { "type": "object", "required": ["bedroom", "living_room", "kitchen", "bathroom", "balcony"], "properties": { "bedroom": {"type": "integer", "minimum": 0}, "living_room": {"type": "integer", "minimum": 0}, "kitchen": {"type": "integer", "minimum": 0}, "bathroom": {"type": "integer", "minimum": 0}, "balcony": {"type": "integer", "minimum": 0} } }, "beds": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["size_m", "count"], "properties": { "size_m": {"type": "number", "minimum": 0.5, "maximum": 3.0}, "count": {"type": "integer", "minimum": 1} } } }, "rental_modes": { "type": "array", "items": { "type": "string", "enum": ["day", "week", "month"] } } } } } }bedroom这类数量字段设置minimum: 0比较合适,因为有些单间公寓可能没有独立客厅和厨房。size_m的范围可以根据常见床型定义,一般单人床 0.9 米、1.2 米,双人床 1.5 米、1.8 米,最大不会超过 2.2 米。如果出现超过 3 米的尺寸,大概率是数据有问题。
注意:JSON Schema 只能做格式和范围校验,不能保证业务正确。比如某条数据同时标记“可月租”但没有填写月租价格,这类业务一致性需要由服务端代码继续检查。
3. 用 Python 解析器把广告文本自动变成 JSON
3.1 解析前的输入约定
自动解析文本前,先约定输入格式,否则规则会越写越复杂。样例文本算是比较规整的,因为房间和床位信息用数字、量词和分隔符表达。真实业务中还会出现“三室一厅”“1.8米大床+1.5米床”这样的写法。
一个可行的策略是:发布入口尽量使用结构化表单,只把“文本解析”作为辅助录入手段。辅助解析的价值在于减少录入工作量,但结果要落到同一个校验逻辑里。下面先以这条样例文本作为输入:
raw_text = "碧桂园山湖城观澜1街1座2903,3室1厅1厨1卫2阳台,3床:1.8m+1.5m+1.2m,可日租/周租/月租,欢迎各位邻居咨询。"3.2 正则提取房间、床位、租期等关键信息
这段 Python 代码演示如何从原始文本提取结构,并不代表生产级方案,但足够说明思路。
import re import json def parse_house_text(text: str) -> dict: result = {} room_pattern = re.compile( r"(?P<bedroom>\d+)室" r"(?P<living_room>\d+)厅" r"(?P<kitchen>\d+)厨" r"(?P<bathroom>\d+)卫" r"(?P<balcony>\d+)阳台" ) m = room_pattern.search(text) if m: result["rooms"] = {key: int(value) for key, value in m.groupdict().items()} else: result["rooms"] = None bed_sizes = re.findall(r"(\d+(?:\.\d+)?)m", text) result["beds"] = [{"size_m": float(size), "count": 1} for size in bed_sizes] rental_modes = [] if "日租" in text: rental_modes.append("day") if "周租" in text: rental_modes.append("week") if "月租" in text: rental_modes.append("month") result["rental_modes"] = rental_modes project_match = re.search(r"([\u4e00-\u9fa5A-Za-z0-9]+?)观澜", text) if project_match: result["project_name"] = project_match.group(1) room_match = re.search(r"(\d{3,4})", text) if room_match: result["room_no"] = room_match.group(1) return result if __name__ == "__main__": parsed = parse_house_text(raw_text) print(json.dumps(parsed, ensure_ascii=False, indent=2))输出结果:
{ "rooms": { "bedroom": 3, "living_room": 1, "kitchen": 1, "bathroom": 1, "balcony": 2 }, "beds": [ { "size_m": 1.8, "count": 1 }, { "size_m": 1.5, "count": 1 }, { "size_m": 1.2, "count": 1 } ], "rental_modes": [ "day", "week", "month" ], "project_name": "碧桂园山湖城", "room_no": "2903" }正则re.findall(r"(\d+(?:\.\d+)?)m", text)会把文本里所有带m的数字都找出来。这个简单写法在样例里能正确提取 1.8、1.5、1.2,但如果出现“60m² 面积”这类信息,就会误提取。生产环境的正则需要更严格的上下文判断,比如只提取紧跟在“床”或“+”附近的数值。
这里把房间信息放在rooms对象里,而不是拆成bedroom_count等顶层字段,是为了 JSON 展示更清晰。落库时,还是要把它们映射到house表的独立列。
3.3 输出结果和边界情况处理
解析器必须考虑边界情况。常见问题包括:
- 中文数字:比如“三室一厅”,当前正则匹配不到。
- 分隔符不统一:有人写
1.8m+1.5m+1.2m,有人写1.8m*2,还有人写1.8米和1.5米。 - 房号歧义:
2903可能是 29 层 03 号,也可能是 2 栋 903 室。 - 文本顺序变化:有的文本把“可月租”放在开头,有的放在中间,正则只要定位关键词就不影响。
处理办法不是把所有规则都写进一个巨大的正则,而是先标准化输入,再解析。比如在录入页面规定床尺寸之间用半角加号+分隔,房间数量用阿拉伯数字。对历史数据,可以运行一个一次性清洗脚本,把“三室”转成“3室”,把“1.8米”转成“1.8m”。
关键判断:不要把解析器当成保证数据质量的唯一手段。解析器负责降低录入成本,最终数据质量要靠字段校验、人工确认和定期抽样检查来兜底。
4. 结构化之后才能做发布、计费和房态管理
4.1 生成标准发布文案模板
一旦数据变成结构化,各个发布渠道的展示文案就可以统一生成,不需要各平台分别手写。下面用 Python 生成一套比较标准的发布文案:
def build_publish_text(house: dict) -> str: rooms = house["rooms"] beds = " + ".join( f"{bed['size_m']}m" for bed in house["beds"] ) modes = "/".join(house["rental_modes"]) text = ( f"{house['project_name']} {house.get('block_name', '')}" f"{house.get('building_no', '')}{house.get('room_no', '')}," f"{rooms['bedroom']}室{rooms['living_room']}厅" f"{rooms['kitchen']}厨{rooms['bathroom']}卫{rooms['balcony']}阳台," f"{len(house['beds'])}床:{beds}," f"可{'/'.join(modes)}租,欢迎咨询。" ) return text这样生成的文本可以保持一致性。但要注意,不同平台的标题长度限制和展示规则不一样。有的平台要求标题不超过 50 字,有的平台允许更宽松。模板化只是基础,还要针对每个平台定义字段拼接规则和长度校验。
4.2 日租/周租/月租的计费逻辑
短租系统的计费不是简单地把日租价乘以天数。日租、周租、月租通常有不同的价格体系。比如日租价是 300 元/晚,周租可能是 1800 元/周,月租可能是 6000 元/月。如果只存日租价,再按“月租价 = 日租价 × 30 × 折扣”计算,遇到淡旺季、含水电费、不含清洁费等情况时,就会非常难维护。
推荐做法是直接在每个house_rental_mode记录中保存对应价格和约束。下面是计算接口的伪代码:
def calc_rental_price(house_id: int, mode: str, days: int) -> float: # 从 house_rental_mode 表查询当前模式记录 # 校验 days 是否满足 min_days 和 max_days # day: 日租价 * days # week: 周租价 * (days // 7) + 日租价 * (days % 7) # month: 月租价 * (days // 30) + 日租价 * (days % 30) pass示例只做思路说明。真实项目里还要处理月份不固定问题:28 天、29 天、30 天、31 天都不同。如果要精确计算月租,可以按自然月区间处理,比如从 2025-04-10 到 2025-05-09 算一个月,而不是简单除以 30。
4.3 用日历管理房态和订单冲突
日租、周租、月租混合模式下,订单占用的日期粒度不同。日租订单占用一天,周租订单占用连续 7 天,月租订单可能占用 30 天。如果不把订单展开到日期级别,很容易出现两个订单在时间上重叠,但系统没有发现。
一个简单的房态表设计如下:
CREATE TABLE house_calendar ( house_id BIGINT NOT NULL, date DATE NOT NULL, status VARCHAR(16) NOT NULL, order_id BIGINT, PRIMARY KEY (house_id, date) );当新订单创建后,相关日期写入house_calendar表,状态为BOOKED。检查房源是否可订时,只需要查询目标日期段内是否存在BOOKED记录。月租订单跨月时,要把整个自然月对应的日期全部展开,例如从 4 月 10 日到 5 月 9 日,就写入 30 条日历记录。
这个方案的优点是逻辑简单,缺点是日租、周租、月租比例高的系统,日历表数据量会增长。通常还需要配合索引和分区来优化查询。极端场景下,可以用不展开的区间表加重叠判断,但开发复杂度会更高。
5. 短租系统最常见的三类问题要按链路排查
5.1 床型信息解析错误
现象:录入“1.8m+1.5m+1.2m”,系统只解析出[1.8, 5, 1.2],把1.5当中转文本吃掉了,或者把1.8m+1.5m理解成“1.8 米和 1.5 米”,但最后的1.2m丢失。
可能原因:
- 正则写得太宽松,优先匹配到错误位置。
- 输入分隔符不是统一的加号,混用了全角加号、空格、
/。 - 床尺寸后面带有其他长度信息,比如“1.8m×2m”被误拆。
检查方式:
- 打印正则匹配到的中间结果。
- 准备多组床型用例做单测:
1.8m+1.5m+1.2m、1.8米+1.5米、1.8m*2。 - 检查
source_text是否保留了原始文本。
处理建议:
- 统一录入规范:床尺寸之间只用半角加号。
- 解析后增加人工确认,或在管理后台展示解析出的床型列表。
- 对无法识别的文本不强行解析,而是进入待人工处理队列。
5.2 租期价格配置对不上
现象:租客选择“月租”,系统按“日租价 × 30”计算;或者“周租”跨月时,按自然周分段计价,结果和报价不一致。
可能原因:
- 把租期模式当作布尔字段,只存了“是否支持月租”,没有存月租价格。
- 计费函数里用固定 30 天代表一个月,忽略了不同月份天数不同。
- 价格配置已经有生效日期,但查询时没有过滤
effective_date和expired_date。
检查方式:
- 查看
house_rental_mode表中该房源的mode = 'month'记录。 - 检查计费函数传入的起始日期和结束日期。
- 打印订单快照,看用户下单时看到的报价和最终订单金额是否一致。
处理建议:
- 每个租期模式独立存价格。
- 月租按自然月区间计算,不按 30 天硬算。
- 订单创建时保存价格快照,避免后续改价影响历史订单。
5.3 同一房源多个发布平台状态不一致
现象:运营在后台把房源设置为下架,但外部 OTA 平台仍然展示可预订状态,用户下单后才发现无法入住。
可能原因:
- 平台只做了单向同步,后台改状态后没有触发推送。
- 同一个房源在外部平台有不同 ID,没有建立映射关系。
- 同步任务失败后没有重试和告警。
检查方式:
- 查看
house表的状态字段和外部平台的house_code。 - 查看同步日志中的
last_sync_time、sync_result、error_message。 - 在外部平台搜索该房源,确认实际状态。
处理建议:
- 建立房源平台映射表,保存
platform_house_id。 - 增加同步状态表,记录每次同步任务的执行结果。
- 对同步失败设置重试和告警,避免静默失败。
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 床型解析错误 | 分隔符不统一、正则上下文不严 | 打印正则中间结果,增加单测 | 统一输入规范,解析后人工确认 |
| 月租价格错误 | 没有独立保存月租价格 | 查看租金模式表 | 价格按模式独立存储,按自然月计算 |
| 多渠道状态不一致 | 同步失败且无重试机制 | 查看同步日志和失败记录 | 增加映射表、状态记录和失败告警 |
6. 从学习环境到生产环境,短租房源信息模型怎么落地
6.1 学习环境如何快速跑通
如果是学习或验证想法阶段,不需要一上来就引入复杂框架。可以用 Python 加 SQLite 快速把流程跑通。
python -m venv venv source venv/bin/activate pip install pyyaml然后创建schema.sql,建立house、house_bed、house_rental_mode三张表,再执行解析脚本,把结构化 JSON 写入 SQLite。这样就能看到一条房源文本从字符串到表记录的全过程。
学习环境的重点是验证解析规则和表结构是否合理,不需要过度关注性能。但至少要做到:多次运行解析脚本不会产生重复数据,或者重复数据能被house_code唯一约束拦住。
6.2 生产环境需要补的工程能力
从学习环境到生产环境,差距不只是换一个数据库。以下这些能力需要补齐:
- 地址库和地理编码:把“碧桂园山湖城观澜1街1座2903”解析成标准地址和坐标,或者至少与已有的楼盘字典对齐。
- 房源唯一 ID 生成规则:不要用用户输入文本做主键,要用规则生成并加唯一约束。
- 权限和审核:房东、店长、客服、运营看到的操作按钮不同,房源上架前需要审核。
- 日志和监控:记录解析失败率、同步失败率、订单冲突率,出现异常时能及时告警。
- 回滚和备份:价格、房态、订单表需要保留变更历史,方便追溯。
- 缓存策略:房源详情页不能每次都全表查,需要缓存热点数据,同时通过版本号或事件失效。
- 异常重试:外部平台同步、短信通知、支付回调都要有重试和幂等处理。
6.3 从房源字段出发的可复用清单
最后给一份发布前的可复用检查清单,适合在短租或房源管理系统评审时直接使用。
- 房源基本信息是否完整:小区、楼栋、房号、户型、朝向、楼层。
- 房间字段是否合法:卧室、客厅、厨房、卫生间数量不能为负数,不能超过常见上限。
- 床型是否准确:每张床的尺寸、数量、摆放位置是否清楚,是否预留了沙发床。
- 租期模式是否可计算:日租、周租、月租是否都有独立价格、起租天数、生效时间。
- 联系方式是否按平台规则处理:是否隐藏手机号,是否支持临时虚拟号。
- 房源状态是否正确:上架、下架、停用、审核中是否流转正常。
- 多渠道同步状态是否正常:每个平台的
house_code映射是否存在,最近一次同步是否成功。 - 是否保留原始文本:解析出问题时要能回溯到用户填写的原始内容。
单条房源文本只是短租系统里的一个最小输入样例。真正的价值在于把非结构化文本转换成可计算、可追踪、可复用的数据模型,让日期、床型、价格、房态这些关键信息不再依赖人工阅读。接下来可以继续扩展的方向包括自动识别中文数字、接入地址库、生成多平台适配文案、用日历服务处理订单冲突。如果从这条样例开始动手,建议先用一个本地脚本把原始文本解析成 JSON,再把 JSON 写入 SQLite,最后再考虑完整后台。