校验字符串只允许包含问号、0和1,这个需求看起来简单,但在接口参数校验、协议字段解析和输入过滤中经常出现。只要忽略一个边界条件,就可能导致非法字符混入,甚至引发下游逻辑异常。实现方式不复杂,关键在于把规则定义清楚:字符串里的每一个字符都必须属于问号、数字0或数字1,除此之外的任何字符都应被拒绝。

需求拆解与白名单校验思路
首先要明确校验对象中“问号”指的是标准ASCII字符,即十进制63对应的?,而不是中文全角问号?。全角问号在Unicode中是另一个码点,如果业务要求严格,必须显式排除。数字0和1同样只允许半角字符,禁止字母O、字母l等形近字符混入。空字符串是否合法取决于具体场景:如果字段必填,应拒绝空串;如果允许空值,则可设计为可选校验。
推荐使用白名单策略进行校验,也就是只放行明确允许的字符,而不是试图枚举所有非法字符。黑名单永远列不全,尤其是面对不可见字符、控制字符和Unicode扩展字符时。白名单可以简单实现为判断每个字符是否等于?、0或1,也可以用集合结构统一管理允许字符集。下面先给出一个伪代码级别的判断逻辑,帮助理清流程:遍历输入字符串,逐字符比较,遇到不允许的字符立即返回失败,全部通过才返回成功。
正则表达式方案:简洁但要注意细节
正则表达式是处理这类校验最常用的工具。模式^[?01]+$表示从字符串开头到结尾,字符只能是?、0或1,且至少出现一次。如果允许空字符串,可以把加号换成星号,写成^[?01]*$。锚点^和$必须成对出现,否则正则可能只匹配到子串,导致整串校验失效。
在Java中,可以配合Pattern预编译来提升性能。以下是一个完整的工具方法示例:
import java.util.regex.Pattern;
public class StringValidator {
private static final Pattern PATTERN = Pattern.compile("^[?01]+$");
public static boolean isValid(String input) {
if (input == null) {
return false;
}
return PATTERN.matcher(input).matches();
}
}
Python里的实现更直观,用re.fullmatch可以避免手动写锚点。注意fullmatch要求整个字符串匹配,不能只匹配一部分。示例代码:
import re
PATTERN = re.compile(r'^[?01]+$')
def is_valid(input_str: str) -> bool:
if input_str is None:
return False
return bool(PATTERN.fullmatch(input_str))
JavaScript在浏览器或Node环境中同样可以使用正则。由于JavaScript正则字面量创建成本较低,但仍建议在模块顶层定义好正则对象,避免在循环中反复创建。下面是校验函数:
const VALID_PATTERN = /^[?01]+$/;
function isValid(input) {
if (typeof input !== 'string') {
return false;
}
return VALID_PATTERN.test(input);
}
正则实现虽然简洁,但要注意?在字符类内部不需要转义,如果写在字符类外面,?是量词元字符,含义完全不同。例如错误写法^?01+$根本无法表达仅允许这三种字符。另外,如果输入字符串很长,正则引擎的匹配效率通常足够,但极端情况下仍可考虑非正则方案。
手动遍历校验:更直观且可控
对于字符集很小的情况,手动遍历字符串往往更直白,也更容易调试。每个字符取出来,直接判断是否属于允许集合。Java中可以用char比较,也可以借助indexOf判断。下面这段代码使用最简单的字符比较,时间复杂度为O(n):
public static boolean isValidByLoop(String input) {
if (input == null || input.isEmpty()) {
return false; // 按业务需求调整空串策略
}
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c != '?' && c != '0' && c != '1') {
return false;
}
}
return true;
}
如果允许的字符集稍大一些,比如扩展为?、0、1、a、b,可以改用Set来维护白名单。这样增加或删除字符时不需要修改判断逻辑,可读性更好。Java的HashSet或BitSet都可以,对于ASCII字符范围,BitSet的查找效率更高。下面是用BitSet实现的校验器:
import java.util.BitSet;
public class FlexibleValidator {
private final BitSet allowed = new BitSet(128);
public FlexibleValidator() {
allowed.set('?');
allowed.set('0');
allowed.set('1');
}
public boolean isValid(String input) {
if (input == null) {
return false;
}
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c >= 128 || !allowed.get(c)) {
return false;
}
}
return true;
}
}
手动遍历方案的优势在于完全透明,字符级别的判断逻辑可以随意扩展。比如需要区分大小写、需要跳过某些位置、需要统计字符出现次数时,遍历方案比正则更容易插入自定义逻辑。缺点是代码量稍多,如果校验规则频繁变化,维护成本会高于正则。
常见误区和安全注意事项
一个典型误区是使用String.contains来判断整个字符串是否合法。例如写input.contains("?")和input.contains("0"),这只能说明字符串中至少包含这些字符之一,无法保证字符串里没有其他字符。非法字符串如?0x1也会通过这类判断,因此必须逐个字符校验。
另一个问题是忽略null和类型检查。如果参数可能为null,先判断空值能避免NullPointerException。在Java以外的动态类型语言中,还要确认传入的是字符串,否则调用正则或遍历方法可能直接抛异常。测试用例应当覆盖空串、纯合法字符、包含非法字符、包含空格、包含全角问号、包含控制字符等边界情况,确保校验逻辑稳定。
安全层面,输入校验不能只依赖前端,后端必须独立执行同样的规则。尤其是当这个字符串会参与数据库查询、文件路径拼接或命令执行时,严格限制字符集能够显著降低注入风险。虽然仅允许?、0、1本身已经很安全,但仍需防止后续业务把这些字符解释成通配符或协议关键字,必要时在业务层再做一层转义或语义校验。
性能对比与选型建议
对于长度在几十到几百字符的普通输入,正则和手动遍历的性能差异可以忽略不计。但在高并发接口中,如果校验逻辑被频繁调用,预编译正则和手动遍历都能达到很高的吞吐量。Java中Pattern.matcher().matches()会创建Matcher对象,虽然开销很小,但手动遍历只操作基本类型char,通常更快。
如果字符串长度非常长,比如超过几万字符,手动遍历的优势会更明显,因为正则引擎需要维护状态,而遍历只是线性扫描。不过大多数实际业务中,输入长度受限于协议或框架,一般不会达到这个量级。选型时优先考虑可读性和团队习惯:如果团队对正则熟悉,使用正则方案;如果需要更强的字符级控制和调试能力,使用遍历方案。
无论选择哪种实现,都建议把校验逻辑封装成独立方法或工具类,提供清晰的命名和单元测试。这样后续调整规则时,只需要修改一处,不会散落在多个控制器或服务中。校验函数应当保持无状态,方便并发调用。
总结
校验字符串仅允许包含问号、0和1,本质上是一个字符白名单匹配问题。正则表达式^[?01]+$可以快速完成整串校验,但必须注意锚点和元字符的使用。手动遍历方案更直观,适合需要精细控制字符逻辑的场景。实际开发中还要处理空值、全角字符、类型检查等边界情况,并用单元测试覆盖关键路径。
最终选择正则还是遍历,取决于具体项目的技术栈和团队维护习惯。只要规则明确、实现完整,两种方案都能保证只有合法的输入进入后续流程。希望本文的代码示例和误区分析能帮助你在实际项目中少走弯路。