1. 程式人生 > >[Python] 開啟字串使像 open(file, 'r') 一樣可迭代

[Python] 開啟字串使像 open(file, 'r') 一樣可迭代

[Python] 開啟字串使像 open(file, ‘r’) 一樣可迭代

需求

你想要像 open(file, ‘r’) 一樣 OPEN(string), 然後可以按行讀取或者在 for 語句中迭代它。

Solution

solution 這個放的程式碼是個人的最終版本,有改進或建議請在評論區中指出,我會在可以更新的時候更新本處程式碼!

Core Code

從 string 生成可迭代物件最簡單方式

string_as_list_which_can_be_iterable = string.splitlines(True)
#
# iterable usage:
#
for line in string_as_list_which_can_be_iterable:
    print(line, end="")

仿 open(file, 'r') 完整方法實現方式

N/A

如何使用

N/A

測試

N/A

原理

實現上述功能的方式有很多,這裡我們將參照 Fluent Python

中的解釋迭代器的章節,一步一步實現早期 python 版本的寫法 __item__,和現代 python 版本的寫法 __iter__ 以及使用 yield 實現的版本等。

你將學到什麼

  1. python 可迭代條件(型別)
  2. python 建立自定義的可迭代型別
  3. python yield 的應用(使用),再解釋工作原理
  4. 涉及到一處簡單的 RE 表示式,內建 RE 庫的使用
  5. 1, 和 2, 我們自己實現了 python 的 魔術 特殊方法,這解釋了 python 為什麼有強大的 “一致性”。

用於測試我們的實現的 string 說明

在開始具體的實現之前,有必要說明一下我們用於測試的 string 內容是什麼。

將 string open 得到一個可迭代的例項物件最初是我在學習 TDD1 的時候,邊測試邊開發一個 django web 應用時,遇到一章開始建立表單的地方,為了“測試” web 應用是否正確地渲染了前端頁面(渲染後地頁面用於響應瀏覽器請求),需要用“手動”渲染地頁面原始碼和通過 HTTP GET 請求經過正常後臺渲染響應的頁面原始碼比較,看看是否一致。

就像我上面說的,這是一個 django + 表單 頁面,所以裡面在預設情況下是含有 token 的,而測試的時候是經過了兩種不同的路徑渲染頁面,導致了 token 是不同的。因此是需要刪除 token 後再比較頁面原始碼的。

當然,刪除的辦法有很多,我當時想到的是因為 token 欄位就在頁面原始碼中的獨立一行,像這樣:

django token
那麼我在做比較的測試程式碼中,可以用 for 按行迭代,判斷如果某一行中有 “csrfmiddlewaretoken” 就跳過比較。

所以,你可以想象在我還沒有實現現在正要實現的“開啟 string 得到一個可迭代物件”的時候,我使用了一種行數不多但是仍然並不優雅(使用場景很受限,程式碼不夠可複用)的方式實現了我想要的判斷2

因此,學習實現一份 “開啟 string 得到一個可迭代物件” 正是一項既實用又能夠學習應用 python 迭代器,生成器這方面知識的好例子。

最後,如果你沒了解過沒學過 django,所以沒有完全明白我上面在講什麼也沒有任何關係,我上面只是講述了我實現這份程式碼的背景和這份程式碼可以使用的一個場景。

因此下面我用於測試的 string 就是一份“網頁原始碼”,一般網頁原始碼就是會包括 HTML,JavaScript 程式碼,並且有相當可觀的長度。我使用 requests 包3 獲取了我自己開發的一個 web 應用 - “django cloud” 的早期版本的上傳檔案頁面的網頁原始碼,這個頁面長這樣:
localhost:8028/basic/ 頁面

因此獲取這個頁面的網頁原始碼能夠限制我們這篇文章的篇幅 - 不會因為有太多的原始碼貼出來導致文章太長。
右擊瀏覽器,“檢視網頁原始碼”,可以觀察這個頁面的程式碼如下:
localhost:8028/basic/ 網頁原始碼

使用 requests 包獲取這張網頁原始碼的方式如下:

$ pip -V  # 這一行只是為了說明我使用 “pip”,但使用的 pip 是對應 python3 版本的!
.....Python 3.x.x....
$ pip install requests
## 如果你想要像我獲取“網頁原始碼”到一個 string 中來測試接下來要實現的程式碼,
## 並且還沒有安裝 requests 庫的話,那麼現在就可以執行那一行命令了。
$ python -V
Python 3.x.x ....
$ python
>>> import requests
>>> r = requests.get("http://localhost:8028/basic/")
### 因為這是我自己本地啟動的 web 伺服器。
### 你可以使用 r = requests.get("https://www.baidu.com")
### 這樣的形式抓取百度首頁的網頁原始碼
>>> string = t.text
### 上面這行程式碼就將 string 關聯到了“網頁原始碼”。
>>> string  # 在python互動直譯器中檢視 string 資訊
'<!DOCTYPE html>\n<html>\n<head>\n\t<title>httpserver-cloud | basic</title>\n[...剩下的程式碼...]'

這段程式碼是一個示範,而且它是在 python 互動直譯器中執行的。下面你就會看到我們將在程式碼檔案中使用上面相同的獲取網頁原始碼的程式碼。

早期版本 python 的 __item__ 實現可迭代

使用正則表示式(RE 庫)將 string 按行分割

如果你瞭解正則表示式已經知道如何在 python 中使用正則表示式,那麼這一小節當然可以跳過。

但是如果你之前沒有接觸過正則表示式,那麼我將解釋一下下面會用到的正則表示式。

完全介紹/解釋正則表示式及其使用偏離了本文的主題。
並且由於博主目前使用頻率不高,自認為不會比起其它專門講解正則表示式的文章解釋的更好,所以有興趣的讀者可以上網搜尋正則表示式的文章學習。
本文只使用到一句正則表示式,理解了這句程式碼就足夠對於閱讀本文就不會有這個知識點上的障礙了。


  • 待補充

撰文要點筆記:匯入庫 -> 表示式符號含義 -> 對應的匹配 ->
.findall() 方法介紹 -> .findall() 返回值型別,繼承關係 -> 如何使用返回值
直接轉化返回值為列表物件 - 可迭代型別

實現 __item__ 得到可迭代物件

  • 只放程式碼,待補充細節!!!
import re
import reprlib

RE_LINE = re.compile('.+\\n')


class SuperOpen(object):
    def __init__(self, text):
        self.text = text
        self.words = RE_LINE.findall(text)

    def __getitem__(self, index):
        return self.words[index]

    def __len__(self):
        return len(self.words)

    def __repr__(self):
        return "SuperOpen(%s)" % reprlib.repr(self.text)


#
# Test/Usage
#
import requests

r = requests.get("http://localhost:8028/basic/")
string = r.text

tp = SuperOpen(string)
for line in tp:
    print(line, end="")

現代版本 python __iter__ 實現可迭代

import re
import reprlib

class SuperOpen(object):
    RE_LINE = re.compile('.+\\n')

    def __init__(self, text):
        self.text = text
        self.lines = self.RE_LINE.findall(text)

    def __iter__(self):
        return SuperOpenIterator(self.lines)

    def __repr__(self):
        return "SuperOpen(%s)" % reprlib.repr(self.text)


class SuperOpenIterator:

    def __init__(self, lines):
        self.lines = lines
        self.index = 0

    def __next__(self):
        try:
            line = self.lines[self.index]
        except IndexError:
            raise StopIteration()
        self.index += 1
        return line

    def __iter__(self):
        return self

#
# Test/Usage
#
import requests

r = requests.get("http://localhost:8028/basic/")
string = r.text

tp = SuperOpen(string)
for line in tp:
    print(line, end="")

yield 版本的開啟 string 可迭代

import re
import reprlib

class SuperOpen(object):
    RE_LINE = re.compile('.+\\n')
    def __init__(self, text):
        if __debug__:
            print("yield solution")
        self.text = text
        self.lines = self.RE_LINE.findall(text)

    def __repr__(self):
        return 'SuperOpen(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        for line in self.lines:
            yield line
        return
# 完成

#
# Test/Usage
#
if __name__ == "__main__":
    import requests

    r = requests.get("http://localhost:8028/basic/")
    string = r.text

    tp = SuperOpen(string)
    for line in tp:
        print(line, end="")

惰性實現 - 節省記憶體

import re
import reprlib

class SuperOpen(object):
    RE_LINE = re.compile('.+\\n')
    def __init__(self, text):
        if __debug__:
            print("lazy yield solution")
        self.text = text

    def __repr__(self):
        return 'SuperOpen(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        for match in self.RE_LINE.finditer(self.text):
            yield match.group()

#
# 測試程式碼同上
#

Reference

N/A

致謝

None for now.


  1. Test-Driven Development (測試驅動開發)。 ↩︎

  2. Django with TDD 對 load template 生成的 HTML 比較去除 CSRF 和空白符↩︎

  3. 非 Python 標準庫的常用於獲取網路上的網頁原始碼(網路資料/資源)的庫。 ↩︎