Friday, November 24, 2017

較快把卡提諾小說轉成epub的方法

轉成epub的好處是下一頁直接點一下即可。用網頁看沒辦法這樣;捲頁比較麻煩。另外一點是,網頁版沒辦法記住這次看到哪裡,每次開網頁都得重來。遇到那種1000多章的小說,真的會很煩。

還有,epub版本最大的優點:不需要網路也可以看。

幾個步驟:

  1. 先用CSNovelCrawler把要抓的小說抓下來,CSNovelCrawler可以從這裡抓:http://rngmontoli.blogspot.tw/2013/06/csnovelcrawler.html
  2. CSNovelCrawler抓下來的檔案是純文字檔,得把它轉成HTML或是doc檔案。
    1. 轉成HTML檔案
    2. 轉成DOC檔案:
  3. 用calibre把HTML或是DOC檔案轉成epub
麻煩的是第二點,一開始是用DOC檔案,但是每一章標題得自己轉成DOC的標題,不然轉出來的epub就沒有bookmark。在WORD內要偵測每一章標題的標題就只能用VBA,但是這玩意的語法很奇怪,微軟文件也沒有很詳細的介紹。最後還是用硬幹法寫出來了,但是速度很差,轉個1MB檔案要快10分鐘。這還可以接受,反正只是放著就好。

最終放棄DOC檔案的契機是,我不是每台電腦都有WORD啊。要換成open office還得重寫open office的script。算了,試試看HTML檔案吧。

首先得先選擇轉HTML檔的語言。C/C++,JAVA跟C#都出局,因為他們處理文字跟檔案實在很麻煩。那就來試試看python吧。

結果太好了,沒幾行就可以了。下面是python的程式碼。它做的事情很簡單:

  1. 開檔案
  2. 把一些字轉換掉(主要是空白跟簡體) 
  3. 一行一行找"第x章"這類的文字,有的話就加上<h2>標籤,不然就是<p>標籤。
  4. 如果是<h2>標籤的文字,再把中文數字轉成阿拉伯數字,原因是我不喜歡中文數字,很不直覺跟佔字數。"一千一百一十八"跟"1118"比,我還是喜歡1118。直接在網路上找到這個用即可(https://github.com/binux/binux-tools/blob/master/python/chinese_digit.py)。

還有一點要注意:用Calibre把HTML檔轉成epub的時候,因為它預設是不會把<h2>標籤輸出成epub的bookmark,所幸可以手動指定(第一級目錄頁設為 //h:h1,第二級目錄頁設為//h:h2):



import os, sys
import os, sys
dict ={u'零':0, u'一':1, u'二':2, u'三':3, u'四':4, u'五':5, u'六':6, u'七':7, u'八':8, u'九':9, u'十':10, u'百':100, u'千':1000, u'萬':10000,
       u'0':0, u'1':1, u'2':2, u'3':3, u'4':4, u'5':5, u'6':6, u'7':7, u'8':8, u'9':9,
       u'0' :0, u'1' :1, u'2' :2, u'3' :3, u'4' :4, u'5' :5, u'6' :6, u'7' :7, u'8' :8, u'9' :9,
                u'壹':1, u'貳':2, u'參':3, u'肆':4, u'伍':5, u'陸':6, u'柒':7, u'捌':8, u'玖':9, u'拾':10, u'佰':100, u'仟':1000, u'萬':10000,
                         u'兩':2,
       u'億':100000000}
def GetResultForDigit(a, encoding="utf-8"):
    count = 0
    result = 0
    tmp = 0
    Billion = 0
    while count < len(a):
        tmpChr = a[count]
        #print tmpChr
        tmpNum = dict.get(tmpChr, 0)
        #如果等于1亿
        if tmpNum == 100000000:
            result = result + tmp
            result = result * tmpNum
            #获得亿以上的数量,将其保存在中间变量Billion中并清空result
            Billion = Billion * 100000000 + result
            result = 0
            tmp = 0
        #如果等于1万
        elif tmpNum == 10000:
            result = result + tmp
            result = result * tmpNum
            tmp = 0
        #如果等于十或者百,千
        elif tmpNum >= 10:
            if tmp == 0:
                tmp = 1
            result = result + tmpNum * tmp
            tmp = 0
        #如果是个位数
        elif tmpNum is not None:
            tmp = tmp * 10 + tmpNum
        count += 1
    result = result + tmp
    result = result + Billion
    return str(result)


def Katino_format( input_file_name, author ):
    filename, file_extension = os.path.splitext(input_file_name)
    output_file_name = filename + '.html'

    with open(input_file_name, 'r', encoding='utf8') as input_file:
        print('file is opened.');
        with open(output_file_name, 'w', encoding='utf8') as output_file:
            output_file.write("<!DOCTYPE html>\n");
            output_file.write("<html>\n");
            output_file.write("<title>" + filename + "</title>\n");
            output_file.write("<meta name=Author content=\"" + author + "\">");
            output_file.write("<body>\n");
            output_file.write("<h1>" + filename + "</h1>\n");
            for line in input_file:
                #print(line);
                # replace redundent line break
                line = line.replace("\n\n", "\n");

                # remove 4 spaces
                line = line.replace("    ", "");

                # remove 2 full-size spaces
                line = line.replace("  ", "");

                # replace 1 full-size space with half-size space
                line = line.replace(" ", " ");

                # replace 1 ? space with half-size space
                line = line.replace(" ", " ");
                
                # replace  '(' with '('
                line = line.replace("(", "(");

                # replace  ')' with ')'
                line = line.replace(")", ")");

                # replace  ',' with ','
                line = line.replace(",", ",");

                line = line.replace(":", ":")
                line = line.replace("!", "!");
                line = line.replace("?", "?");
                line = line.replace("隻是", "只是");
                line = line.replace("麵對", "面對");

                # find 第x章
                keyword1_pos = line.find("章");
                keyword2_pos= -1
                chapter_find = 0;
                if keyword1_pos >=0 and keyword1_pos < 10:
                    keyword2_pos = line.find("第");
                    if keyword2_pos >= 0 and keyword2_pos < 5 and keyword2_pos < keyword1_pos:
                        # replace '  ' with ' '
                        line = line.replace("  ", " ");
                        # add ' ' if no ' ' after '章'
                        if line[keyword1_pos+1] != ' ':
                            line = line[:keyword1_pos+1] + ' ' + line[keyword1_pos+1:];
                        # try convert chinese number to arabic number
                        chinese_number = line[keyword2_pos+1:keyword1_pos];
                        arabic_number = GetResultForDigit(chinese_number)
                        print(line + ": " + chinese_number + "-->"+ arabic_number)
                        line = line.replace(chinese_number, arabic_number);                        
                        # it is <h2>
                        chapter_find = 1;

                if chapter_find == 1:
                    line = line.replace("\n", "");
                    output_file.write("<h2>" + line + "</h2>\n");
                else:
                    if line != "\n":
                        line = line.replace("\n", "");
                        output_file.write("<p>" + line + "</p>\n");

            output_file.write("</body>\n");
            output_file.write("</html>\n");
            output_file.close()
        input_file.close()
    return

author = '蝴蝶藍';
Katino_format('[蝴蝶藍] 天醒之路[1].txt', author)
print('done.');

Sunday, August 16, 2015

USB Host 簡介

USB2.0規格書內有關於HOST的內容,只有第十章,短短22頁。相較於第十一章的HUB150幾頁,真的有夠少。

為什麼會這麼少?我看是因為相關內容都已經在前面提過了:基本上Device要遵守的,HOST不是要實作,就是也得遵守。所以也沒有甚麼可以提的。

但是,凡事都會有的但是,HOST的實作是有規格的。如果要支援full/low speed裝置,請看OHCIUHCI規格書。如果是要支援high speed,請看EHCI規格書。

看官一定會問為什麼full/low speed HOST有兩種規格?那是因為不同陣營提出的不同規格,幸運的是你不必兩個都看,因為一個HOST硬體只會實作一種規格。應該不會有人那麼無聊實做兩種規格吧...

本系列只討論EHCI,因為只有它支援high speed裝置。很有趣的是,它也只支援high speed裝置,如果使用者插入的是full/low speed裝置,那就是OHCIUHCI HOST起來了。

EHCI規格書是免費的,可以在intel網站下載。跟USB裝置的規格不同,EHCI規格非常詳細,詳細到硬體需要做的事情都定義好了,所以基本上讀完EHCI也就等於讀完硬體的規格了,至於軟體嘛,也沒甚麼差別,因為硬體規格都一樣。根據同一份硬體寫出來的軟體架構大同小異,差別可能只在於效能。


下圖是整個USB軟硬體的架構圖(規格書)。整個架構很清楚,EHCIOHCI/UHCI都得有個驅動程式,這邊叫HCDHost Controller Driver)。USBD則是EHCI/OHCI/UHCI的抽象層,這個很好理解,因為總不能叫上層驅動程式自己依據USB連線速度去呼叫EHCI/OHCI/UHCI驅動程式的API吧。Client Driver Software則是使用者接上HOST的裝置所需要的驅動程式。所以要讓HOST可以正確跟一個裝置互動,總共需要HCDClient Driver SoftwareHCD通常會跟OS綁在一起,所以大多數的人都是無感,Client Driver Software則是所謂的USB驅動程式,這個大部分的人就會有感覺,因為一插上電腦Windows就會問的嘛。


要注意一點,EHCI規格書包含軟體跟硬體的實作描述,所以在實作軟體時,必須注意到那些部分是Host Controller會接手,以及軟體如何跟Host Controller溝通及同步。

1.1 Root Hub

EHCI規格書規定Host Controller必須實作Root HubRoot Hub,顧名思義,就是個Hub,但是它是包含在Host Controller內。當然,Host Controller可以讓Root Hub只有一個port,這種情況下,如果要支援多個裝置,就還得外接一個Hub才行。

值得注意的是,Root Hub跟一般的Hub在實作上還是有差異,對Host Controller來說,Hub就是個裝置,它還是需要掛個Client Driver Software,而Root Hub驅動程式則是直接內建在HCD內即可。

1.2 Registers

EHCI規格書規定了兩種register interfacePCI Configuration RegistersMemory-mapped USB Host Controller Registers。前者其實只是後者的wrapper,不過還真的是佛心來著,一般都是沒在寫的。

USB Host Controller Register分兩類,一種是capability,另外一種是operational。這個也是很好理解:先讀capability registers看這個Host Controller的能耐,之後根據這些資訊對operational registers寫值。

capability registers這邊就不提了,因為這會跟Host Controller硬體有關。

1.2.1 Operational registers

operational registers則是如下:

1.2.1 USBCMD

HCDHost Controller下命令用的register

1.2.2 USBSTS

這個register代表Host Controller的目前狀態。

1.2.3 USBINTR

HCD可以透過這個register設定Host Controller回報的interrupt

1.2.4 FRINDEX

Host Controller目前的SOF numberPeriodic Shcedule會用到這個register

1.2.5 CTRLDSSEGMENT - Control Data Structure Segment Register

支援64-bitHost Controller才會用到的register

1.2.6 PERIODICLISTBASE - Periodic Frame List Base Address Register

Periodic Shcedule會用到這個register

1.2.7 ASYNCLISTADDR - Current Asynchronous List Address Register

Asynchronous Shcedule會用到這個register

1.2.8 CONFIGFLAG - Configure Flag Register

1表示所有Root Hubport都是給EHCI用,寫0表示所有Root Hubport都是給EHCIOHCI用。請參考1.3.2 Port Routing

1.2.9 PORTSC(1-N_PORTS) - Port Status and Control Register

用來控制跟讀取Root Hub的每個port

1.3 Operational Model

1.3.1 Initialization

 這部分不用多講,規格書寫的很清楚。

1.3.2 Port Routing

當一個fulllow speed裝置插入EHCI的某個port時,理論上EHCI Host Controller是無法處理它的,所以就要把這個port的所有人轉給UHCIOHCI Controller來處理這個裝置。在這邊,規格書把UHCIOHCI Controller統稱為Companion Host ControllerConpanion HC)。而把設定port所擁有者的方式叫Port Routing


Port Routing的設定方式基本上分全域設定跟Port設定。這很好理解。全域設定就是設定CONFIGFLAG register,一次設定所有Port的擁有者。但是當要把某個Port轉給某個擁有者時,則是設定PORTSC register

下面是規格書內的Port Routing流程,基本上EHCI才是主控者:

1. EHCI HCD偵測到某個port有裝置插入。
2. EHCI HCD先檢查PORTSC register內的LineStatus欄位:


如果是不是low speed裝置的話,EHCI HCD則先透過PORTSC register內的PortReset欄位reset裝置:


然後再檢查PORTSC register內的PortEnable欄位。如果PortEnable欄位是1的話,表示這個裝置是high speed,反之則是full speed


當偵測到fulllow speed時,EHCI HCD必須把PoetOwner欄位設為1,把這個portowner轉給companion HC,讓companion HC HCD去對付這個裝置。


 3. 當compaion HC擁有一個port之後,只有在裝置被移除之後,這個port才會被還給EHCI。請注意,[裝置移除]這個事件是companion HCEHCI都可以同時偵測到,所以這兩個HC HCD必須有一個同步的機制,不然有可能會出問題。

1.3.3 Port Power

Port Power能否關閉是根據Host Controller的實作而定,請參考capability registers內的HCSPARAMS register


AgainEHCI規格書真是佛心,還幫大家歸納出各種Port Power的設定:


1.3.4 Port Over-Current detection

偵測Over-Current的實作規格並不在EHCI規格書內,它通常是透過Host Controller的外部線路來達成。

HCD可以透過PORTSC的兩個register來偵測over-current狀況。請注意一點,當over-current發生的時候,PortEnablePortPower會被設為0,這很合理,電流都過載了,當然要斷電。


1.3.5 Suspend/Resume

如果要實現Selective Suspend/Resume,可以透過PORTSC內的Suspend跟[Force Port Resume]欄位:


如果要suspend/resume整個BUS,則是設定USBCMD register內的[Run/Stop]欄位。我猜這中間還是需要HCDHost Controller之間的溝通跟同步,不然應該會出問題,不過這邊規格書就沒提要如何做了。

1.3.6 Schedule Traversal Rules

整個EHCI最精華的部分就是Schedule。什麼是Schedule?還記得USB是種Master-Slave BUS架構嗎?這種架構就意味著Host同一時間只能對一個裝置發出一種要求。所以問題就來了,Host要怎麼應付上層各種裝置的需求?當然就是Schedule了。

EHCI規格書定義兩種Schedule方式:一種是periodic schedule,另一種是asynchronous schedule

Asynchronous schedule是用來應付ControlBulk transfer的需求。因為他們並不要求即時性,所以這種schedule方式是非同步。

Periodic schedule是用來應付InterruptIsochronous transfer的需求。因為他們要求週期即時性。

1.3.6.1 Asynchronous Schedule

要開啟Asynchronous schedule,必須設定USBCMD內的Asynchronous Schedule Enable bit.

Host Controller開始Asynchronous schedule的時機:
1.      periodic schedule結束
2.      periodic schedule未啟動
3.      periodic List是空的

總之一句話,就是沒有InterruptIsochronous transfer要傳的時候才會啟動。

至於Host Controller要開始Asynchronous schedule的時候,它怎麼知道要去哪裡抓資料呢?那就是透過ASYNCLISTADDR這個register,下圖是示意圖:


ASYNCLISTADDR其實只是告訴Host Controller一個記憶體位址,那個記憶體位址包含一個Queue Head資訊:


請注意Queue HeadQH)可以構成一個環狀列,Queue Head Horizontal Link Pointer就是下一個Queue Head的記憶體位址。DeviceEndPt則是指定這個Queue Head所代表的Device+Endpoint組合。

前三列都是HCD要填的,後面九列則是稱為transaction working space,顧名思義就是Host Controller的工作區域。要傳輸的資料則是透過Queue Element Transfer DescriptorqTD)來取得:


每個qTD最多可以傳(4K*5) = 20KB資料。

所以整個結構會是:


至於Asynchronous schedule停止的時機:
1.          micro-frame結束
2.          沒有asynchronous transfer
3.          Asynchronous schedule被關閉
1.3.6.1.1 操作QH環狀列
接下來討論怎麼操作QH環狀列。要討論這個的原因是因為,當Host Controller在讀寫QH環狀列的時候,HCD是有可能新增或移除其中的某個QH,這會導致不可預期的結果,所以EHCI規格還特地說明HCD要怎麼跟Host Controller同步QH環狀列。

插入一個QH沒甚麼問題,直接插入即可。

移除一個QH倒是需要兩個步驟:
1.          停掉Asynchronous Schedule
2.          移除某個QH

移掉某個QH之後,HCD還必須確定Host Controller沒有在存取這個QH,不然HCD一但寫入這個QHHost Controller就有可能會出錯。這個確定的機制稱為Doorbell

Doorbell機制很簡單:
1.          HCDUSBCMD register內的Interrupt on Async Advance Doorbell bit設為1
2.          HCD開啟interrupt
3.          HCD等待interrupt,並確定USBINTR register內的Interrupt on Async Advance bit1
4.          HCD清除USBINTR register內的Interrupt on Async Advance bit
整個機制做完後,HCD就可以釋放這個QH的記憶體,或是重新拿來使用。

1.3.6.2 Periodic Schedule

要開啟Periodic schedule,必須設定USBCMD內的Periodic Schedule Enable bit.

Host Controller開始Asynchronous schedule的時機:

至於Host Controller要開始Periodic schedule的時候,它怎麼知道要去哪裡抓資料呢?那就是透過PERIODICLISTBASE這個register,下圖是示意圖:


Periodic Frame List是個4K-page aligned指標。Periodic Frame List的個數是有可能可以調整的,請參考HCCPARAMS register。如果不能調整的話,數量是1024個。

下圖是Periodic Frame List Element的格式,每個Element對應到一個 SOF



下圖是EHCI規格內對於Frame List整個結構的說明,剛開始看真的是一頭霧水:


沒關係,先來看Periodic Frame List Element指向的iTDIsochronous (High-Speed) Transfer Descriptor)結構:


一個iTD有八個Transaction Length欄位,所以它的目的就是希望能夠在每個SOF內最多可以每個micro-SOF都可以傳一次資料,這跟USB2.0規格相符。

所以如果要傳對某個裝置達到每個micro-frame都可以傳輸資料,整個frame list的結構如下:



Frame N對應到FRINDEX register內的bit[12-3],而micro frame則對應到FRINDEX register內的bit[2-0]

如果要傳對某個裝置達到每八個micro-frame都可以傳輸資料,整個frame list的結構如下:


1.3.7 Nak counter

有注意到Queue Head結構內有個NakCnt欄位嗎?


這個欄位基本上是用來增加Hostbulk/control傳輸反應速度的。為何這樣說?因為Asynchronous Schedule停止的原因有兩個:

1          全部QH環狀列都已經傳完。
2          所有QH環狀列的NakCnt都為0

所以如果NakCnt設很大,表示Host Controller會盡量在每個SOF內嘗試把資料傳出去。這樣做在對付哪種反應速度慢的裝置,可能可以讓速度變快一點。

下面是NakCnt遞減的法則:

基本上這種機制是用在對付EHCI接到high speed HUB但是裝置卻是full/low speed的狀況,這種狀況現在真的很少見了。

NakCnt不適用於interrupt transfer,理由很簡單,interrupt不需要這樣做。這次NAK就等下一次interval到吧。

1.4 Port Test mode

測試流程:
1          關掉Periodic以及Asynchronous schedule
2          讓所有enabled port進入suspend狀態
3          USBCMD register內的[Run/Stop]欄位設為0,停掉Host Controller。然後確定USBSTS register內的HCHalted欄位轉為1
4          設定PORTSC內的[Port Test Control]欄位:

1          如果是[Test FORCE_ENABLE]的話,需要把USBCMD register內的[Run/Stop]欄位設為1
2          測試完之後,確定Host ControllerHalt state,然後Reset Host Controller





codeblock