Python 将一段文字写入文件,每句话一行

81 阅读9分钟

import re from pathlib import Path _SENTENCE_REGEX=re.compile(r'[^.。]+[.。]?') #这个r 是必须写的吗 # 这个正则表达为什么写在开头而不是写在函数里面 def write_to_txt(path,text) -> str: p=Path(path) p.parent.mkdir(parents=True,exist_ok=True) sentences=[m.group(1).strip() for m in _SENTENCE_REGEX.finditer(text)] with p.open("w",encoding="utf-8",newline="\n") as f: for i,s in enumerate(sentences): #enumerate 是干嘛的 if i: f.write("\n") f.write(s) return f"写入完成{len(sentences)}行" if __name__=="__main__": path=r"C:\Users\Isabella\Downloads\test.txt" text="I loveyou.USA地" print(write_to_txt(path,text))

ChatGPT 说:

燎然 👍,这次你把 m.group(0) 改成了 m.group(1),其实这就是报错/结果不对的关键点。我们来逐条拆开:

1. r'...' 是必须写的吗?

  • rraw string(原始字符串) 前缀。

  • 它的作用是让 \ 不用写成 \\

  • 在你的正则里:[^.。]+[.。]? 没有 \,所以加不加 r 都行。
    👉 但是习惯上大家都会加 r,防止以后写 \.\d 等时忘记转义。

2. 为什么正则写在开头,而不是函数里面?

  • 写在函数外:编译一次,整个程序都能复用 → 效率高。

  • 写在函数里:每次调用函数都会重新编译一次正则 → 效率低。
    👉 所以把它当作“常量”,放文件开头最合理。

3. group(0) vs group(1)

  • group(0):总是返回整个匹配结果

  • group(1):返回第一个用 (...) 括号捕获的分组

  • 而你的正则 [^.。]+[.。]? 根本没有括号,所以 group(1) 会报错:

    IndexError: no such group
    

举例:

import re
m = re.match(r"(\d+)-(\d+)", "123-456")
print(m.group(0))  # '123-456'(整个匹配)
print(m.group(1))  # '123'     (第1组)
print(m.group(2))  # '456'     (第2组)

所以在你的代码里应该用:

sentences = [m.group(0).strip() for m in _SENTENCE_REGEX.finditer(text)]

而不是 group(1)

4. enumerate 是干嘛的?

for i, s in enumerate(sentences):
  • enumerate 会给每个元素带上索引。

  • 比如:

    list(enumerate(["a", "b", "c"]))
    # [(0, "a"), (1, "b"), (2, "c")]
    
  • 所以这里:

    • i = 第几个句子(0,1,2…)

    • s = 句子的内容

  • 这样你就能写 if i: → 意思是“从第二句开始才加换行”,避免最后文件多一行空行。

5. 修正后的完整代码

import re
from pathlib import Path

# 编译正则:匹配一段不是句号的文字 + 可选的句号
_SENTENCE_REGEX = re.compile(r'[^.。]+[.。]?')

def write_to_txt(path, text) -> str:
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)

    # 用 group(0) 才是整个匹配结果
    sentences = [m.group(0).strip() for m in _SENTENCE_REGEX.finditer(text)]

    with p.open("w", encoding="utf-8", newline="\n") as f:
        for i, s in enumerate(sentences):
            if i:  # 从第二句开始才换行
                f.write("\n")
            f.write(s)
    return f"写入完成 {len(sentences)} 行"


if __name__ == "__main__":
    path = r"C:\Users\Isabella\Downloads\test.txt"
    text = "I loveyou.USA地"
    print(write_to_txt(path, text))

运行后文件内容:

I loveyou.
USA地

终端输出:

写入完成 2 行

✅ 总结:

  • r'...' → raw string,不是必须,但推荐写。

  • 正则写函数外 → 提升性能。

  • group(0) → 整个匹配;group(1) → 第一个括号捕获的内容。

  • enumerate → 给循环加上索引,方便控制逻辑。

要不要我再帮你写一个带分组的正则版本,让你直观对比 group(0)group(1)group(2) 分别会拿到什么?

您说:

import re m = re.match(r"(\d+)-(\d+)", "123-456") print(m.group(0)) # '123-456'(整个匹配) print(m.group(1)) # '123' (第1组) print(m.group(2)) # '456' (第2组) 我不理解,再通俗点说。

ChatGPT 说:

好 👍,燎然,我给你用最直白的例子解释 group(0)group(1)group(2) 的区别。

1. 先看代码

import re
m = re.match(r"(\d+)-(\d+)", "123-456")
  • 正则 (\d+)-(\d+)

    • (\d+) → 表示一段数字,并且用 () 括号括起来 → 这叫 分组

    • 中间的 - → 表示一个短横杠。

    • (\d+) → 又是第二段数字。

所以整个正则要匹配的就是:

“前面一段数字 + 中间一个横杠 + 后面一段数字”

文本 "123-456" 正好符合。

2. 什么是分组?

括号 () 就像是给一段匹配内容贴了标签,方便单独拿出来。

  • (\d+) → 第 1 组(匹配到 "123"

  • (\d+) → 第 2 组(匹配到 "456"

3. group 的用法

  • group(0)整个匹配结果

  • group(1)第 1 个括号里的内容

  • group(2)第 2 个括号里的内容

所以结果是:

print(m.group(0))  # '123-456'  (整个匹配)
print(m.group(1))  # '123'      (第1个括号里的内容)
print(m.group(2))  # '456'      (第2个括号里的内容)

4. 打个比方(生活类比)

想象你在超市买了一瓶饮料,标签上有完整的说明:

可乐(500ml - 10元)
  • group(0) → 拿下整个标签:"500ml - 10元"

  • group(1) → 只取括号里第 1 段:"500ml"

  • group(2) → 只取括号里第 2 段:"10元"

括号就像给不同部分打了分区,方便你单独取。

5. 再举个例子

m = re.match(r"(\w+)@(\w+)\.com", "alice@gmail.com")
  • (\w+) → 第 1 组,匹配邮箱名 "alice"

  • (\w+) → 第 2 组,匹配域名 "gmail"

  • \.com → 固定匹配 .com

结果:

m.group(0)  # 'alice@gmail.com'   整个匹配
m.group(1)  # 'alice'             第1组(用户名)
m.group(2)  # 'gmail'             第2组(域名)

✅ 总结一句:

  • group(0) = 整个匹配到的字符串

  • group(1) = 正则里第 1 对括号 () 匹配到的内容

  • group(2) = 正则里第 2 对括号 () 匹配到的内容

👉 括号就像“切片刀”,可以让你把大匹配切成小块,单独取出来。

要不要我帮你画一张图,把 "123-456" 对应到正则 (\d+)-(\d+),直观地标出哪部分是 group(0),哪部分是 group(1),哪部分是 group(2)?

您说:

那这里的grounp(1) 没有,是因为我的正则里面没有括号,也就是没有捕获组?

ChatGPT 说:

对 ✅,你理解对了!

1. 什么是捕获组?

  • 在正则表达式里,只有 () 括起来的部分才会被叫做 捕获组 (capturing group)

  • 每一对括号都自动编号,从 1 开始。

例如:

(\d+)-(\d+)
  • 第一个 (\d+) → group(1)

  • 第二个 (\d+) → group(2)

  • 整个 (\d+)-(\d+) → group(0)

2. 如果正则里没有括号

比如你的:

[^.。]+[.。]?

这里没有任何 (),所以它只有:

  • group(0) → 整个匹配的结果

  • 没有 group(1)、group(2),你要取就会报错:

    IndexError: no such group
    

3. 类比

你可以把正则想象成“切水果”:

  • 没有括号:就是整颗西瓜,group(0) 就是完整的西瓜。

  • 有括号:括号就像刀切开西瓜,group(1) 是第一片,group(2) 是第二片……

4. 举个对比例子

import re

# 没有括号
m = re.match(r'\d+-\d+', '123-456')
print(m.group(0))   # '123-456'
# print(m.group(1)) # ❌ 报错,因为没有 group(1)

# 有括号
m = re.match(r'(\d+)-(\d+)', '123-456')
print(m.group(0))   # '123-456'
print(m.group(1))   # '123'
print(m.group(2))   # '456'

✅ 总结:

  • group(0):永远是整个匹配。

  • group(1):只有当正则里有第一对 () 括号时才存在。

  • 没有括号 → 就没有 group(1)、group(2)。

要不要我帮你改造一下你的句子分割正则,加上括号,让你能单独取“正文部分”和“句末标点部分”?这样你能亲眼看见 group(1) / group(2) 的效果。

您说:

_SENTENCES_REGEX=r'[^.。]+[.。]?'

ChatGPT 说:

燎然 👍,你写的这一句:

_SENTENCES_REGEX = r'[^.。]+[.。]?'

我们来分析一下:

1. 这行代码在干嘛

它只是一个 字符串变量,内容是:

'[^.。]+[.。]?'

因为你写了前缀 r,所以这是一个 raw string(原始字符串)

👉 注意:它不是正则对象,只是正则表达式的“文本”。
如果要让它真的能用来匹配字符串,你还需要用 re.compile()

import re

pattern = re.compile(_SENTENCES_REGEX)

2. 这条正则的含义

[^.。]+[.。]?

拆开:

  • [^.。]+ → 匹配一段不是句号/中文句号的文字(至少 1 个字符)

  • [.。]? → 可选的一个句号(英文句号 . 或中文句号

👉 整体意思就是:
匹配“一个句子正文 + 可选的句号”

3. 举例子

import re

pattern = re.compile(r'[^.。]+[.。]?')
text = "I love you.USA地。Python"

print([m.group(0) for m in pattern.finditer(text)])

输出:

['I love you.', 'USA地。', 'Python']

可以看到:

  • 第一句 "I love you." → 匹配到正文 + 英文句号

  • 第二句 "USA地。" → 匹配到正文 + 中文句号

  • 第三句 "Python" → 没有句号,但 ? 允许它为空,所以也能匹配

4. 为什么加 ?

如果写成:

[^.。]+[.。]

那最后的 "Python" 就不会匹配到,因为它没有句号。

加上 ?,就能保证最后一句没有句号也能匹配

✅ 总结:

  • _SENTENCES_REGEX = r'[^.。]+[.。]?' → 定义了一个正则表达式字符串,用来切分句子。

  • 它表示:“匹配一段非句号文字 + 可选的句号”。

  • 要真正用来匹配,得先 re.compile(_SENTENCES_REGEX)

要不要我帮你把这个正则改成 带括号分组的版本,这样你就能用 group(1) 拿正文,group(2) 拿句号?

您说:

for i,s in enumerate sentences:

ChatGPT 说:

燎然,你写的这一句:

for i,s in enumerate sentences:

会报错的 🚨,因为 enumerate 是一个函数,必须加上括号,把它要枚举的对象(比如列表)传进去。

✅ 正确写法:

for i, s in enumerate(sentences):
    ...

🔍 enumerate 干嘛用?

它能让你在 for 循环时,同时拿到下标(索引)和元素

例子:

sentences = ["I love you.", "USA地。", "Python"]

for i, s in enumerate(sentences):
    print("第", i, "行:", s)

输出:

0 行: I love you.
第 1 行: USA地。
第 2 行: Python

🌟 为什么不用普通的 for?

如果你写:

for s in sentences:
    print(s)

只能拿到元素,拿不到索引。

enumerate 就能多拿一个 i,常常用来判断是不是第一行、是不是最后一行之类的。

要不要我帮你画一张 对比图,直观展示 forenumerate 的区别?

您说:

import re from pathlib import Path _SENTENCES_REGEX=re.compile(r'[^.。]+[.。]?') def write_to_txt(path,text): p=Path(path) sentences=[m.group(0).strip() for m in _SENTENCES_REGEX.finditer(text) ] sentences=[s for s in sentences if s] with p.open("w",encoding="utf-8",newline="\n") as f: for i,s in enumerate(sentences): if i: f.write("\n") f.write(s) return f"写入完成{len(sentences)}行" if __name__=="__main__": path=r"C:\Users\Isabella\Downloads\test.txt" text="I loveyou.USA地" print(write_to_txt(path,text))

ChatGPT 说:

已思考 14s

燎然,进步很快!这版已经能跑了,但还有几个关键点要补齐、再顺一点。我先精准点评,再给你一份“最小改动可用版”。

主要问题与建议

  1. 可能写入失败:父目录未创建
    你这版没有 p.parent.mkdir(...)。如果 C:\Users\Isabella\Downloads\ 一直在就没事,但一旦换路径、父目录不存在就会 FileNotFoundError。——应当先确保父目录存在

  2. 句尾标点覆盖不全
    现在正则只识别 .。中文常见还有 !! ?? ;; …。建议把这些都纳入,否则会把整段当成一行。

  3. 小瑕疵

    • return 放在 with 代码块里没问题(离开 with 时会自动关闭文件),只是风格上有人喜欢放到 with 外,便于阅读。

    • 你已经做了空串过滤 👍。

  4. 已知局限(先记着)

    • 小数/缩写3.14U.S.A.e.g.)会被当成句号切开——要严谨处理需要更复杂正则或预处理,后面再进阶。

最小改动可用版(保留你原结构)

import re
from pathlib import Path

# 覆盖常见句末符;保留“可选句末标点”
_SENTENCES_REGEX = re.compile(r'[^。.\.!?!?;;…]+[。.\.!?!?;;…]?')

def write_to_txt(path, text) -> str:
    p = Path(path)
    # 关键补齐:确保父目录存在
    p.parent.mkdir(parents=True, exist_ok=True)

    # finditer 提取“句子主体 + 可选句末标点”,strip 去两端空白
    sentences = [m.group(0).strip() for m in _SENTENCES_REGEX.finditer(text)]
    sentences = [s for s in sentences if s]  # 过滤空串

    with p.open("w", encoding="utf-8", newline="\n") as f:
        for i, s in enumerate(sentences):
            if i:               # 从第二行开始才写换行,避免最后多空行
                f.write("\n")
            f.write(s)
    return f"写入完成 {len(sentences)} 行"

if __name__ == "__main__":
    path = r"C:\Users\Isabella\Downloads\test.txt"
    text = "I loveyou.USA地!再来一行…没有句号也要保留"
    print(write_to_txt(path, text))