小松鼠嚇了一跳,有了魔法眼鏡後,這世界看起來完全不一樣了

2014年11月30日 星期日

用 Spynner 來抓 8Comic 的漫畫 (4): 多線程


平行化的好處

之前抓檔案的方式是,
  1. 用瀏覽器抓 .html,找到圖片 url。
  2. 下載圖片
  3. 換下一頁,跳到第一步。
但是網路可以同時開好幾個連線。所以我們要利用這點來加速。我們的策略是,
  1. 用瀏覽器抓 .html,找到圖片 url。
  2. 把圖片 url 丟進一個 Thread Pool 裡面(平行下載,但是不等待)。
  3. 換下一頁,跳到第一步。
當然也有其他的方式平行化,比方連抓 .html 的工作也一併丟入 Thread Pool。不過這樣代表要多開好幾個 browser,而且相對來說,抓網頁會比抓圖片快,所以我們選擇上面的方式。

import 將會用到的 module

因為使用 python 2.7, 我們利用 urllib2 來抓圖, ThreadPool 來 multi threading 平行處理。
In []:
import spynner
import os, sys
from PyQt4.QtWebKit import QWebSettings # 用來設定 QtWebKit
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest # 控制 browser 的網路連線
from PyQt4.QtCore import QUrl # Qt 的 Url 類別

# 下面是新增的兩個 module
import urllib2
from multiprocessing.pool import ThreadPool

# 下面是 IPython 相關
from IPython.display import display, Image
from IPython.html.widgets import ImageWidget, IntProgressWidget

建立瀏覽器

這部份一樣
In []:
# 建立瀏覽器
browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)

# 建立一個 webview
browser.create_webview()
settings = browser.webview.settings()
# settings.setAttribute(QWebSettings.AutoLoadImages, False)
settings.setAttribute(QWebSettings.JavaEnabled, False)        # 不需要  Java
settings.setAttribute(QWebSettings.DnsPrefetchEnabled, True)  # 試著節省 Dns 花的時間
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) # 不需要瀏覽紀錄或者 cookie

# 建立一個空的  url
BLANK_REQUEST = QNetworkRequest(QUrl())
# 建立一個空的圖片 url
DUMMY_IMG_REQUEST = QNetworkRequest(QUrl("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="))

# 客製化的 NetworkAccessManager
class EightComicNetworkAccessManager(QNetworkAccessManager):
    # 只需要取代  createRequest 這個 method 即可 
    def createRequest(self, op, request, device=None):        
        url = str(request.url().toString()) # 參數很多,但只取 url 就夠用        
        if 'comic' not in url[:20]: 
            # 用很醜的方式來判斷非 8comic 網站的 url 
            # 用空的 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, BLANK_REQUEST)
        elif not url.endswith('js') and not url.endswith('css') and '.html' not in url:
            # 凡是  .js .css .html 之外的,都用空的圖片 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, DUMMY_IMG_REQUEST)
        else:
            # 傳回原本的 url
            return QNetworkAccessManager.createRequest(self, op, request, device)

# 設定  browser 的 NetworkAccessManager
browser.webpage.setNetworkAccessManager(EightComicNetworkAccessManager())

設定 Widget

增加一個 Progress Bar, 分別來顯示分析過的 .html 數字以及已經下載的圖片數
In []:
browser.show()
# 漫畫的網頁
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='

# 要下載第一本
book_no = 1

# 取得總頁數
browser.load(base_url+str(book_no))
total_pages = browser.runjs('ps').toInt()[0] 

# 建立 Image Widget 用來顯示圖片預覽
img = ImageWidget()
img.set_css("height", 300) # 讓圖片不要太大

# 顯示下載進度的 Progress bar
html_progress = IntProgressWidget(min=1, value=1, max=total_pages)
img_progress = IntProgressWidget(min=1, value=1, max=total_pages)

# 顯示 Widget
display(html_progress)
display(img_progress)
display(img)

利用 ThreadPool 來下載

另用 ThreadPool 來達成 multithreading (多線程) 即為容易,只要將想丟進 pool 的程式碼包進函數裡面即可。
In []:
# 建立一個下載目錄
dir_name = "download/{:02d}".format(book_no)
if not os.path.exists(dir_name):
            os.makedirs(dir_name) 
print "Download to {}/{}".format(os.getcwd(), dir_name)
sys.stdout.flush()

# 建立 ThreadPool, 5 條 thread
pool = ThreadPool(5)
        
# 開始下載
downloaded_images = 0
for page in range(1, total_pages+1):
    # 取得 image url
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())

    # 將下載圖片的工作包成 save_img,推進 pool 裡
    def save_img(img_url, page):
        global downloaded_images
        fn = "{}/{:03d}.jpg".format(dir_name, page)
        data = urllib2.urlopen(img_url).read()
        with open(fn, "wb") as f:
            f.write(data)
        # 更新 widget 的狀態
        downloaded_images += 1
        img_progress.description = "img: %d/%d"%(downloaded_images, total_pages)
        img_progress.value = downloaded_images
        img.value = Image(filename=fn).data
    pool.apply_async(save_img, (img_url, page))
    
    # 更新 Widget 的狀態
    html_progress.description = "html: %d/%d"%(page, total_pages)
    html_progress.value = page

    # 等待所有任務結束
pool.close()
pool.join()
    

結果

到這裡,探索期正式結束,該有的技術已經完整。
有興趣的話,也可以實測比較一下有 multithreading 和沒有 multithreading 的差異。但這裡就發現我們需要封裝了,因為沒有封裝、整理好的關係,要測試兩種程式碼,必須要把程式碼寫兩遍,而無法共用相同的部份。
另外,程式碼裡面用到了 global 這個 keyword, 常常也代表我們需要封裝了。



用 Spynner 來抓 8Comic 的漫畫 (3): 節省頻寬


要解決的問題

  • 還是有文字廣告這樣不需要的流量來浪費頻寬和時間。
  • QtWebKit 的 QWebSettings.AutoLoadImages=false 有 bug,記憶體已用不還。

解法

如同這裡 http://stackoverflow.com/questions/21357157/is-there-any-solution-for-the-qtwebkit-memory-leak 以及相關的連結、討論,記憶體的 bug 目前找得到的解法就是定期砍掉 process 重新再開。
理論上,也可以深入 QtWebKit, 找到配置的記憶空間,然後手動釋放,順便將解法回給上游。這樣雖然是正解,但是與我們的主題無關。
既然無解,那要等 QtWebKit 修正 bug 之後再來抓? 太久。
定期砍 Process 重開? 可以,但太醜。
不用 QtWebKit, 改用其他套件來解? 可以,但其他套件可能有其他問題,人生不能一直逃避,會養成習慣的。
仔細思考,其實我們的目標是要讓 browser 不去抓不必要的網路資源,這個原則包含了圖片以及文字廣告,但不僅限於這兩個,還有像是 google 統計等等。
所以只要我們封鎖這些不必要的連線,看起來瀏覽器的圖片就不會被顯示了,不需要真的設定 QWebSettings.AutoLoadImages=false
比方可以設定 proxy,由 proxy 來控制網路資訊流。不過 QtWebKit 有幾個內建的功能,可以讓你控制網路的存取,比 proxy 更裡層一點。一個是 browser.webpage 的 acceptNavigationRequest, loadStarted, loadFinished. 這幾個搭配起來,你可以限制 browser 不抓取子 iframe。 但這個方法對我們來說,不夠用。我們想要控制更多,所以我們用另外一種方式,直接控制比較外層一點的 QNetworkAccessManager 。

一樣 import 所有我們將會用到的東西

In []:
import spynner
import os, sys
from PyQt4.QtWebKit import QWebSettings # 用來設定 QtWebKit
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest # 控制 browser 的網路連線
from PyQt4.QtCore import QUrl # Qt 的 Url 類別

# 下面這行是 IPython 相關
from IPython.display import display, Image
from IPython.html.widgets import ImageWidget, IntProgressWidget

建立瀏覽器

In []:
# 建立瀏覽器
browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)

# 建立一個 webview
# 我們不設定 AutoLoadImages=False, 但增加一些其他設定
# 這裡並不是重點,但適合我們的應用
browser.create_webview()
settings = browser.webview.settings()
# settings.setAttribute(QWebSettings.AutoLoadImages, False)
settings.setAttribute(QWebSettings.JavaEnabled, False)        # 不需要  Java
settings.setAttribute(QWebSettings.DnsPrefetchEnabled, True)  # 試著節省 Dns 花的時間
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) # 不需要瀏覽紀錄

建立一個 QNetworkAccessManager 子類別

當 browser.webpage 在要求網路資源前,會先詢問 QNetworkAccessManager 來確定要不要抓,或者怎麼來抓這個資源。 我們可以用 browser.webpage.setNetworkAccessManager 來指定自己客製過的 manager。
In []:
# 建立一個空的  url
BLANK_REQUEST = QNetworkRequest(QUrl())
# 建立一個空的圖片 url
DUMMY_IMG_REQUEST = QNetworkRequest(QUrl("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="))

# 因為只需要用一次,可以取個又臭又長的名字
class EightComicNetworkAccessManager(QNetworkAccessManager):
    # 只需要取代  createRequest 這個 method 即可 
    def createRequest(self, op, request, device=None):        
        url = str(request.url().toString()) # 參數很多,但只取 url 就夠用        
        if 'comic' not in url[:20]: 
            # 用很醜的方式來判斷非 8comic 網站的 url 
            # 用空的 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, BLANK_REQUEST)
        elif not url.endswith('js') and not url.endswith('css') and '.html' not in url:
            # 凡是  .js .css .html 之外的,都用空的圖片 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, DUMMY_IMG_REQUEST)
        else:
            # 傳回原本的 url
            return QNetworkAccessManager.createRequest(self, op, request, device)

# 設定  browser 的 NetworkAccessManager
browser.webpage.setNetworkAccessManager(EightComicNetworkAccessManager())

後面的程式碼都一樣

In []:
# 漫畫的網頁
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='

# 顯示瀏覽器,確認 browser 內容乾淨清爽
browser.show()

# 要下載第一本
book_no = 1

# 取得總頁數
browser.load(base_url+str(book_no))
total_pages = browser.runjs('ps').toInt()[0] 

# 建立 Image Widget 用來顯示圖片預覽
img = ImageWidget()
img.set_css("height", 300) # 讓圖片不要太大

# 顯示下載進度的 Progress bar
progress = IntProgressWidget(min=1, value=1, max=total_pages)

# 顯示 Widget
display(progress)
display(img)

# 建立一個下載目錄
dir_name = "download/{:02d}".format(book_no)
if not os.path.exists(dir_name):
            os.makedirs(dir_name) 
print "Download to {}/{}".format(os.getcwd(), dir_name)
sys.stdout.flush()

# 開始下載
for page in range(1, total_pages+1):
    # 取得 image url
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
    # 下載圖片
    fn = "{}/{:03d}.jpg".format(dir_name, page)
    with open(fn, "wb") as f:
        browser.download(img_url, outfd=f)
    
    # 更新 Widget 的狀態
    progress.description = "%d/%d"%(page, total_pages)
    progress.value = page
    img.value = Image(filename=fn).data

結果

和第一篇的結果比較,
的確清爽很多,廣告和圖片都消失了。速度也快了很多。其實 .css 檔案也可以不用抓,不過因為有 cache 的緣故, .css 和 .js 本來都只會抓一次,所以影響有限。
因為這一部分的探索已經告一段落,所以現在是封裝的時候。不過因為之後我們要介紹兩種不同的封裝方式,所以在這之前,我們要再做一件事情,那就是更加節省一點。
看起來不是我們不是已經夠節儉了嗎?只抓有需要的東西,沒有多抓任何東西。 也許 .html 也可以不用抓?沒錯,也許可以直接反推出每一頁漫畫圖片的 url,但即使這個例子可以,但一般來說,伺服器端完全可以將必要的資訊放在 .html 裡面,讓你必須要抓 .html 才能獲得必要資訊。 這系列的目的是介紹一個簡單的萬用抓資料方式,所以做到這裡就可以了。
那還有什麼可以節省的? 頻寬就這樣了,但是時間還可以節省。 下一篇介紹簡單的 multithreading 抓圖。
In []:



2014年11月29日 星期六

用 Spynner 來抓 8Comic 的漫畫 (2): 用 Widget 美化介面


已經可以抓了,還有什麼問題?

  • browser 瀏覽頁面時,已經顯示圖了。之後,又再 download 一次,浪費頻寬。
  • 介面不夠美觀,無法看到進度。

頻寬問題

概念上,有兩個方向。 一是既然 browser 顯示了圖片,表示 browser 有這份圖,我們跟 browser 要就好了。另一個剛好相反,告訴瀏覽器,不要顯示圖片,把圖片的 url 交給我們即可。
這兩個方向各有利弊,以現在這個應用來說,我選擇第二個。原因有三:

  •  QtWebKit 有選項讓你這樣做。 
  • 這樣可行。 browser 仍然會傳回正確的圖片 url。 
  • 可以順便擋住廣告圖片。

介面問題

因為我們用使用 IPython notebook,所以使用 IPython notebook 的 interactive widget。

接下來,一樣先 import 所有我們將會用到的東西

In []:
import spynner
import os, sys
from PyQt4.QtWebKit import QWebSettings # 用來設定 QtWebKit
# 下面是 IPython 相關
from IPython.display import display, Image
from IPython.html.widgets import ImageWidget, IntProgressWidget

再來是建立瀏覽器,並且設定不要載入圖片

In []:
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='
# 建立瀏覽器
browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)

# 建立一個 webview,並且設定不要自動載入圖片
browser.create_webview()
settings = browser.webview.settings()
settings.setAttribute(QWebSettings.AutoLoadImages, False)

# 要下載第一本
book_no = 1

# 取得總頁數
browser.load(base_url+str(book_no))
total_pages = browser.runjs('ps').toInt()[0] 

再來是建立 Interactive Widget

In []:
# 建立 Image Widget 用來顯示圖片預覽
img = ImageWidget()
img.set_css("height", 300) # 讓圖片不要太大

# 顯示下載進度的 Progress bar
progress = IntProgressWidget(min=1, value=1, max=total_pages)

下載

跟之前一樣,不過外加進度顯示
In []:
# 顯示之前建立的 Widget
display(progress)
display(img)

# 建立一個下載目錄
dir_name = "download/{:02d}".format(book_no)
if not os.path.exists(dir_name):
            os.makedirs(dir_name) 
print "Download to {}/{}".format(os.getcwd(), dir_name)
sys.stdout.flush()

# 開始下載
for page in range(1, total_pages+1):
    # 取得 image url
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
    # 下載圖片
    fn = "{}/{:03d}.jpg".format(dir_name, page)
    with open(fn, "wb") as f:
        browser.download(img_url, outfd=f)
    
    # 更新 Widget 的狀態
    progress.description = "%d/%d"%(page, total_pages)
    progress.value = page
    img.value = Image(filename=fn).data

看起來還不錯?

這樣似乎看起來還不錯,沒有幾行程式碼,邊抓還能邊看 preview。 盯著朝著目標奔跑的 progress bar,有種莫名的療癒效果。而且沒有重複抓圖,省了不少頻寬。似乎探索期已經結束,接下來只要把這些程式碼封裝起來,方便重複使用就行了。
但仔細一看,還是有不少問題:

  •  如果用 browser.show() 打開瀏覽器,會發現還是有許多廣告被載入。我們希望這些廣告也不見。 
  • 用了一段時間之後,發現機器越來越慢,仔細一看,記憶體使用量大得驚人。相當詭異。 
第一個問題的成因很單純,有些廣告是文字廣告。第二個問題就比較棘手一點了, QWebSettings.AutoLoadImages 有 bug,會造成記憶體無法回收的問題: http://stackoverflow.com/questions/21357157/is-there-any-solution-for-the-qtwebkit-memory-leak 仔細查看討論之後,會發現這個問題目前基本無解,要等 Qt 解決。(不然就只能很醜的 fork,然後 close process。
那怎麼辦? 放棄這個方案? 碰到無解的問題,如果是駭客(hacker),要深入系統,修正 bug,釋放記憶體。身為自造者(maker),那要捨棄壞掉的系統,自幹一個。
但不過是抓個漫畫,這樣搞,未免太累了點。下一篇,我們要用廢客(faker)的方式來解決問題。那就是,假裝解決問題就行了。
In []:



2014年11月24日 星期一

用 Spynner 來抓 8Comic 的漫畫 (1): 基本技術



需要套件

  • Spynner (需要 PyQt4 或 PySide, autopy)
  • IPython notebook (因為這個範例是用 IPython notebook 示範,不然跳過 IPython 相關部份也行) 安裝 先安裝 Python, IPython notebook, PyQT4

在 mac 下(如果 autopy 安裝不起來):

  • 先安裝 Qt。 brew 的話, brew install qt 即可。
  • 安裝 PyQt 。 brew, pip 都行。
  • easy_install -N spynner
  • 在適當的地方, touch autopy.py,如 touch /usr/local/lib/python2.7/site-packages/autopy.py。假裝有 autopy 就行了,因為 autopy 其實用不到。

在 windows 下:

  • 安裝 python(x,y) 2.7
  • 接下來打開 IPython 然後輸入 !easy_install spynner(win8 可用搜尋找到 IPython)
  • 最後,打開 IPython notebook,按下 New notebook 開始。
In []:
# This is for windows 
# on linux, simply sudo easy_install spynner in command line
!easy_install spynner 
# restart the kernel

先 import 所有我們將會用到的東西

In []:
import spynner
import os, sys

# 下面這行是 IPython 相關
from IPython.display import display, Image
In []:

再來我們試試看建立瀏覽器

browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)
如果看起來什麼事情都沒發生,那大概就對了。 spynner 已經在背景建立了一個 webkit 瀏覽器(叫做 browser)。
通常我們不需要 browser 真的被顯示出來,不過為了方便了解發生了什麼事情,我們先讓它能夠被顯示。
In []:
browser.show() # 告訴  browser,要它之後不要隱身
# 為了避免法律上的疑慮,這裡你要自己找到適當的 url,把 ???? 換掉
base_url = 'http://???.com/show/????-????.html?ch='  
browser.load( base_url+'1')
這時候,成功的話,一個瀏覽器會跳出來,顯示漫畫第 1 話的封面。
瀏覽器能夠改變大小,但是看來像是當掉一樣,沒有回應。
這其實是好事,因為我們希望能夠完全控制瀏覽器,所以先凍結它,再慢慢來蹂躪它。
接下來,我們要把封面圖的 url 抓出來。
In []:
browser.load_jquery(True)   #  spynner 內建有 jquery,用這個 method 載入,比較方便。
img_url = str(browser.runjs('$("#TheImg").attr("src")').toString())
print img_url
# 當然不用 jquery 也可以
img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
print img_url
上面先用 runjs 跑 javascript 得到一個結果。
這個結果是一個 Qt (C++)物件,可能是數字、字串或者物件。因為我們知道我們要的是字串,所以用 .toString 讓他成為一個 Qt 字串。
最後,再用 str 轉成 Python 字串。

抓圖

知道了圖片的 url, 那要如何將圖片抓下來呢?
可以用 browser.download(img_url, outfd=fd) 直接下載到檔案裏面。
不過這裡先直接在 IPython notebook 裡面秀一下圖片。
In []:
# 直接顯示 url 看看
display(Image(url=img_url, width=200))
In []:
# 先用 browser 抓下圖檔內容, 然後顯示
display(Image(data=browser.download(img_url), width=200))
漫畫每一頁的 url 格式是 .......ch=M-N 其中 M, N 是數字, 分別是卷數及頁數, 所以現在我們只要知道有幾頁就行了。
一般來說,可以從 html 內容中找到資訊。 8comic 控制 UI 的 javascript 就有這個資訊了,我們直接利用。
一樣先用 runjs 得到 ps 這個 javascript 變數的內容, 然後轉成整數。
因為 toInt 的結果包含一些額外資訊,所以我們用 [0] 取出數字。
In []:
total_pages = browser.runjs('ps').toInt()[0] 
print total_pages
所以我們用一個迴圈把每一頁都抓下來吧
In []:
book_no = 1
for page in range(1, total_pages+1):
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())
    print page, img_url
    display(Image(url=img_url, width=100))
    continue
    # 上面只是顯示每一頁的圖片
    # 如果你現在就想真的抓檔案下來, 把上面那個 continue 註解掉
    with open("{}-{}.jpg".format(book_no, page), "wb") as f:
        browser.download(img_url, outfd=f)
        print "File saved in", os.getcwd()

到這裡為止,基本的功能已經有了,下一篇將會討論一些細節問題。


題外話。
寫這篇一部份是因為「如何用 Python 抓網站內容」的詢問度一直很高,另一部分是因為發現  JComicDownloader 無法抓 8Comic 的圖。我看了一下,覺得這是一個不錯的例子。




2014年11月21日 星期五

地堡系列、14 號門、起點人系列、黑塔系列、別相信任何人、控制、記憶傳承人


這半年看的小說。
地堡系列

羊毛記看起來還挺不錯的,特別是第一段。故事的感情與情境、情節融合的很好。
續集星移、塵土故事也說得不錯,情節很吸引人。雖然也是一個未來反烏托邦的故事,但是主角(或者說作者)比飢餓遊戲、分歧者這類的要成熟一點。對這點感冒的人,可能會比較喜歡這本。整個故事的謎底還算令人滿意。
整體來說,相當推薦,是好看的小說。當初買來羊毛記之後,我立刻熬夜讀完。

14 號門


這本的情節很吸引人,但是謎底讓人有點失望。不是特別爛的結局,但感覺只是一個普通的結局。
故事敘述一間公寓中,充滿著各種詭異的事情。一個被重重鎖住,相當可疑的門。奇怪的規矩和事件。中間解謎的過程可以說是緊張、懸疑中帶著歡樂。我還挺享受這一段的閱讀。相比之下,謎底和結局也不是那麼重要了。
推薦程度中等。

起點人、終點人

這個系列也是以青少年當主角、青少年、兒童取向的反烏托邦小說。但跟飢餓遊戲和分歧者比起來,比較適合成年人的口味一點點。
設定很有趣,講的是一個某種原因之下,所有的大人都死亡,只剩下起點人(小孩)和終點人(老人)的世界。老人掌握資源和權力,可以制定法律。這樣的法律表面上看起來冠冕堂皇,但沒有例外的偏袒掌權者。沒有老人照顧的小孩,因為未成年,連工作權也沒有。
故事情節也不差。其中的謎團解答繞了幾圈,還算有趣。
推薦程度中上。

黑塔系列

黑塔系列很有名,評論的人也相當多。我看了一到七集,還沒看外傳。
對我來說,第一本不會構成障礙。不過第一本的確故事性較低。其實整個黑塔系列的故事進展都不算太快,就像史蒂芬金的其他小說一樣。有趣的是情境、心境描述、氣氛營造。
這個系列我從圖書館每個星期兩本兩本的借,每一本的感覺都不太一樣。但多半都挺好看的。其中一本是出門時從圖書館借出,然後搭捷運的來回途中就看完了。回來剛好直接還書,借下一本。但其中讓我感受最深的是結局。以這個系列的份量來說,這是一個完美的結局。稍微有一點驚訝,一點失落,但也很感動。不是那種會掉眼淚的感動,而是感受到整個世界的深度。
這本是否推薦是見仁見智,因為相對來說需要花一些力氣來讀。不過如果你能接受史蒂芬金的文字風格的話,我覺得會感受很多。

別相信任何人

從一開頭就相當吸引人。主角在設定上雖然年齡不小了,但是在心智成熟度上,感覺跟飢餓遊戲的主角差不了太多。
主角有失憶症,所以寫本日記當成外部記憶體。也因此常會出現故事中的故事中的故事(像是小說中的主角讀著日記裡面寫著聽到別人說的事情)。因為這樣的設定,作者可以巧妙的運用各式各樣的手法技巧,來誘導、誤導、引導你。讓你分不清真假。
各種線索間的衝突,新事證(或者偽證)不斷的像是擠牙膏一樣的慢慢被擠出來。爆點、疑點不斷,相當過癮。
結局不算驚豔,但至少還合格。
非常推薦。

控制

這本我讀英文版,讀了好久,前後大概快一個月吧。雖然別相信任何人也是讀英文,但份量多很多,而且用字也稍微難一點。
控制跟別相信任何人一樣,主軸是夫妻的事情。一開始的情節並不像「別相信任何人」那麼吸引人,但也不算無聊。
第一部的內容也參雜了許多日記,所以前面說的技巧,也一樣可以使用,只是沒有那麼霸氣外露,相對看起來比較平淡。即使是真實的場景,作者也用了一些手法來處理真假難分的氣氛。各種線索也像擠牙膏一樣慢慢擠出來。不過這些線索不像「別相信任何人」一樣接連引爆,很多只是默默的埋梗。
但剛開始相對平實的步調只是用來襯托後來的瘋狂而已。從第一部大約三分之二開始,勁道就開始變強。前面的部份開始發酵。前面埋的一些梗開始爆了。
第二部就直接露出瘋狂的本性,情節突然加快。除了埋更多梗之外,前面的梗也接二連三引爆。
到了第三部以後,更可以說是超越了瘋狂,直接到達了變態的境界。前面你以為已經爆開的地雷,到這裡居然又爆開了一次。
如果喜歡閱讀的人,這本非常推薦。

記憶傳承人

讀完前面兩本後(中間只看了鋼之鍊金術師漫畫),讀這本有種失速的感覺。
除了內容上的差異外,份量上也輕薄很多。
主角設定比之飢餓遊戲、分歧者、向達倫這類比較現代的青少年小說,沒有那麼.......要怎麼說,中二?這樣可能有點濫用。總之,就是沒有特別血緣,也不會設定成太勇敢、太特別、特救世主、太主角投射、太瑪麗蘇。主角就是很平常的青少年,帶有一點叛逆沒錯,有一點點的「特異能力」沒錯,但沒有那種莫名其妙的多愁善感。
開頭的設定沒有太驚奇之處,情節描述也很平淡。但還算有趣,而且進展很快(因為篇幅小),所以讀起來沒什麼問題。中後段開始漸入佳境,幾個爆點不錯,頗有狂人日記的禮教吃人感覺。
後段情節開始翻轉,有種青少年版的重裝任務的感覺。就當翻轉中又有轉折,計畫趕不上變化,開始讓人期待的時候,慢慢發現感覺有點怪,進展有點緩慢。然後情節戛然而止。
這時候你就會開始計較設定模糊奇怪、謎底沒有解答。
持平而論,這一系列有四本,也許看到後面會好一點,
推薦程度: 比較推薦給小朋友看。大人看也可以,但是不要期待太高,注意裡面的優點即可。