当前位置:首页>python>Python 字符串专项练习:6 道编程题从入门到精通

Python 字符串专项练习:6 道编程题从入门到精通

  • 2026-10-11 07:29:01
Python 字符串专项练习:6 道编程题从入门到精通

配套专栏:Python 全栈修炼之路 第 03 篇《字符串 —— 不只是文本那么简单》

难度分布:⭐ → ⭐⭐ → ⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐⭐

核心覆盖:切片、常用方法、f-string 格式化、正则表达式、回文判断、编码解码、不可变性


题目一:回文判断 ⭐

📌 题目描述

编写函数 is_palindrome(s),判断字符串是否为回文(正读和反读相同)。需忽略大小写和非字母数字字符。

输入:"A man, a plan, a canal: Panama"输出:True解释:去掉非字母数字并转小写后为 "amanaplanacanalpanama",是回文输入:"race a car"输出:False

💡 编程思路

回文判断是字符串入门经典题,核心是清理 + 反转比较:

  • 方法一(切片反转)
    :先过滤非字母数字字符并转小写,然后用 s[::-1] 反转,比较是否相等。一行搞定,最 Pythonic。
  • 方法二(双指针)
    :左右两个指针从两端向中间移动,跳过非字母数字字符,逐个比较。无需创建新字符串,空间 O(1)。

方法一简洁直观,方法二更节省内存,面试中两种都应掌握。

🖥️ 参考代码

def is_palindrome_slice(s):    """方法一:切片反转 —— O(n) 时间,O(n) 空间"""    # 过滤:只保留字母和数字,转小写    cleaned = ''.join(ch.lower() for ch in s if ch.isalnum())    return cleaned == cleaned[::-1]def is_palindrome_two_pointer(s):    """方法二:双指针 —— O(n) 时间,O(1) 空间"""    left, right = 0, len(s) - 1    while left < right:        # 跳过非字母数字字符        while left < right and not s[left].isalnum():            left += 1        while left < right and not s[right].isalnum():            right -= 1        # 比较(忽略大小写)        if s[left].lower() != s[right].lower():            return False        left += 1        right -= 1    return True# 测试if __name__ == "__main__":    test_cases = [        ("A man, a plan, a canal: Panama", True),        ("race a car", False),        ("", True),        (" ", True),        ("0P", False),        ("Was it a car or a cat I saw?", True),        ("12321", True),        ("12345", False),    ]    for s, expected in test_cases:        r1 = is_palindrome_slice(s)        r2 = is_palindrome_two_pointer(s)        assert r1 == expected, f"slice方法失败: {s}"        assert r2 == expected, f"双指针方法失败: {s}"        print(f"  '{s[:30]}...' → {r1} ✓")

🔗 关联知识点

知识点
说明
s[::-1]
切片反转字符串的经典技巧
str.isalnum()
判断是否为字母或数字
str.lower()
转小写
''.join(generator)
高效拼接字符串
双指针
空间 O(1) 的原地比较

题目二:字符串反转(保持单词顺序) ⭐⭐

📌 题目描述

编写函数 reverse_words(s),反转字符串中每个单词的字符顺序,但保持单词之间的相对顺序不变。

输入:"Hello Python World"输出:"olleH nohtyP dlroW"输入:"  the sky is blue  "输出:"  eht yks si eulb  "

要求:保留原始空格(包括前导、尾部和单词间的多个空格)。

💡 编程思路

这道题考察的是精确的字符串分割与重组:

  • 方法一(split + 切片反转)
    :用 split(' ') 按单个空格分割(保留空字符串),对每个非空片段反转,再用 ' '.join() 拼接。注意 split(' ') 和 split() 的区别:前者保留空串,后者丢弃所有空白。
  • 方法二(正则分割)
    :用 re.split(r'(\s+)', s) 按空白分组,奇数位是分隔符,偶数位是单词。只反转偶数位的单词,保留分隔符不变。

方法二更精确,能完美保留原始空格格式。

🖥️ 参考代码

import redef reverse_words_split(s):    """方法一:split + 切片反转"""    # split(' ') 保留空串,split() 丢弃空白    parts = s.split(' ')    reversed_parts = [part[::-1] for part in parts]    return ' '.join(reversed_parts)def reverse_words_regex(s):    """方法二:正则分割,精确保留空白"""    # 按连续空白分组,保留分隔符    groups = re.split(r'(\s+)', s)    # 偶数索引是单词,奇数索引是空白    for i in range(0, len(groups), 2):        groups[i] = groups[i][::-1]    return ''.join(groups)# 测试if __name__ == "__main__":    print(reverse_words_split("Hello Python World"))    # "olleH nohtyP dlroW"    print(reverse_words_split("  the sky is blue  "))    # "  eht yks si eulb  "    print(reverse_words_regex("  the sky   is blue  "))    # "  eht yks   si eulb  "    # 边界测试    print(reverse_words_split(""))       # ""    print(reverse_words_split("a"))      # "a"    print(reverse_words_split("  "))     # "  "

🔗 关联知识点

知识点
说明
split(' ')
 vs split()
前者保留空串,后者丢弃所有空白
s[::-1]
单词级反转
' '.join()
用空格连接
re.split(r'(\s+)', s)
分组分割,保留分隔符
列表推导式
批量反转每个单词

题目三:手机号脱敏与信息提取 ⭐⭐

📌 题目描述

编写函数 process_user_info(text),对文本中的手机号进行中间四位脱敏(替换为 ****),同时提取文本中的所有邮箱地址。

输入:"""联系方式:13812345678,备用13987654321。邮箱:zhangsan@example.com 和 lisi@test.org工作邮箱:wangwu@company.cn"""输出:脱敏后文本:"联系方式:138****5678,备用139****4321。\n邮箱:zhangsan@example.com 和 lisi@test.org\n工作邮箱:wangwu@company.cn"提取的邮箱:["zhangsan@example.com", "lisi@test.org", "wangwu@company.cn"]

💡 编程思路

这道题综合考察正则表达式的匹配、分组替换和查找:

  1. 手机号脱敏
    :用 re.sub() + 捕获分组(\d{3})\d{4}(\d{4}),替换为 \1****\2。分组让我们保留前3位和后4位。
  2. 邮箱提取
    :用 re.findall() 匹配邮箱格式 \w+@\w+\.\w+。更严谨的写法是 [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}。

🖥️ 参考代码

import redef mask_phone_numbers(text):    """手机号中间四位脱敏"""    # (\d{3})\d{4}(\d{4}) → 分组1 + **** + 分组2    pattern = r'(\d{3})\d{4}(\d{4})'    return re.sub(pattern, r'\1****\2', text)def extract_emails(text):    """提取所有邮箱地址"""    pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'    return re.findall(pattern, text)def process_user_info(text):    """综合处理:脱敏 + 提取"""    masked_text = mask_phone_numbers(text)    emails = extract_emails(text)    return masked_text, emails# 测试if __name__ == "__main__":    text = """    联系方式:13812345678,备用13987654321。    邮箱:zhangsan@example.com 和 lisi@test.org    工作邮箱:wangwu@company.cn    """    masked, emails = process_user_info(text)    print("=== 脱敏后文本 ===")    print(masked)    print("\n=== 提取的邮箱 ===")    for email in emails:        print(f"  {email}")    # 更多测试    assert mask_phone_numbers("13812345678") == "138****5678"    assert mask_phone_numbers("电话13812345678号") == "电话138****5678号"    print("\n所有断言通过 ✓")

🔗 关联知识点

知识点
说明
re.sub(pattern, repl, text)
正则替换
捕获分组 () + \1 \2
保留匹配的部分内容
re.findall(pattern, text)
提取所有匹配
邮箱正则
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
手机号正则
(\d{3})\d{4}(\d{4})
 分组捕获

题目四:简易模板引擎 ⭐⭐⭐

📌 题目描述

实现一个简易字符串模板引擎,支持 {{变量名}} 占位符替换。

template = "你好,{{name}}!你有 {{count}} 条未读消息,来自 {{source}}。"data = {"name": "张三", "count": 5, "source": "CSDN"}输出:"你好,张三!你有 5 条未读消息,来自 CSDN。"

扩展要求:

  • 支持默认值:{{name|未知用户}},变量不存在时使用默认值
  • 支持格式化:{{price|%.2f}},对数值进行格式化
  • 未匹配的占位符保持原样

💡 编程思路

这道题考察正则表达式匹配 + 动态替换:

  1. 用 re.sub() 的替换函数模式(传入 callable),对每个匹配到的占位符动态处理。
  2. 正则模式 {{(\w+)(?:\|([^}]+))?}}: 
    • (\w+)
       捕获变量名
    • (?:\|([^}]+))?
       可选地捕获 | 后的默认值/格式化串
  3. 替换函数中,先从 data 字典取值,取不到则用默认值,再尝试格式化。

🖥️ 参考代码

import reclass TemplateEngine:    """简易模板引擎 —— 支持 {{变量}} 和 {{变量|默认值}} 语法"""    # 匹配 {{变量名}} 或 {{变量名|默认值/格式}}    pattern = re.compile(r'\{\{(\w+)(?:\|([^}]+))?\}\}')    @classmethod    def render(cls, template, data):        """渲染模板"""        def replacer(match):            var_name = match.group(1)     # 变量名            default = match.group(2)       # 默认值/格式            # 从数据中取值            value = data.get(var_name)            if value is None:                # 变量不存在,使用默认值                if default is not None:                    return default                return match.group(0)  # 保持原样            # 尝试格式化(默认值是格式化字符串,如 %.2f)            if default is not None:                try:                    return default % value                except (TypeError, ValueError):                    pass            return str(value)        return cls.pattern.sub(replacer, template)# 测试if __name__ == "__main__":    # 基础用法    template1 = "你好,{{name}}!你有 {{count}} 条未读消息。"    data1 = {"name": "张三", "count": 5}    print(TemplateEngine.render(template1, data1))    # 你好,张三!你有 5 条未读消息。    # 默认值    template2 = "用户:{{name|访客}},积分:{{score|0}}"    data2 = {"name": "李四"}  # score 不存在    print(TemplateEngine.render(template2, data2))    # 用户:李四,积分:0    # 格式化    template3 = "商品:{{item}},价格:¥{{price|%.2f}},折扣:{{discount|%.0f%%}}"    data3 = {"item": "Python教程", "price": 99.5, "discount": 0.85}    print(TemplateEngine.render(template3, data3))    # 商品:Python教程,价格:¥99.50,折扣:85%    # 未匹配保持原样    template4 = "Hello {{name}}, {{unknown_var}} is not replaced."    print(TemplateEngine.render(template4, {"name": "World"}))    # Hello World, {{unknown_var}} is not replaced.

🔗 关联知识点

知识点
说明
re.sub(pattern, callable, text)
传入函数进行动态替换
正则分组 ()
捕获变量名和默认值
非捕获组 (?:...)
分组但不捕获
dict.get(key)
安全取值
str % value
% 格式化

题目五:罗马数字转整数 ⭐⭐⭐

📌 题目描述

罗马数字包含以下七种字符:I(1), V(5), X(10), L(50), C(100), D(500), M(1000)。

编写函数 roman_to_int(s),将罗马数字字符串转换为整数。

输入:"III"       输出:3输入:"IV"        输出:4输入:"IX"        输出:9输入:"LVIII"     输出:58    (50 + 5 + 3)输入:"MCMXCIV"   输出:1994  (1000 + 900 + 90 + 4)

💡 编程思路

罗马数字的规则:通常小的数字在大的数字右边(如 VI = 6),但特殊情况小的在左边表示减法(如 IV = 4, IX = 9)。

核心逻辑:从左到右遍历,如果当前值 < 下一个值,则减去当前值(减法规则);否则加上当前值。

  • 方法一(字典 + 遍历)
    :用字典映射字符到数值,逐个比较当前值与下一个值。
  • 方法二(字典 + 替换)
    :先把减法组合替换为加法(IV → IIII),然后直接求和。思路巧妙但效率稍低。

🖥️ 参考代码

def roman_to_int(s):    """罗马数字转整数 —— 遍历法 O(n)"""    roman_map = {        'I': 1, 'V': 5, 'X': 10, 'L': 50,        'C': 100, 'D': 500, 'M': 1000    }    total = 0    for i in range(len(s)):        current = roman_map[s[i]]        # 如果当前值 < 下一个值,说明是减法        if i + 1 < len(s) and current < roman_map[s[i + 1]]:            total -= current        else:            total += current    return totaldef int_to_roman(num):    """整数转罗马数字(逆向转换,帮助理解)"""    value_symbols = [        (1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),        (100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'),        (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')    ]    result = []    for value, symbol in value_symbols:        while num >= value:            result.append(symbol)            num -= value    return ''.join(result)# 测试if __name__ == "__main__":    test_cases = [        ("III", 3),        ("IV", 4),        ("IX", 9),        ("LVIII", 58),        ("MCMXCIV", 1994),        ("MMMCMXCIX", 3999),  # 最大标准罗马数字    ]    for roman, expected in test_cases:        result = roman_to_int(roman)        assert result == expected, f"{roman} → {result}, 期望 {expected}"        # 逆向验证        back = int_to_roman(result)        assert back == roman, f"{result} → {back}, 期望 {roman}"        print(f"  {roman} → {result} → {back} ✓")    print("\n所有测试通过 ✓")

🔗 关联知识点

知识点
说明
字典映射
字符 → 数值的一一对应
遍历时「向前看」
i + 1 < len(s)
 判断下一个值
减法规则
当前值 < 下一个值时做减法
''.join(list)
字符列表拼接为字符串

题目六:正则表达式驱动的日志分析器 ⭐⭐⭐⭐

📌 题目描述

实现一个日志分析器,解析 Nginx 风格的访问日志,支持以下功能:

  1. 解析日志行
    :提取 IP、时间、请求方法、URL、状态码、响应大小
  2. 统计 TOP-N 访问 IP
  3. 统计各状态码出现次数
  4. 筛选指定时间段的请求
  5. 统计请求路径的访问频率
日志格式:192.168.1.1 - - [10/Oct/2025:13:55:36 +0800] "GET /api/users HTTP/1.1" 200 123410.0.0.1 - - [10/Oct/2025:13:55:37 +0800] "POST /api/login HTTP/1.1" 401 89192.168.1.1 - - [10/Oct/2025:14:00:00 +0800] "GET /api/users HTTP/1.1" 200 567

💡 编程思路

这道题综合考察正则表达式、字符串解析、字典统计、f-string 格式化:

  1. 日志正则:Nginx 日志格式固定,可以用一个精确的正则一次性提取所有字段:

    ^(\S+) \S+ \S+ \[([^\]]+)\] "(\w+) (\S+) \S+" (\d+) (\d+)'    )    def __init__(self):        self.entries: List[LogEntry] = []    def parse_line(self, line: str) -> Optional[LogEntry]:        """解析单行日志"""        match = self.LOG_PATTERN.match(line.strip())        if not match:            return None        return LogEntry(*match.groups())    def parse(self, log_text: str):        """解析多行日志"""        for line in log_text.strip().splitlines():            entry = self.parse_line(line)            if entry:                self.entries.append(entry)    def top_ips(self, n: int = 10) -> List[Tuple[str, int]]:        """统计 TOP-N 访问 IP"""        ip_counter = Counter(entry.ip for entry in self.entries)        return ip_counter.most_common(n)    def status_distribution(self) -> Dict[int, int]:        """状态码分布"""        return dict(Counter(entry.status for entry in self.entries))    def top_urls(self, n: int = 10) -> List[Tuple[str, int]]:        """统计 TOP-N 请求路径"""        url_counter = Counter(entry.url for entry in self.entries)        return url_counter.most_common(n)    def filter_by_status(self, status: int) -> List[LogEntry]:        """按状态码筛选"""        return [e for e in self.entries if e.status == status]    def filter_by_time(self, start: str, end: str) -> List[LogEntry]:        """按时间段筛选(格式:'10/Oct/2025:13:00:00 +0800')"""        start_time = datetime.strptime(start, "%d/%b/%Y:%H:%M:%S %z")        end_time = datetime.strptime(end, "%d/%b/%Y:%H:%M:%S %z")        return [e for e in self.entries if start_time <= e.time <= end_time]    def generate_report(self) -> str:        """生成分析报告"""        top_ips = self.top_ips(5)        status_dist = self.status_distribution()        top_urls = self.top_urls(5)        total_size = sum(e.size for e in self.entries)        report = []        report.append("=" * 60)        report.append("  Nginx 日志分析报告".center(58))        report.append("=" * 60)        report.append(f"\n总请求数: {len(self.entries)}")        report.append(f"总流量: {total_size / 1024:.1f} KB")        report.append(f"\n--- TOP 5 访问 IP ---")        for ip, count in top_ips:            bar = "█" * min(count, 30)            report.append(f"  {ip:<18}{count:>5}{bar}")        report.append(f"\n--- 状态码分布 ---")        for status, count in sorted(status_dist.items()):            label = {200: "OK", 301: "Redirect", 401: "Unauthorized",                     403: "Forbidden", 404: "Not Found", 500: "Server Error"                     }.get(status, "")            report.append(f"  {status}{label:<15}{count:>5}")        report.append(f"\n--- TOP 5 请求路径 ---")        for url, count in top_urls:            report.append(f"  {url:<30}{count:>5}")        return '\n'.join(report)# 测试if __name__ == "__main__":    log_text = """192.168.1.1 - - [10/Oct/2025:13:55:36 +0800] "GET /api/users HTTP/1.1" 200 123410.0.0.1 - - [10/Oct/2025:13:55:37 +0800] "POST /api/login HTTP/1.1" 401 89192.168.1.1 - - [10/Oct/2025:14:00:00 +0800] "GET /api/users HTTP/1.1" 200 567172.16.0.1 - - [10/Oct/2025:14:01:00 +0800] "GET /api/products HTTP/1.1" 200 234510.0.0.1 - - [10/Oct/2025:14:02:00 +0800] "GET /api/orders HTTP/1.1" 403 120192.168.1.1 - - [10/Oct/2025:14:03:00 +0800] "POST /api/users HTTP/1.1" 201 456172.16.0.1 - - [10/Oct/2025:14:05:00 +0800] "GET /api/products/1 HTTP/1.1" 200 89010.0.0.2 - - [10/Oct/2025:14:10:00 +0800] "GET /favicon.ico HTTP/1.1" 404 0"""    analyzer = LogAnalyzer()    analyzer.parse(log_text)    # 打印报告    print(analyzer.generate_report())    # 按状态码筛选    print("\n--- 404 请求 ---")    for entry in analyzer.filter_by_status(404):        print(f"  {entry.ip}{entry.method}{entry.url}")    # 按时间筛选    print("\n--- 14:00-14:03 的请求 ---")    for entry in analyzer.filter_by_time(        "10/Oct/2025:14:00:00 +0800",        "10/Oct/2025:14:03:00 +0800"    ):        print(f"  {entry.time_str}{entry.ip}{entry.method}{entry.url}")

    🔗 关联知识点

    知识点
    说明
    复合正则
    多个捕获组一次提取全部字段
    re.compile()
    预编译正则,提升性能
    Counter.most_common()
    TOP-N 统计
    datetime.strptime()
    字符串解析为时间对象
    f-string 格式化
    报告中的对齐输出
    dataclass
     思路
    用类封装结构化数据

    总结:知识点覆盖矩阵

    题目
    难度
    核心考察点
    回文判断
    ⭐
    isalnum()
    、切片反转 [::-1]、双指针
    字符串反转(保持单词序)
    ⭐⭐
    split
     vs split(' ')、join、正则分组分割
    手机号脱敏与信息提取
    ⭐⭐
    re.sub
     分组替换 \1\2、re.findall、邮箱正则
    简易模板引擎
    ⭐⭐⭐
    re.sub
     传入 callable、非捕获组、动态替换
    罗马数字转整数
    ⭐⭐⭐
    字典映射、遍历向前看、减法规则
    日志分析器
    ⭐⭐⭐⭐
    复合正则、Counter、datetime、f-string 报告

    建议:按顺序从第 1 题做到第 6 题。前 3 题巩固基础方法,第 4-5 题锻炼综合应用能力,第 6 题是实战级项目,完整实现后你将掌握正则表达式和字符串处理的工程级用法。


    延伸阅读


    本文是《Python 全栈修炼之路》第 03 篇配套练习,欢迎点赞收藏!

    • LeetCode 125. 验证回文串
    • LeetCode 557. 反转字符串中的单词 III
    • LeetCode 13. 罗马数字转整数
    • LeetCode 12. 整数转罗马数字
    • LeetCode 8. 字符串转换整数 (atoi)
    • LeetCode 10. 正则表达式匹配

最新文章

随机文章