简介:geolog-app 是一份基于 Python/Django 开发的地质类 Web 应用源码包,面向初入门的 Web 开发者或想了解 Django 项目结构的读者。压缩包仅 14KB,共 22 个文件,以 14 个 .py 文件为核心,覆盖路由、视图、模型、配置等标准 Django 模块;另含 HTML/CSS 前端页面、requirements.txt 依赖清单与 SQLite 数据库文件,结构精简,便于直接运行和二次修改。目前已有 162 人浏览学习。通过学习这份资源,可以快速掌握一个小型 Django 项目的目录布局、数据库表与视图联动方式,以及地质数据在网页中的展示与交互处理思路,适合作为入门级实战样例。若想从零理解 Python Web 应用的基本工程组织,这份代码包能提供清晰参照。
1. geolog-app 的本质:一套把测井曲线、岩性描述和分层数据揉在一起的 Python 工作流
干地质或者搞过录井的人都有这种体验:甲方发来一个压缩包,里面是几口井的 LAS 文件、几张岩性描述的 Excel 表、还有一堆画得乱七八糟的地质剖面图。资料是齐的,但格式是乱的,换一口井就要重新整理一次数据,等真正开始画图、算储量的时候,时间已经耗掉一大半。geolog-app 就是朝这个痛点来的一个 Python 项目,它把测井曲线解析、岩性录井表清洗、地层分层和剖面图绘制串成了一条完整的本地工作流,目标用户是地质工程师、录井技术人员和刚转行写脚本的石油专业学生。它不是用来做精细解释或建模的大型平台,而是让你手里那些"半结构化"的地质数据变得规整、可复现、能批量处理的一个工具包。适合的场景很明确:数据整理阶段反反复复、出图效率低、每口井的处理流程都不一致。
这个项目最大的特点是"用脚本替代手工整理"。代码思路非常符合工程师的直觉——先定数据标准,再做转换,最后输出图纸和统计表。全文按照"数据从哪来 → 怎么清洗 → 如何出图 → 哪些坑会卡住你"的顺序展开,下面每一步都会给出可以直接改用的代码。
2. 测井曲线等深度重采样:把乱糟糟的 LAS 文件变成统一深度域
2.1 为什么要先做深度对齐
拿到 LAS 文件后第一件事永远不是画图,而是检查深度采样。不同仪器、不同井段测出来的深度步长经常不一样,有的是 0.1 米一个点,有的是 0.125 米,还有的是 0.2 米。如果直接把两条曲线叠在一起画,分辨率不匹配会导致后面分层时出现错位,最典型的表现就是同一套地层的边界在 GR 曲线上是一个深度,在电阻率曲线上又差了几十厘米。这种情况手工调整很痛苦,但用脚本做等深度重采样却是十分钟的事。
geolog-app 里的思路很简单:选定一个基准深度序列,然后对每一条曲线做线性插值。常见的做法是选步长最小、起始深度最早的那条曲线作为基准,这样原始信息不丢失。比如基准是 1000 米到 2000 米、步长 0.1 米,另一条曲线步长 0.2 米,那就把后者插值到 0.1 米的网格上。
2.2 使用 lasio 读取并统一采样网格
这里用到一个广泛使用的测井数据解析库——lasio,它是读取 LAS 文件的事实标准工具之一,能够处理 LAS 2.0 和 3.0 格式,也能处理常见的编码问题。
import lasio import pandas as pd import numpy as np def resample_las_to_grid(las_path, step=0.1): # 读取 LAS 文件,lasio 会自动识别曲线名和深度列 las = lasio.read(las_path) # 取出原始深度序列 depth_old = las.index # 构造目标深度网格:从顶深到底深,步长 step depth_new = np.arange(depth_old[0], depth_old[-1] + step, step) # 对每一条曲线做线性插值 df_resampled = pd.DataFrame({'DEPTH': depth_new}) for curve in las.curves: curve_name = curve.mnemonic # 跳过深度列本身 if curve_name.upper() in ('DEPT', 'DEPTH'): continue values = las[curve_name] # np.interp 是 numpy 的线性插值,要求 x 递增 df_resampled[curve_name] = np.interp(depth_new, depth_old, values) return df_resampled # 示例:对一口井的 LAS 做 0.1 米重采样 df_one_well = resample_las_to_grid('well_01.las', step=0.1) print(df_one_well.head())这段代码的核心是两个函数:lasio.read负责把 LAS 文件里的文本元数据和曲线数值读成 Python 对象,np.interp负责把离散的采样点映射到新的网格上。参数上需要注意step的取值,不要小于原始数据的最小采样间隔,否则插值等于在猜数据,没有新增任何地下信息。如果原始数据是 0.2 米采样,你却重采样到 0.05 米,除了文件变大之外没有任何意义。
2.3 曲线名统一与单位检查
重采样只是第一步,真正让人头疼的是不同井的曲线命名规则不一样。某口井的 GR 曲线叫GR,另一口井叫GR_R,还有一口井直接叫GAPI,你要是写死了一个名字,换井就崩。geolog-app 里维护了一个别名映射表,建议你本地也建一个。
curve_alias = { 'GR': ['GR', 'GR_R', 'GAPI', 'GR_EDTC'], 'RT': ['RT', 'RES', 'LLD', 'RT_DEEP'], 'DEN': ['DEN', 'RHOB', 'DEN_COMP'], 'CN': ['CN', 'NPHI', 'CNC'], } def normalize_curve_name(raw_name): # 先转大写,再去掉特殊符号 key = raw_name.strip().upper().replace('-', '_') for standard, aliases in curve_alias.items(): if key in aliases: return standard return key # 没匹配上就保留原名这个函数逻辑很直接:做成一个字典,键是标准名,值是别名列表。匹配时先把原始曲线名转大写、去掉连字符,再逐一比对。实际使用中你还需要把 LAS 文件里的单位和深度基准加进一个 Excel 元数据表里,方便后续跟踪——哪个文件是哪口井、谁的曲线被重命名了、单位是什么。这些信息不写下来,三个月以后你自己都记不清。
3. 岩性录井表清洗:从"人写的"变成"机器认的"
3.1 自由文本岩性描述的坑
岩性录井表是地质数据里最"脏"的部分。录井人员填写的时候经常用简称,比如"细砂岩"写成"细砂"、"粉砂质泥岩"写成"粉泥",同一个层段的描述在不同井里风格完全不一致。更麻烦的是,一张表里可能是多井合并导出,A 井用的是中文,B 井是英文缩写,C 井还有拼音首字母缩写。直接把这些文本当成数据用,后面画图时分类统计就是灾难。
3.2 构建岩性字典实现标准化映射
geolog-app 的做法是把岩性描述映射到一个标准分类集合上,而不是试图理解复杂的自然语言。这个思路更务实:录井岩性描述虽然口语化,但实际岩性大类是有限的,比如泥岩、砂岩、粉砂岩、石灰岩、白云岩、膏岩等,加上少量过渡类型就够了。
import re litho_dict = { '泥岩': ['泥岩', '泥', 'MUD', 'CLAYSTONE', 'MUDST'], '粉砂质泥岩': ['粉砂质泥岩', '粉泥', 'SILTY MUDST'], '泥质粉砂岩': ['泥质粉砂岩', '泥粉', 'MUDDY SILTST'], '粉砂岩': ['粉砂岩', '粉砂', 'SILTSTONE', 'SILT'], '细砂岩': ['细砂岩', '细砂', 'FINE SAND', 'F.SAND'], '中砂岩': ['中砂岩', '中砂', 'MED SAND', 'M.SAND'], '粗砂岩': ['粗砂岩', '粗砂', 'COARSE SAND', 'C.SAND'], '石灰岩': ['石灰岩', '灰岩', 'LIMESTONE', 'LIME'], '白云岩': ['白云岩', 'DOLOMITE', 'DOLO'], } def normalize_litho(raw_text): if not isinstance(raw_text, str): return 'UNKNOWN' # 去掉数字、空格和特殊符号 text = re.sub(r'[\d\s\(\)()·]', '', raw_text).upper() for standard, aliases in litho_dict.items(): for alias in aliases: if alias.upper() in text: return standard return 'UNKNOWN'逻辑上这段代码的关键点是"包含匹配"而不是"完全相等"。因为原始描述经常是"浅灰色细砂岩"或者"灰绿色泥岩夹薄层粉砂岩",你期望的是提取出主岩性。把标准词别名做成列表的好处是一旦遇到新的叫法,只需要往列表里加一行,不需要改任何逻辑。但也要注意,这种映射策略只能应对"主岩性单一"的情况,如果一段描述里有两种岩性,它会返回第一个匹配到的结果,后面你还需要决定是取主还是取次——这是有歧义的部分,我一般在映射之后加一道人工抽查。
3.3 深度区间合并与空白段补全
录井表通常是不等间距的,比如 1000.0 到 1002.5 米是泥岩,1002.5 到 1007.0 是细砂岩,深度连续但分段长度不一。清洗后的数据要转成连续的深度域,方便和测井曲线深度对齐。
def merge_litho_intervals(df, depth_start_col='depth_top', depth_end_col='depth_bottom', litho_col='litho_std'): # 先按顶深排序 df_sorted = df.sort_values(depth_start_col).reset_index(drop=True) merged = [] for _, row in df_sorted.iterrows(): if merged and merged[-1][litho_col] == row[litho_col] and merged[-1][depth_end_col] >= row[depth_start_col]: # 相邻且岩性相同,合并区间,取最大底深 merged[-1][depth_end_col] = max(merged[-1][depth_end_col], row[depth_end_col]) else: merged.append(row.to_dict()) return pd.DataFrame(merged)这里的逻辑是相邻检查:如果当前区间的顶深小于等于上一个区间的底深,同时岩性相同,就把两个区间合并成一个,底深取较大值。这个步骤会消除录井表里因为人工分段造成的"同一个岩性被拆成好几段"的问题。合并之后建议检查一下有没有空隙段,如果录井表的深度和测井曲线的深度范围不一致,后期画图会出现断层。
4. 地质剖面图绘制:matplotlib 出图与四个常见排查项
4.1 为什么不用专业绘图软件非要自己画
很多单位买了商业地质软件,但实际用起来有门槛——学习成本高、许可证不在手、投影方式不灵活。geolog-app 选择用 matplotlib 做剖面图并不寒酸,它的好处在于:所有图件直接从清洗后的 DataFrame 生成,换参数重跑一遍就行,不需要在软件界面上手动挪井位、改刻度。而且脚本出图,图例、颜色、深度比例都处于可控状态,领导要改配色,改一个字典重跑,而不是在软件里一层层点菜单。
4.2 深度-岩性柱状图的核心代码
按标准化后的岩性分类填色,深度轴从顶到底,右侧画岩性柱,左轴叠上 GR 曲线,这是最常见的一版图件。
import matplotlib.pyplot as plt from matplotlib.patches import Rectangle litho_color = { '泥岩': '#4C4C4C', '粉砂质泥岩': '#8C8C8C', '泥质粉砂岩': '#BEBEBE', '粉砂岩': '#E8D154', '细砂岩': '#F5DEB3', '中砂岩': '#D2B48C', '粗砂岩': '#A0522D', '石灰岩': '#87CEEB', '白云岩': '#FFA500', } def plot_well_profile(ax_depth, well_df, litho_df, gr_curve_name='GR'): # 左轴:画 GR 曲线 ax_depth.plot(well_df['DEPTH'], well_df[gr_curve_name], color='blue', lw=0.8, label='GR') ax_depth.set_xlabel('GR (API)') ax_depth.invert_yaxis() # 深度轴必须反转,浅在上、深在下 ax_depth.set_ylabel('DEPTH (m)') # 右轴:叠岩性柱 ax_litho = ax_depth.twiny() for _, interval in litho_df.iterrows(): top = interval['depth_top'] bottom = interval['depth_bottom'] litho = interval['litho_std'] # Rectangle 的坐标是 (y_left_bottom, width, height) rect = Rectangle((0, top), 1, bottom - top, facecolor=litho_color.get(litho, '#CCCCCC')) ax_litho.add_patch(rect) ax_litho.set_xlim(0, 1) ax_litho.set_xticks([]) ax_litho.set_xlabel('LITHOLOGY') plt.show() # 示例调用 fig, ax_depth = plt.subplots(figsize=(6, 12)) plot_well_profile(ax_depth, df_one_well, df_merged_litho)这里的核心图形学逻辑是:左边一个正常轴用来画 GR 曲线,twiny()复制出一个共享 y 轴的叠加轴,加矩形块来画岩性。参数上要注意Rectangle的第一个坐标元组是左下角,y 方向是深度,所以top要放在 y 的起点位置,高度是bottom - top。invert_yaxis一定不能省略,否则整个图上下颠倒,地层顺序完全反掉。
4.3 常见排查项一:GR 曲线反向叠加时被岩性柱遮挡
现象:曲线画在下面,岩性色块把曲线盖住了,根本看不出来对应关系。 原因:绘图顺序是先画曲线、后画矩形,默认后绘制的对象覆盖先绘制的对象。 解决:在ax_litho.add_patch(rect)前给矩形设置透明度,或者调整 zorder。推荐透明度:
rect = Rectangle((0, top), 1, bottom - top, facecolor=litho_color.get(litho, '#CCCCCC'), alpha=0.5, zorder=1) ax_depth.plot(..., zorder=3)4.4 常见排查项二:深度范围不重合导致剖面缺段
现象:录井表的顶深比测井曲线起始深度浅,导致曲线有一段空白。 原因:录井表和测井曲线的深度起始点不一致,比如 LAS 文件是从 1000 米开始测的,但录井表从 990 米就有描述。 解决:在画图前先取交集深度范围,直接裁剪数据。
top_limit = max(well_df['DEPTH'].min(), litho_df['depth_top'].min()) bottom_limit = min(well_df['DEPTH'].max(), litho_df['depth_bottom'].max()) well_df = well_df[(well_df['DEPTH'] >= top_limit) & (well_df['DEPTH'] <= bottom_limit)] litho_df = litho_df[(litho_df['depth_bottom'] >= top_limit) & (litho_df['depth_top'] <= bottom_limit)]4.5 常见排查项三:中文标签乱码
现象:图例和坐标轴中文显示成方块。 原因:matplotlib 默认字体不含中文字符。 解决:在代码开头指定一个系统已有的中文字体。常见做法是:
plt.rcParams['font.sans-serif'] = ['SimHei'] # 或者 ['Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False如果字体指定后仍然报错找不到字族,可以用matplotlib.font_manager列出所有可用字体名,再挑一个实际的加载进去,不要硬抄网上的配置。
4.6 常见排查项四:图例重复
现象:同一套图用循环画了很多条曲线,图例上出现几十行重复的 GR。 原因:label参数被赋给了循环里的每一条曲线,但曲线名称相同,导致图例合并逻辑失效。 解决:只在第一个数据项上加 label,其余不设,或者画完后用ax.legend().set_visible(False)手动关掉。
for idx, well_name in enumerate(well_list): if idx == 0: ax.plot(depth, curve, label='GR') else: ax.plot(depth, curve)这四条排查项是我实际绘图时最常遇到的翻车现场,尤其是透明度遮挡和深度范围裁剪,属于看起来不难但第一次画必错的点。
5. 进阶技巧:多井批量成图与地层对比图的坐标轴联动
5.1 批量处理多口井:用配置清单驱动绘图
当手头有三口井以上的数据时,靠一份脚本反复改路径的方式效率太低,我一般会用一份 CSV 配置清单来驱动整个绘图流程。每一行就是一个井号,记录它的 LAS 文件路径、录井表路径、画图颜色、显示顺序。脚本只需要读这份清单,然后循环调用上面写过的resample_las_to_grid和plot_well_profile,输出到独立的图片文件。
import pandas as pd config_df = pd.read_csv('wells_config.csv', encoding='gbk') for _, row in config_df.iterrows(): las_path = row['las_path'] litho_path = row['litho_path'] well_name = row['well_name'] df_curve = resample_las_to_grid(las_path, step=0.1) df_litho = pd.read_excel(litho_path) df_litho['litho_std'] = df_litho['litho_description'].map(normalize_litho) df_litho = merge_litho_intervals(df_litho) fig, ax = plt.subplots(figsize=(6, 12)) plot_well_profile(ax, df_curve, df_litho) plt.title(well_name) plt.savefig(f'profile_{well_name}.png', dpi=300, bbox_inches='tight') plt.close()encoding='gbk'这个参数不能省,很多本地 Excel 导出 CSV 文件默认是 GBK 编码,用默认 utf-8 读会被乱码卡住。bbox_inches='tight'保证保存的图片四周不留大片白边,这个参数决定了打印出来是否美观。
5.2 地层对比图:多井深度轴联动
单井剖面图之外,geolog-app 最实用的场景是多井对比图。做法是横向排列若干口井,各井都有自己的深度轴,同时把同一个标志层的底界深度连成一条等时线。多井对比图的关键在于每个子图的 y 轴刻度必须一致,否则地层对应关系在视觉上是扭曲的。
fig, axes = plt.subplots(1, N, figsize=(4*N, 14), sharey=True) for ax, well_name in zip(axes, well_names): df = well_data[well_name] ax.plot(df['GR'], df['DEPTH'], color='blue', lw=0.6) ax.invert_yaxis() ax.set_title(well_name)sharey=True是参数设置的核心,它让所有子图共享同一个深度轴刻度。如果没有这个参数,各井因为深度范围不同会自动错开刻度,对比图就从"科学对比"退化成"形状比对",这就失去了地层对比的意义。对比图上还可以额外加一个横向色带当标志层,用ax.axhspan(1005, 1012, color='#FFD700', alpha=0.3)画一个半透明带子,提示地层对比的参考意义。
5.3 加入"复核清单"的工程习惯
把所有中间输出的 CSV 文件、图件整理成一个固定目录结构,并在跑完脚本后自动生成一个简单的复核报告。这个报告只列两件事:哪些井输出成功,哪些井出现了 UNKNOWN 岩性(映射失败)或深度不匹配警告。
report_lines = [] for well in well_list: unknown_count = len(data[well][data[well]['litho_std'] == 'UNKNOWN']) if unknown_count > 0: report_lines.append(f'{well}: {unknown_count} 段岩性未识别') if report_lines: with open('check_report.txt', 'w') as f: f.write('\n'.join(report_lines))这个习惯非常有效,它把问题从"画图之后肉眼看"前移到"跑完脚本顺手看报告"。每次拿到一批新井数据,我都强制自己走一遍清洗、成图、看报告这个流程,能省掉不少熬夜调图的后悔药,希望帮到你。
本文还有配套的精品资源,点击获取