2010年6月28日 星期一

[PHP] 使用 cURL + HTTP REFERER + Cookie + File:自製 my_wget 下載資料存到檔案

對 wget 這個 tool 不熟,平常使用 wget 下載一些資料時,可以輕易地使用 --referer 來偽造 HTTP Header 資料,因此能夠通過對方 Server 檢查


wget --referer="REFERER_URL" "TARGET_URL"


然而,上述的 REFERER_URL 和 TARGET_URL 都是固定的位置,如果是會根據 session / cookie 的而改變的話,不曉得還有沒有辦法?對我而言,寫 PHP 比去看 manpage 來得快 XD 所以我就寫成 PHP 囉!或許 wget 也有更方便的下法吧,改天再努力看 manpage


程式碼:


<?php
$output_file = 'result.file';  // 儲存結果
$cookie_file = 'cookie.tmp';  // cookie file
$source_url = 'SOURCE_URL';  // 之後會變成 REFERER_URL
$pattern = '/class="download" href="(.*?)"/';  // 此為一個範例, 用來撈 TARGET_URL

$ch = curl_init();
curl_setopt( $ch , CURLOPT_URL, $source_url );
curl_setopt( $ch , CURLOPT_COOKIEFILE , $cookie_file );
curl_setopt( $ch , CURLOPT_COOKIEJAR , $cookie_file );
curl_setopt( $ch , CURLOPT_RETURNTRANSFER , true );

$result = curl_exec( $ch );

if( preg_match_all( $pattern , $result , $match ) )
{
        if( isset( $match[1][1] ) )
        {  
                $target_url = $match[1][1];  // 請依 pattern 決定
                $referer_url = $source_url;

                curl_setopt( $ch , CURLOPT_URL, $target_url );
                curl_setopt( $ch , CURLOPT_REFERER , $referer_url );
                curl_setopt( $ch , CURLOPT_COOKIEFILE , $cookie_file );
                curl_setopt( $ch , CURLOPT_COOKIEJAR , $cookie_file );
                //curl_setopt( $ch , CURLOPT_RETURNTRANSFER , true );

                $fp = fopen ( $output_file, 'wb' );

                curl_setopt( $ch , CURLOPT_FILE , $fp );

                echo "GO...\n";
                curl_exec( $ch );
                echo "Finish..\n";

                fclose( $fp );
        }   
}

curl_close( $ch );
?>


以上是要從 SOURCE_URL 上頭, 找到下載位置(target_url), 然而, 那個位置卻每次都不一樣, 最重要的是跟 session 有關係並且下載 target_url 時還必須奉上 cookie 資訊, 所以, 先收集一下 cookie 囉!(上述程式並不謹慎, 例如儲存結果的檔案有可能開檔失敗)


後記,無聊又改寫成 tool mode:


<?php

$shortopt = array();

$shortopt['h'] =  array(
    'value' => '' ,
    'text' => '-h, help' );
$shortopt['c:'] =  array(
    'value'    => '' ,
    'text'    => "-c '/tmp/cookie_file' , tmp file for cookie" );
$shortopt['o:'] = array(
    'value'    => '' ,
    'text'    => "-o '/tmp/output_file' , path for result file. default use stdout" );
$shortopt['u:'] = array(
    'value'    => NULL ,
    'text'    => "-u 'http://www.google.com' , source url" );
$shortopt['e:'] = array(
    'value'    => NULL ,
    'text'    => "-e '/class=\"normal-down\" href=\"(.*?)\"/is' , regexp pattern for extract the target url" );
$shortopt['m:'] = array(
    'value'    => '' ,
    'text'    => "-m '1,1' , choose the result matched to be used. e.g. use the match[5][2] is '5,2'" );
$shortopt['d'] = array(
    'value'    => 'true' ,
    'text'    => "-d , disable test mode for showing the target matched by regexp pattern" );

// check function
if( !function_exists( 'getopt' ) )
{
    echo "'getopt' is not supported in current PHP version.\n";
    exit;
}

// help menu
$shortopt_list = '';
$shottopt_help = '';
foreach( $shortopt as $k => $v )
{
    $shortopt_list .= $k;
    $shottopt_help .= "\t".$v['text']."\n";
}

// start to parse...
$parse_arg = getopt( $shortopt_list );

// show help
if( isset( $parse_arg['h'] ) )
{
    echo "Usage> php ".$argv[0]." -h\n";
    echo $shottopt_help;
    exit;
}

// set the value
foreach( $parse_arg as $k => $v )
{
    if( isset( $shortopt[$k] ) )
        $shortopt[$k]['value'] = !strcasecmp( $shortopt[$k]['value'] , 'false' ) ? true : false ;
    else if( isset( $shortopt[$k.':'] ) )
        $shortopt[$k.':']['value'] = $v;
}

// check value (cannot be NULL)
$check_out = '';
foreach( $shortopt as $k => $v )
    if( !isset( $v['value'] ) )
        $check_out .= "\t".$v['text']."\n";
if( !empty( $check_out ) )
{
    echo "Usage> php ".$argv[0]." -h\n";
    echo "Must Set:\n$check_out\n";
    exit;
}

$cookie_file = !empty( $shortopt['c:']['value'] ) ? $shortopt['c:']['value'] : NULL ;
$source_url = $shortopt['u:']['value'];
$output_file = !empty( $shortopt['o:']['value'] ) ? $shortopt['o:']['value'] : NULL ;
$regexp_pattern = $shortopt['e:']['value'];

if( !empty( $shortopt['m:']['value'] ) )
    $shortopt['m:']['value'] = trim( $shortopt['m:']['value'] );
$choose_match = !empty( $shortopt['m:']['value'] ) ? explode( ',' , $shortopt['m:']['value'] ) : NULL;
$test_mode = empty( $choose_match ) || $shortopt['d']['value'];

$ch = curl_init();
curl_setopt( $ch , CURLOPT_URL, $source_url );

if( !empty( $cookie_file ) )
{
    curl_setopt( $ch , CURLOPT_COOKIEFILE , $cookie_file );
    curl_setopt( $ch , CURLOPT_COOKIEJAR , $cookie_file );
}
curl_setopt( $ch , CURLOPT_RETURNTRANSFER , true );

$result = curl_exec( $ch );

if( preg_match_all( $regexp_pattern , $result , $matches ) )
{
    $target_url = getTargetURL( $matches , $choose_match );
    if( $test_mode || empty( $target_url ) )
    {
        echo "Matched Target URL: \n";
        print_r( $matches );
        echo "Choose option(Cannot be empty):".$shortopt['m:']['value']."\n";
        echo "Target(Cannot be empty):$target_url\n";
    }
    else
    {
        curl_setopt( $ch , CURLOPT_URL, $target_url );
        curl_setopt( $ch , CURLOPT_REFERER , $source_url );

        if( !empty( $cookie_file ) )
        {
            curl_setopt( $ch , CURLOPT_COOKIEFILE , $cookie_file );
            curl_setopt( $ch , CURLOPT_COOKIEJAR , $cookie_file );
        }

        if( !empty( $output_file ) )
        {
            echo "Target URL:$target_url\n";
            echo "Referer URL:$source_url\n";

            if( ( $fp = fopen ( $output_file , 'wb' ) ) == NULL )
            {
                echo "ERROR: Cannot open the output file to write:$output_file\n";
                exit;
            }
            curl_setopt( $ch , CURLOPT_FILE , $fp );

            echo "Begin...\n";
            curl_exec( $ch );
            echo "...Finish\n";
            fclose( $fp );
        }
        else
        {
            curl_exec( $ch );
        }
    }
}
curl_close( $ch );
exit;

function getTargetURL( $matches , $choose )
{
    if( !isset( $matches ) )
        return NULL;
    if( is_array( $matches ) && is_array( $choose ) && count( $choose ) > 0 )
    {
        $index = array_shift( $choose );
        if( isset( $matches[ $index ] ) )
            return getTargetURL( $matches[ $index ] , $choose );
        return NULL;
    }

    if( !is_array( $matches ) )
        return $matches;
    else if( isset( $matches[ $choose ] ) )
        return $matches[ $choose ];
    return NULL;
}
?>


用法:


單純以抓 Yahoo! New 為例


尚未指定 -m
# php my_wget.php -u 'http://tw.yahoo.com' -e '/<h3><a href="([^"]+)" title="([^"]+)"/is'


Matched Target URL:
Array
(
    [0] => Array
        (
            [0] => <h3><a href="news/a/h1/t/*http://tw.news.yahoo.com/article/url/d/a/100628/5/289yr.html" title="莫拉克風災 學者:無關暖化"
            [1] => <h3><a href="news/a/h2/t/*http://tw.news.yahoo.com/article/url/d/a/100628/69/289tr.html" title="立院藏七寶 總價數億元"
        )

    [1] => Array
        (
            [0] => news/a/h1/t/*http://tw.news.yahoo.com/article/url/d/a/100628/5/289yr.html
            [1] => news/a/h2/t/*http://tw.news.yahoo.com/article/url/d/a/100628/69/289tr.html
        )

    [2] => Array
        (
            [0] => 莫拉克風災 學者:無關暖化
            [1] => 立院藏七寶 總價數億元
        )

)
Choose option(Cannot be empty):
Target(Cannot be empty):


指定 -m '1,1'
# php my_wget.php -u 'http://tw.yahoo.com' -e '/<h3><a
href="([^"]+)" title="([^"]+)"/is' -m '1,1'


Matched Target URL:
Array
(
    [0] => Array
        (
            [0] => <h3><a href="news/a/h1/t/*http://tw.news.yahoo.com/article/url/d/a/100628/5/289yr.html" title="莫拉克風災 學者:無關暖化"
            [1] => <h3><a href="news/a/h2/t/*http://tw.news.yahoo.com/article/url/d/a/100628/69/289tr.html" title="立院藏七寶 總價數億元"
        )

    [1] => Array
        (
            [0] => news/a/h1/t/*http://tw.news.yahoo.com/article/url/d/a/100628/5/289yr.html
            [1] => news/a/h2/t/*http://tw.news.yahoo.com/article/url/d/a/100628/69/289tr.html
        )

    [2] => Array
        (
            [0] => 莫拉克風災 學者:無關暖化
            [1] => 立院藏七寶 總價數億元
        )

)
Choose option(Cannot be empty):1,1
Target(Cannot be empty):news/a/h2/t/*http://tw.news.yahoo.com/article/url/d/a/100628/69/289tr.html


正式要下載請記得加 -d (disable test) , 但此例不適用, 因為抓出來的 url 並不完整, 開頭只是 "news/a/h2/t/*....."
# php my_wget.php -u 'http://tw.yahoo.com' -e '/<h3><a
href="([^"]+)" title="([^"]+)"/is' -m '1,1' -d


輸出到檔案
# php my_wget.php -u
'http://tw.yahoo.com' -e '/<h3><a
href="([^"]+)" title="([^"]+)"/is' -m '1,1' -d -o '/tmp/output'


需要 cookie

# php my_wget.php -u
'http://tw.yahoo.com' -e '/<h3><a
href="([^"]+)" title="([^"]+)"/is' -m '1,1' -d -o '/tmp/output' -c '/tmp/cookie'


UIImageWriteToSavedPhotosAlbum Domain=ALAssetsLibraryErrorDomain Code=-3301 "寫入忙碌中"

前陣子在 iPhone 模擬器上寫之儲存照片的小程式,使用連續儲存的方式,發現可以很順利,但是移到實體機子上頭,卻發現儲存的照片張數不對,假設有 10 張照片要儲存,但真正存進去的只有 5 張不到,而且現象是有時張數多有時張數少,但一定沒有達到 10 張。


後來透過 error report 並在實體機上跑時才看到錯誤訊息:


Error Domain=ALAssetsLibraryErrorDomain Code=-3301 "寫入忙碌中" UserInfo=0xXXXXXX {NSLocalizedFailureReason=, NSLocalizedRecoverySuggestion=再試著寫入一次, NSLocalizedDescription=寫入忙碌中}


偶爾也還有看到 sqlite3 等訊息(但它有容錯處理)


sqlite error 5 [database is locked]
sqlite prepare statement retry was successful.  Continuing.


而真正的問題還是儲存照片問題,最後則是想到用 sleep 的方式解決,也就是避開短時間儲存,改成每存完一張 sleep 1.5 秒


- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error
contextInfo:(void *)contextInfo
{
        if( error != nil )
               
NSLog( @"SaveError(%f,%f) Message:%@", image.size.width,
image.size.height, error);
}

- (void)saveToPhotoLibrary
{
    for( id raw_data in [images allValues] )
    {
        UIImage *image = [[UIImage alloc] initWithData:raw_data];
        UIImageWriteToSavedPhotosAlbum( image, self, @selector(image:didFin    ishSavingWithError:contextInfo:), nil );
        [image release];
        [NSThread sleepForTimeInterval:1.5];
    }
}


修正後,還是偶爾會出現其他的訊息,並且隨著 Photos Library 內的圖片增加,導致訊息顯示會越來越頻繁甚至不正常當掉(當掉可能比較跟記憶體控制有關)


slow transaction: time was 0.xxxxxx seconds
Received memory warning. Level=1
Received memory warning. Level=2


暫時還沒想到恰當的解法(可以考慮依 Photos Library 的照片張數調整 sleep 秒數),未來設計上可能還是要避開把資料塞進 Photos Library 吧!


2010年6月27日 星期日

iOS 開發教學 - 讓 App 支援多國語言、依語言顯示 App 名稱

LocalizableString

iTunes App Store 可在於多個國家販售、散佈分享所寫的 App,因此,客製化不同語言的操作介面是很基本的功夫,概念上很簡單,凡是在 App 上使用 NSString 印出來的字串,都可以透過查表的方式,依照設定的語言,更改成想要的。

如:

UILabel *show = [[UILabel alloc] init];
show.textLabel.text = @"Hello Moto";

希望可以依照語言改變時,那就改用 NSLocalizedString 處理:

UILabel *show = [[UILabel alloc] init];

show.textLabel.text = NSLocalizedString( @"Hello Moto" , @"note" );

其中後面的 @"note" 也可以空白,那類似註解、用來回憶的,但如果要顯示的文字已經很有記憶性跟意義性,那其實後面就乾脆留白就好囉。透過上述的使用,如果沒有建立甚麼查表,那其實 show.textLabel.text = NSLocalizedString( @"Hello Moto" ,
@"note" ); 等同於
show.textLabel.text = @"Hello Moto"; 這樣的效果,也就是可以把一堆常用輸出的部分改成 NSLocalizedString 也不會怎樣。

接著建立查表,在此僅提供簡單的 source code 轉換的部分,關於使用 Interface Builder 也可以透過 Xcode 介面產出,只是我沒再用啦,所以也忘了。

$ cd YourProject ;
$ mkdir English.lproj ;
$ genstrings -o English.lproj/ iPhone/*.m iPhone/*.h iPad/*.m iPad/*.h *.m *.h

透過上述三步,就將 YourProject 裡頭所有的 *.m 和 *.h 裡頭用到的 NSLocalizedString 部份,拉出來見出可以查詢的表格,並儲存在 YourProject/English.lproj/ 中,其內文:

/* No comment provided by engineer. */
"Hello Moto" = "Hello Moto";

這等同於 Key-Value Pair 的模式,因此,如果你希望在繁體中文顯是為"你好機車",那對應則改成:

/* No comment provided by engineer. */

"Hello Moto" = "你好機車";

以上是簡短的範例。真正的使用的過程,一開始是使用 genstrings 產生出 Localizable.strings 檔案,接著把他拖拉進去 Project 中,這時彈跳的視窗記得要選成 UTF-16 ,而當你想要新增另一個語言時,則是對 Localizable.strings 點選右鍵挑選 Get Info,彈跳的視窗下面則可以直接按 Add Localization ,接著可以打入語言,如 zh_TW 、 zh_CN 等

utf16

Add Localization

如此一來,在 Localizable.strings 下則會自動幫你複製一份 zh_TW 出來,而 Project 目錄下也會自動產生 zh_TW.lproj 目錄。接著一樣的用法,只是變成在 zh_TW 語言時,想要顯示的字是甚麼。而 App 預設的語言是 English ,可以在 Project-Info.plist 中 Localization native development region 項目裡看到這個設定值。

經過上述的設定,你的程式就可以依照設備的語言,顯示內容囉,然而,還有一種設定,那就應用程式在設備上顯示的名稱,此部分則是另外在建立一個檔案 InfoPlist.strings 並且拉到 Project 裡頭,一樣挑選 UTF-16 ,其內文:

CFBundleName = "HelloMoto";  
CFBundleDisplayName = "HelloMoto";

一樣可以透過對 InfoPlist.strings 進行 Get Info 和 Add Localization 來建立其他語言的顯示。

木偶人動畫電影版"BBS鄉民的正義"預告片





 


第一次是看鐵拳無敵孫中山,這次預告片拍得很讚!看到這些影片,都讓人想起 BBS 對台灣人的影響,有的人覺得那邊只不過是個純文字的瀏覽介面,再不然頂多加個 ASCII 圖片,或更進階成一秒秒切換成的動畫等等,除此之外,還有人設計相關的程式互動,讓你玩遊戲甚至在上頭寫寫程式等等的。但重要的並不是這些,因介面的簡單,少了許多像網站的廣告;因 telnet 協定,讓切換頁面用的資源更少更快;最重要的,裡頭的鄉民、文章、板規、站規不就等同於一個小型社會嗎?人才是最珍貴的價值,以 PTT 來說,技術上已經允許同時上線人數突破 15 萬關卡了!像最近的世足賽,其 WorldCup 可以同時超過一萬人的板友互動哩,當然,還有記者最愛挖新聞的八卦板,幾乎每次看都一定有上千人在裡頭。


前陣子高中的 BBS 站復站了,回憶起 1999 年的時光,過了十年後,到底還需不需要 BBS 呢?我想,這早就已是另一種台灣的文化了。有人再探討需要或去除哪些文化嗎?一切就讓他靜靜地翻騰吧


更多資訊請上 木偶人動畫專區


 






























99交通大學畢業 MV








很讚的 MV ,就算畢業了三年,很多場景就像昨天一般,瞬地轉入了眼簾。