RewriteMap 是 mod_rewrite 提供的一类映射指令,它不直接重写 URL,而是定义一张可由 RewriteRule 或 RewriteCond 引用的查找表。语法为 RewriteMap mapname maptype:mapsource,其中 mapname 是后续引用名称,maptype 决定数据来源和查找方式,mapsource 是文件路径、数据库连接信息或外部程序路径。定义之后,可以在规则中用 ${mapname:key} 或 ${mapname:key|defaultvalue} 取值。如果 key 不存在,默认返回空字符串,也可以设置默认值。

理解 RewriteMap 的关键在于它把重写规则的数据部分从逻辑部分中剥离出来。传统做法中,每个重定向都要写一条 RewriteRule,几百条旧链接映射会非常难维护,而且修改后必须重新加载 Apache。使用 RewriteMap 后,逻辑只需要一条规则,映射关系集中在数据源中。对于文本映射和哈希映射,修改映射文件后仍然需要 reload,但至少结构更清晰;对于程序映射,则可以做到每次请求实时计算,完全不用触碰 Apache 配置。
最常见的映射类型是 txt。它的文件格式非常简单,每行一个键值对,中间用至少一个空格或制表符分隔。例如把旧新闻 ID 映射到新路径,文件 oldnews.txt 可以写成:
101 /news/2024/launch 102 /news/2024/security-update 103 /news/2025/release-notes
然后在 VirtualHost 或 Directory 上下文定义映射并引用它:
RewriteMap oldnews txt:/etc/apache2/maps/oldnews.txt
RewriteEngine On
RewriteRule ^/news/old/([0-9]+)/?$ ${oldnews:$1|/news/index} [R=301,L]
这段配置会把 /news/old/101/ 重定向到 /news/2024/launch。如果请求了一个不存在的编号,例如 /news/old/999/,则落到默认值 /news/index。这个默认值机制可以避免空重定向或 500 错误,是生产环境必须利用的特性。
一、文本映射之外的几种高性能类型
txt 映射适合几百到几千条记录,但数据量很大时线性扫描会拖慢请求。Apache 提供了 dbm 类型,它使用哈希数据库存储映射关系。dbm 查找是 O(1) 级别,能轻松支撑几十万甚至上百万条映射。要生成 dbm 文件,需要先用文本文件编写映射,再通过 httxt2dbm 工具转换。假设有一个 redirects.txt:
/product/old-camera /cameras/alpha-7c /product/old-laptop /laptops/thinkpad-x1 /support/2022 /support/archive
执行命令 httxt2dbm -i redirects.txt -o redirects.dbm,会生成一个二进制数据库文件。Apache 配置写成 RewriteMap productmap dbm:/etc/apache2/maps/redirects.dbm。由于 dbm 文件不是纯文本,修改时需要重新生成并 reload,但查询性能远高于 txt。对于访问量较大的站点,这个差异非常明显。
还有一种 rnd 类型用于随机选择,它把一个 key 映射到多个 value 中的一个。语法中每个 value 前可以加权重,权重越大被选中的概率越高。例如让下载请求随机分发到多个镜像节点:
download /mirror1|/mirror2|/mirror3
默认权重相同,Apache 会根据随机数选择。某些版本支持 key value1|value2 这种管道分隔格式,也可写作多行同一 key 自动合并。配合 RewriteRule 可以实现简单的负载均衡,但要注意 rnd 映射不支持变量 key 匹配,只能做固定 key 的随机分发。对于需要按 URL 路径分发的情况,通常用 prg 脚本更灵活。
int 类型是内置的整型映射,支持 toupper、tolower、escape、unescape 四个函数。它不需要外部数据源。比如把路径统一转小写再重写,可以用 RewriteMap lowercase int:tolower,然后 ${lowercase:$1}。escape 会对特殊字符做 URL 编码,这在把路径嵌入查询参数时很有用。
二、程序映射 prg:让规则动态化
prg 类型将一个外部程序作为映射源,Apache 启动时拉起这个程序,并通过标准输入输出与它通信。请求中每次遇到 ${mapname:key} 时,Apache 会把 key 写入程序的标准输入,程序处理后返回一行 value 到标准输出,Apache 再继续处理。这个机制非常强大,因为映射逻辑可以是任意复杂的代码:查数据库、调用内部 API、根据请求头做判断,甚至结合机器学习模型。
脚本必须满足几个约定。它要循环读取 stdin 的每一行,把 key 去掉换行符后处理,再把结果以单行写回 stdout,且每次写回后要 flush。如果脚本崩溃,Apache 会继续尝试,但响应会报错。因此脚本内部要捕获异常并返回默认值。下面是一个 Python 3 示例,它根据传入的用户名后缀决定跳转到不同区域站点:
import sys
def lookup(user):
if user.endswith('_eu'):
return 'https://eu.ipipp.com/profile/' + user
if user.endswith('_ap'):
return 'https://ap.ipipp.com/profile/' + user
return 'https://www.ipipp.com/profile/' + user
def main():
for line in sys.stdin:
key = line.strip()
if not key:
sys.stdout.write('\n')
sys.stdout.flush()
continue
result = lookup(key)
sys.stdout.write(result + '\n')
sys.stdout.flush()
if __name__ == '__main__':
main()
Apache 配置写成 RewriteMap profileprg prg:/usr/local/bin/profile_lookup.py。然后规则中引用:
RewriteEngine On
RewriteMap profileprg prg:/usr/local/bin/profile_lookup.py
RewriteCond %{REQUEST_URI} ^/profile/([a-zA-Z0-9_]+)$
RewriteRule ^/profile/([a-zA-Z0-9_]+)$ ${profileprg:%1|https://www.ipipp.com/profile/notfound} [R=302,L]
这里用 RewriteCond 捕获路径中的用户名并传给 RewriteRule,实际上 RewriteRule 本身的正则已经捕获了 $1,可以直接用 ${profileprg:$1|...}。分开写是为了展示 RewriteCond 也可以参与映射查找。注意 prg 脚本的 stdout 必须带换行符,否则 Apache 会等待直到超时。对于高并发场景,一个 prg 进程可能成为瓶颈,因为默认 Apache 与每个映射程序只维护一个连接。可以通过在脚本内部使用多线程处理来缓解,但要注意输出顺序与输入顺序对应。更稳妥的做法是使用 fastcgi 或数据库映射,但 prg 仍然适合中小规模业务。
另一个容易被忽视的问题:prg 脚本由 Apache 启动,其工作目录和权限与 Apache 子进程相关。如果脚本需要读取配置文件,最好使用绝对路径,并确保运行用户有权限。日志不要写回 stdout,否则会被当成映射结果,应该写到 stderr 或独立日志文件。
三、高级组合技巧与避坑指南
实际项目中很少单独使用一张映射表,更多是组合多张表完成复杂逻辑。例如 URL 大小写不统一时,可以先做小写规范化,再用 dbm 查询旧地址,从而避免因为大小写差异导致映射失效。配置如下:
RewriteEngine On
RewriteMap lower int:tolower
RewriteMap oldurls dbm:/etc/apache2/maps/oldurls.dbm
RewriteRule ^/old/(.*)$ ${oldurls:${lower:$1}|/notfound} [R=301,L]
这段配置里先通过内层 ${lower:$1} 将捕获的路径统一转成小写,再把转写后的字符串作为 key 传给外层 dbm 映射,形成嵌套查找。对于从 Windows 迁移到 Linux 的站点,URL 大小写前后不一致的情况很常见,这个组合能显著减少映射文件里的重复条目。
如果还想在映射不存在时返回 410 而不是跳转到默认页,可以用 RewriteCond 先判断 key 是否存在:
RewriteEngine On
RewriteMap urlmap dbm:/etc/apache2/maps/urlmap.dbm
RewriteCond ${urlmap:%{REQUEST_URI}|__MISS__} !__MISS__
RewriteRule ^/old/(.*)$ ${urlmap:%{REQUEST_URI}} [R=301,L]
RewriteRule ^/old/(.*)$ - [G,L]
这里假设 dbm 文件中的 key 是预先处理过的完整路径,例如 /old/intro。第一段规则在映射存在时执行 301,第二段规则捕获同样的路径并返回 410 Gone。虽然看起来多了一条规则,但语义清晰,也能让搜索引擎更快删除失效页面。需要注意的是 __MISS__ 是自定义哨兵值,保证不会和真实映射值冲突即可。
noescape 选项是另一个值得注意的细节。默认情况下,RewriteMap 在替换 URL 时会转义某些特殊字符,这在大多数场景是合适的。但如果映射的 value 中已经包含百分号编码,比如 %2F,Apache 可能把它再次转义成 %252F,导致目标 URL 出错。定义映射时加上 noescape 可关闭这个行为:RewriteMap mymap dbm:/path/to/map.dbm noescape。该选项虽然不常用,但排查 URL 重写后出现乱码或双重编码问题时应该想到它。
关于映射文件更新,txt 和 dbm 都需要 reload Apache 才能生效。reload 会短暂中断新请求的建立,但比 restart 温和得多。如果采用 prg 脚本,可以修改脚本让它在收到特定 key 或信号时重新加载外部数据,这样完全不用 reload Apache。例如在脚本中每分钟检查一次文件修改时间,发现变化就重新读入。这样兼顾了动态性和性能,但会增加脚本复杂度。
最后一个容易踩的坑是映射 key 的大小写。直接用 ${mapname:key} 是区分大小写的,因为内部查找字符串精确匹配。如果 URL 可能混合大小写,应该先用 NC 标志做不区分大小写的匹配捕获,再用 tolower 等 int 映射把 key 转换成数据库中的统一格式。否则用户输入 /News/Old/101/ 可能匹配不到任何映射。
通过以上几类用法可以把 RewriteMap 从一个简单的文本替换工具升级为 Apache 重写体系里的核心调度组件。遇到成百上千条规则维护困难、频繁修改旧链接映射、需要动态计算目标地址等情况时,优先考虑是否能用 RewriteMap 收敛逻辑,往往能让配置体量减少一个数量级。
Apache RewriteMapURL重写Apache配置修改时间:2026-09-18 12:11:12