背景
一次工作中,我需要完成某個檔案的字元串替換。
需求是這樣的:檔案A有個占位符,需要利用Python3,把占位符替換成檔案B的内容。檔案都不大,可以一次性讀到記憶體處理。
我想,這不是簡單的
open
read
replace
write
就搞定了嘛?
結果,還真有點麻煩!
思路
- 全量讀取檔案A,儲存到變量templace
- 全量讀取檔案B,儲存到變量text
- 利用python的
實作正則替換,儲存到新變量resultre.sub
- 把變量result内容寫入檔案A
with open('A', encoding='utf8') as f:
template = f.read()
with open('B', encoding='utf8') as f:
text = f.read()
result = re.sub(r'占位辨別符', text, template, 1)
with open('A', 'w', encoding='utf8') as
遇到的問題
檔案B内有換行符,也有字元串
\n
,按上文的方式處理後,所有的字元串
\n
都變成了換行符!
舉個例子,template是
我是:{}
(其中
{}
就是占位符),text是下面的文本:
哈哈
哈哈\n哈哈
替換後,如下圖所示:
可以看到,當我列印
re.sub
結果時,所有的
\n
都變成了換行符,字元串
\n
消失了!
這的确令人煩躁,本來五分鐘可以搞定,結果要花多餘的時間處理這個問題。如果你學會了本文,以後都不用再去費腦筋了~
思考過程
一開始遇到這個問題,是在寫入檔案後發現的,是以并沒定位的這麼準确,當時跟換行符相關的,我懷疑了以下方面:
- 字元串定義沒有使用 Raw String(例如
這種方式)。r'xxx'
- 正則替換出了問題。
- 寫入檔案時,
參數導緻。newline
如果我們能把這3個問題全都弄清楚,以後定位就非常快了!
Raw String
Python中,如果字元串常量的定義前加了個
r
,就表示 Raw String 原始字元串。
Raw String 特點在于,字元串常量裡的
\
将不具有轉義作用,它僅僅代表它自己。
例如,你定義個普通字元串
"\n"
,這個字元串長度其實是1,它隻包含了1個換行符,對應的 ASCII 是10。
如果你定義了原始字元串
"\n"
,這個字元串長度就是2,它包含了字元
\
和字元
n
。
如果字元串沒轉義字元,那麼 Raw String 跟普通 String 完全一緻
轉義字元有這些:
也就是說
r'\haha'
跟
'\haha'
是完全一緻的,因為
\h
不是轉義字元,是以這種情況下,沒必要加
r
。
誤區:注意引号問題
有一個令人疑惑的點:理論上講,
r'\'
應該就是
'\\'
,但是當你使用
r'\'
時,Python會報錯。
這是因為Python在編譯時,讀取字元串時,如果字元串以單引号開頭,遇到
\'
後,不論你是不是Raw String,都會繼續認為是字元串,不會把
'
當作結束符。估計是一個曆史遺留問題。我們隻能接受現實。但是其實編譯後,
\
和
'
又變成了2個字元。
如何證明呢?你給字元後面加個空格,發現它們是相等的:
r'\ '
和
'\\ '
。但是單獨的字元
r'\'
就報錯了。
但是這種情況隻有字元串末尾是奇數個
\
時(例如
r'\'
或
r"\"
)才會發生,如果字元串最後一個字元不是
\
,或者字元串最後是連續偶數個
\
,是沒問題的,例如
r"\\"
可以被合法定義。
啟發
定義字元串時,如果你是這麼定義:
"哈哈\n哈哈"
,那麼這個字元串長度是5,包含了1個換行符。
如果你是這麼定義:
r"哈哈\n哈哈"
,那麼這個字元串長度是6,不包含換行符,包含字元
\
和
n
。
同樣,當你寫入檔案時,如果是
f.write('\n')
,就表明寫入了換行符,但如果是
f.write(r'\n')
,就表明寫入了字元串
"\n"
。
在正規表達式中,我們通常用 Raw String,但我們依然使用
\n
表達換行,是因為正則文法中,會把2個連續的字元
\
和
n
當作換行符,這是正規表達式庫做的轉義,而非定義字元串時Python做的轉義。
正則替換的問題
這是導緻本文問題的根本原因。使用
re.sub
時,所有的字元串
r"\n"
都被當作了換行符。
怎麼辦呢?
隻要我們替換前,把原始檔案對應的字元串的
r"\n"
都改為
r"\\n"
,手動多加了一次轉義符,那麼
re.sub
時,就不會把
r"\n"
當作一個整體改成換行符了,反而會把
r"\\"
當作一個整體,替換為字元
\
。這樣
r"\n"
字元串就保留下來了!當然,其它轉義字元,也統統保留下來了。這就是正确的解法了。
open 檔案的 newline 參數
with open(filename, 'r', newline=None) as
這個主要是因為不同作業系統的換行符不同,是以有了這個參數。Windows 是 CRLF 即
\r\n
,Unix 是 LF 即
\n
,舊版 Macintosh 是 CR 即
\r
。
通常情況下,我們不需要加這個參數,Python 會自動為我們做這些事情:
- 讀取檔案時,自動把文本中的各種換行符統一轉換為
。"\n"
- 寫入檔案時,根據目前的作業系統,自動把
轉換為對應的換行符,通過"\n"
可以檢視目前作業系統換行符。os.linesep
當然,你也可以主動設定 newline 參數:
- 讀取檔案時,如果 newline 是空字元串
,則Python不會做任何自動轉換,讀到什麼就是什麼。''
- 讀取檔案時,如果 newline 是非空字元串,則Python會把換行符轉化為這個非空字元串,例如你可以指定為
或'\r'
或其它。'\r\n'
- 寫入檔案時,如果 newline 是空字元串
,則Python不會做任何自動轉換,現在換行符是什麼,就寫入什麼。''
- 寫入檔案時,如果 newline 是非空字元串,則Python會把
轉化為這個非空字元串,例如你可以指定為\n
或'\r'
或其它。'\r\n'
注意,
newline
參數隻對文本檔案有效,如果是二進制讀寫,
newline
是無用的。