月度归档:2012年11月

Windows中的UTF-8与Unicode实验

查看“汉”字的Unicode编码(小尾)是496C,过程:(实际这里是UTF-16的编码,由于它在使用2字节编码的时候跟UCS-2编码一致,所以可以通过这个方式直接查看到它的UCS-2编码)

使用16进制编辑器打开:

小尾: 496C的二进制是0100 1001 0110 1100,如果转换为UTF-8,496C落在000800 – 00D7FF中,使用这个二进制分别套入1110xxxx 10xxxxxx 10xxxxxx格式中(从左到右替代x),得到11100100 10100101 10101100(E4 A5 AC)。
大尾:6C49的二进制0110 1100 0100 1001,如果转换为UTF-8, 6C49落在000800 – 00D7FF中,使用这个二进制分别套入1110xxxx 10xxxxxx 10xxxxxx格式中(从左到右替代x),得到11100110 10110001 10001001(E6 B1 89)。
验证结果[UTF-8编码]:(应该是E6 B1 89 或 E4 A5 AC)

“汉”字的GB编码为BABA,如下:

可以看到所谓的保存为ANSI在当前中文系统下就是GB编码,如果是繁体中文系统下,这个ANSI就是指所谓的BIG5。

在Windows中,文本文件可以保存的编码有:

ANSI:        	无格式定义;
Unicode:       	前两个字节为FFFE;
Unicode big endian: 	前两字节为FEFF; 
UTF-8:        	前两字节为EFBB;

这里保存为ANSI编码的就是使用系统默认编码(内码)来打开的。默认编码随系统不同而不同。

关于UTF-16的疑惑
首先是UTF-16的编码规则:

对于U+10000—U+10FFFF之间的字符,取原来编码减去0x10000之后,换成二进制取后20位,按每10位分别对应填入y和x中。

比如:
0x2A6A5这个编码
0x2A6A5 – 0x10000 = 0x1A6A5 = 0001101001 1010100101 对应入格式:

0x2A6A5 转成UTF-16编码是 0xD869 0xDEA5

接下来新建一个Unicode(大尾文件)

然后在16进制编辑器中打开此文件,再往文件里面输入:
FE FF D8 69 DE A5
保存打开:

这个字据说是笔画数最多的汉字,由四个繁体龙子组成。这里也充分证明了Windows里面所谓的Unicode编码实际是指UTF-16(区分大尾小尾),至于UTF-8编码它叫UTF-8,而UTF-16它不叫UTF-16而叫Unicode实在是让人不解了,难道它认为UTF-16和Unicode等同?

事实上,当前的Unicode码范围是0000 – 10FFFF,超过这个最大值的就可以认为不是Unicode码了,上例中的0x2A6A5小于0x10FFFF,而它对应的UTF-16编码都是大于10FFFF的(使用4字节的情况)。

至此,疑惑已去。

参考:
http://zh.wikipedia.org/wiki/UTF-8
http://zh.wikipedia.org/wiki/UTF-16

原创文章,转载务必保留出处。
永久链接: http://blog.ifeeline.com/228.html

关于字符编码的基本知识

UTF的字节序和BOM
UTF-8以字节为编码单元,没有字节序的问题(最小单位是字节)。 Unicode规范中推荐的标记字节顺序的方法是BOM。BOM是Byte Order Mark。在UCS 编码中有一个叫做”ZERO WIDTH NO-BREAK SPACE”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议在传输字节流前,先传输 字符”ZERO WIDTH NO-BREAK SPACE”。

如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符”ZERO WIDTH NO-BREAK SPACE”又被称作BOM。

UTF -8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符”ZERO WIDTH NO-BREAK SPACE”的UTF-8编码是EF BB BF。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
Unicode、UCS和UTF
从ASCII、GB2312、GBK到GB18030的编码方法是向下兼容的。而Unicode只与ASCII兼容(准确地说,是与ISO-8859-1兼容),与GB码不兼容。
Unicode 也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是”Universal Multiple-Octet Coded Character Set”,简称为UCS。UCS可以看作是”Unicode Character Set”的缩写。
UCS规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、UTF-16。
UCS-2、UCS-4、BMP
UCS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。
UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。
UCS -4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为 256行 (rows),每行包含256个cells。
group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。
将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。
big endian和little endian
big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。
一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。

最小单位不是字节的编码就存在字节序的问题,比如说UTF-8编码某个字符时用了3个字节,而说UTF-16编码某个字符时用来2个字,由于最小单位是字,两个字节一个字,就存在哪个字节先取哪个自己后取的问题。

unicode
左边8位,目前用到的是00~10(0000 0000 ~ 0001 0000)共17组,每组称为平面(Plane),每个平面有65536个码位(2字节,16位),共1114112个(65536 * 17)。

其中第0个平面称为基本平面(BMP),最常见的字符都放在这个平面,剩下的字符都放在辅助平面(SMP 16个)。

Unicode的编码方法:UTF-32 UTF-16 UTF-8

UTF-32
4个字节表示一个字符,完全对应Unicode编码,查找效率高,但浪费空间

UTF-16
基本平面的字符占用2字节,辅助平面的字符占用4个字节。辅助平面的字符位共有2^20个,需要20个二进制位。UTF-16将这20位拆成两半,前10位映射在U+D800到U+DBFF,称为高位(H),后10位映射在U+DC00到U+DFFF,称为低位。
utf-8

UTF-8

永久链接:http://blog.ifeeline.com/226.html

逗号分隔文件(CSV文件) 和 制表符分隔文件

逗号分隔文件(CSV文件)
基本规则
CSV格式是分隔的数据格式,有字段分隔的逗号字符和行分隔换行符。字段包含特殊字符(逗号,换行符,或双引号),必须以双引号括住。行内包含一个项目是空字符串,可以以双引号括住(也可以不用)。字段的值包含双引号时,要双写这个双引号(就像把一个双引号当做转义符一样)。CSV文件格式并不需要特定的字符编码(字符编码),字节顺序,或行终止格式(使用行分隔符)。

示例:

对应如下代码

年,制造商,型号,说明,价值
1997,Ford,E350,"ac, abs, moon",3000.00
1999,Chevy,"Venture ""Extended Edition""","",4900.00
1999,Chevy,"Venture ""Extended Edition, Very Large""","",5000.00
1996,Jeep,Grand Cherokee,"MUST SELL!
air, moon roof, loaded",4799.00

以上示例说明:
包含逗号, 双引号, 或是换行符的字段必须放在引号内(只有三个特殊值时需要特别处理)。
字段内部的引号必须在其前面增加一个引号来实现引号的转码。
分隔符逗号前后的空格 可能不会被修剪掉。
元素中的换行符将被保留下来。

PHP中CSV文件的处理:

有两个函数可以读入CSV内容
// str_getcsv 是从字符串读入,返回行数组,属于字符串处理类函数
array str_getcsv ( string $input [, string $delimiter = ',' [, string $enclosure = '"' [, string $escape = '\\' ]]] )
// fgetcsv从文件指针中读入一行并解析 CSV 字段,属于文件系统类函数
array fgetcsv ( int handle [, int length [, string delimiter [, string enclosure]]] )

str_getcsv()函数是PHP 5.3.0中新增的。对于使用不同编码的CSV文件,可以先转码到合适编码,然后再使用str_getcsv()函数把值组织成数组。否则的话可以fgetcsv()函数。

比如:

//读入的文件的编码是GB2312
$fp = fopen("text.csv","r");
$data = array();
while(!feof($fp)){
	$ln = fgets($fp);
	//原来编码是gb2312,这里把它转换为utf-8
	$ln = iconv("gb2312","utf-8",$ln);
	$data[] = str_getcsv($ln);
}

$data = array();
while($line = fgetcsv($fcsv,1000)){
	$data[] = $line;	
}

//写入到CSV文件,它的优点是可以处理值有特殊值的情况
$row = array("abc,\"bcd\n","abc","efg");
$fp = fopen("file.csv","w");
fputcsv($fp,$row);

PHP文件的编码格式只是在说明代码本身使用了这种编码,如果此时赋值了一个变量,那么变量的内容也是这种编码,而当从外部读入内容时,这些内容的编码跟它读入之前一致,这个问题在操作数据库时特别要注意。比如在一个GB2312编码的文件的源代码中产生一个变量(编码是GB2312),这个变量提交到一个UTF-8的文件,那么它到达UTF-8文件后,它内容的编码还是GB2312,如果要比较内容,理论应该统一编码,但是如果内容是纯ASCII码字符,则可以不用(因为不同编码都兼容ASCII码字符)。

制表符分隔文件
基本和逗号分隔文件(CSV文件)一样,只是分隔符换成了制表符,其它规则和CSV一样的。
str_getcsv() fgetcsv() fputcsv()都有一个参数指定分隔符,默认是逗号,如果要处理制表符分隔的文件,只要指定\t即可。

原创文章,转载务必保留出处。
永久链接:http://blog.ifeeline.com/217.html

PHP数据采集程序 – 利用phpQuery

此段程序的产生是由于某朋友想把aliexpress上的某个店的某个目录的全部产品图片采集下来。

思路是:把目录页面抓取过来,分析有多少分页,然后循环获取产品的链接,获得链接的同时进入链接对应的产品页面抓取符合规则的图片地址,然后把这些图片下载分别存放,最后生成一个CSV文件,它记录了所有链接。

遇到的问题:
1 在使用PHP抓取网页的时候,被服务器拒绝了,返回403,浏览器访问正常。
第一反应肯定是User Agent的问题,在PHP配置文件中,user_agent配置指令可以修改PHP本身的UA标识,改成user_agent=”Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0) Gecko/20100101 Firefox/16.0″,那么就伪装为firefox浏览器了。

在大部分的服务器设置中,为了防止一些恶意爬虫、采集器等,通常禁止它们访问,比如在Nginx中:

        if ($http_accept_language ~* ^zh) { return 404;}
        if ($http_user_agent ~* (^$|LWP::Simple|BBBike|wget|scrapbot)) { return 403;}

这个配置可以让中文浏览器得到404(浏览器的语言标识可修改),对用户代理是空的或是|LWP::Simple|BBBike|wget|scrapbot之一,全部返回403,当然了,用户代理是可以修改的(伪装)。

2 在抓取到网页后,如何快速提取想要的内容?
刚开始是用正则来提取内容的,实现起来比较费力,后来想用PHP的DOM扩展来提取内容,不过还觉得不方便,最终选择了phpQuery库(jQuery的服务器实现,实际也是用PHP DOM来实现)来提取内容,由于对jQuery比较熟悉,故而对phpQuery的学习成本了零。

3 如何下载图片?
最终使用如下代码实现图片下载:

function GrabImage($url, $filename=""){
	if($url == ""){return false;}
	
	$ext = strrchr($url, ".");//得到图片的扩展名
	if($ext != ".gif" && $ext != ".jpg" && $ext != ".bmp"){echo "格式不支持!";return false;}
	
	if($filename == ""){ $filename = time()."$ext"; }//以时间戳另起名
	
	//开始捕捉
	ob_start();
	readfile($url);
	$img = ob_get_contents();
	ob_end_clean();
	$size = strlen($img);
	$fp2 = fopen($filename , "a");
	fwrite($fp2, $img);
	fclose($fp2);
	return $filename;
}

分析aliexpress的目录链接形式都是如此http://www.aliexpress.com/store/group/目录名/店ID_目录ID.html, 如果第二页就是http://www.aliexpress.com/store/group/目录名/店ID_目录ID/2.html, 所以为了方便,我让“目录名/店ID_目录ID.html”这段直接作为请求的URI,然后分析这个URI取出相关参数,开始抓取,这样只要拷贝粘贴就可以开始了。比如PHP脚本文件名为get.php,要采集的目录链接为http://www.aliexpress.com/store/group/xxxx/666_888.html,那么只要访问http://localhost/aliexpress/get.php/xxxx/666_888.html就可以开始了。

另外,如果目录图片比较多,程序运行时间可能会超过限制值,所以要修改一些PHP的相关指令,开足马力:

max_execution_time=3600
max_input_time = 3600
memory_limit=1024M

这里运行最大执行时间为1小时,最大等待数据返回浏览器时间为1小时,最大可用内存为1G。max_input_time也叫超时时间,当PHP脚本运行了很久还有没有访问,在浏览器端就会弹出一个窗口,关闭后浏览器不再等待服务器的数据返回,所以如果是让浏览器下载文件的,这个值务必要加大才行。

代码参考:

function GrabImage($url, $filename=""){
	if($url == ""){return false;}
	
	$ext = strrchr($url, ".");//得到图片的扩展名
	if($ext != ".gif" && $ext != ".jpg" && $ext != ".bmp"){echo "格式不支持!";return false;}
	
	if($filename == ""){ $filename = time()."$ext"; }//以时间戳另起名
	
	//开始捕捉
	ob_start();
	readfile($url);
	$img = ob_get_contents();
	ob_end_clean();
	$size = strlen($img);
	$fp2 = fopen($filename , "a");
	fwrite($fp2, $img);
	fclose($fp2);
	return $filename;
}

//加载phpQuery库
require_once('phpQuery/phpQuery.php');

ini_set("display_errors","0");
ini_set("max_execution_time","7200");
ini_set("memory_limit","1024M");

//解析地址
$request_arr = explode('/',ltrim($_SERVER['REQUEST_URI'],$_SERVER['SCRIPT_NAME']));
$temp_var = explode('_',$request_arr[1]);
$shop_id = $temp_var[0];
$cat = $request_arr[0];
$cat_id = rtrim($temp_var[1],'.html');

$shop_all_url = "http://www.aliexpress.com/store/group/".$cat."/".$shop_id."_".$cat_id;

//存放目录
$base = dirname(__FILE__)."/";
$new_base = $base.$cat."_".$shop_id."_".$cat_id."/";
if(!is_dir($new_base)){
	mkdir($new_base);
}

$html = file_get_contents($shop_all_url."/1.html");
phpQuery::newDocument($html);
$page_text = pq("span.pg-info")->text();

// $page_total 截获所有页
$page_total = 1;
preg_match('/.*\/([0-9]{1,12})/',$page_text,$page_total);
if(isset($page_total[1])){
	$page_total = (int)$page_total[1];
}
//$page_total = 1; 多页时,限制其为1页,测试用

/**/
$excude = array(); //记录那些重复的,不需要的图片地址

if($page_total > 0){
        $fp = fopen($base.$cat."_".$shop_id."_".$cat_id.".csv","w");
	$csv_str = "ID,Links\n";
	for($i = 1; $i <= $page_total; $i++){
		$html = file_get_contents($shop_all_url."/".$i.".html");
		
		phpQuery::newDocument($html); 
		$links = pq("li.list-item .img .pic a");
		
		$j = 0;
		foreach($links as $ls){
			//if($j > 0){ break; } 多个产品时,限制其只处理一个,测试用
			$phtml_link = pq($ls)->attr('href');
			preg_match('/http:\/\/.*_([0-9]{1,16})\.html/',$phtml_link,$pid);
			$pid = $pid[1];
			
                        fputcsv(array($pid,$phtml_link));
			$img_path = $new_base.$pid.'/';
			//创建目录,如果脚本执行以为终止,可以再次运行,跳过之前下载的数据
			if(!is_dir($img_path)){ mkdir($img_path); }else{ continue; }

			phpQuery::newDocument(file_get_contents($phtml_link));
			$img_links = pq("#custom-description img");
			
			$ii = 0;
			foreach($img_links as $lks){
				$isrc = trim(pq($lks)->attr("src"));
				if((strstr($isrc,"http://style.alibaba.com") !== FALSE) || in_array($isrc,$excude)){
					continue;
				}else{
					GrabImage($isrc,$img_path."/".$ii.'.jpg');
					$ii++;
				}
				
			}
			$j++;
		}
	}
        fclose($fp);
}
//根据请求相关参数,生产文件
//file_put_contents($base.$cat."_".$shop_id."_".$cat_id.".csv", $csv_str);

永久链接:http://blog.ifeeline.com/215.html

服务器端jQuery – phpQuery简要

项目地址:http://code.google.com/p/phpquery/

依赖
PHP5以上、PHP5 DOM扩展、Zend Framework
PHP 5 DOM扩展是默认编译到内核的,这个不会有问题,phpQuery实际是它的二次封装。phpQuery之所以用到ZF是因为它使用了ZF的类库去模拟一个浏览器,它可以用来自动提交表单等交互操作。

Basics
1装载文档

phpQuery::newDocument($html, $contentType = null) Creates new document from markup. If no $contentType, autodetection is made (based on markup). If it fails, text/html in utf-8 is used.
phpQuery::newDocumentFile($file, $contentType = null) Creates new document from file. Works like newDocument()
phpQuery::newDocumentHTML($html, $charset = 'utf-8')
phpQuery::newDocumentXHTML($html, $charset = 'utf-8')
phpQuery::newDocumentXML($html, $charset = 'utf-8')
phpQuery::newDocumentPHP($html, $contentType = null)
phpQuery::newDocumentFileHTML($file, $charset = 'utf-8')
phpQuery::newDocumentFileXHTML($file, $charset = 'utf-8')
phpQuery::newDocumentFileXML($file, $charset = 'utf-8')
phpQuery::newDocumentFilePHP($file, $contentType)

事实上这里只有两类,一个是从标签(字符串)装载文档,另一个是从文件(URL)转载。文档的装载有两个参数需要确定,一个是字符编码,一个是内容类型,所谓内容类型就是指文档是HTML、XHTML、XML或是PHP。在newDocument 和 newDocumentFile函数中可以指定装载内容的类型,字符编码自动检测确定了。它们分别有对应的自定类型的版本,比如newDocumentHTML 和 newDocumentFileHTML。(内部都间接调用newDocument方法)。
注意:如果把一个HTML字符串(或文件)用XHTML或XML的方式装入,那么就会按照XML严格的方式(标签对应)进行装入,如果遇到未结束的标签就悲剧,所以除非确实是要解析一个XML,否则都应该使用HTML方式装入。

$html = <<<'EOT'
<body>
<div class="page">
    <ul>
        <li>1</li>
        <li>2</li>
    </ul>

    <div id="test">hello world</div>
    <br>
    <div>
        <b>bbbbbbbbbbbbbbbb</b>
</body>
EOT;
$dom = phpQuery::newDocumentHTML($html);
echo $dom->html();

///////////////////////
//输出
<div class="page">
    <ul>
<li>1</li>
        <li>2</li>
    </ul>
<div id="test">hello world</div>
    <br><div>
    <b>bbbbbbbbbbbbbbbb</b>
</div>
</div>

可见,它把DIV补齐了。这也是我们需要的。特别是需要解析那些非标准的HTML时。

2 pq函数
模拟jQuery中的$()函数

//插入标签
pq('&lt;div/&gt;')
pq('&lt;div/&gt;', $pq->getDocumentID())
pq('&lt;div/&gt;', DOMNode)
pq('&lt;div/&gt;', $pq)

//查询
pq('div.myClass')
pq('div.myClass', $pq->getDocumentID())
pq('div.myClass', DOMNode)
pq('div.myClass', $pq)

//用phpQuery对象包装DOMNodes
foreach(pq('li') as $li)
  // $li is pure DOMNode, change it to phpQuery object
  pq($li);

一般查询 和 包装是常用操作,插入一般使用更加直观的操作方法。

Ported jQuery section
这部分一共有8个主题,选择器 / 属性 / 过滤 / 文档处理 / Ajax / 事件 / 工具(一些常用函数) 插件接口,对应到jQuery,phpQuery缺少CSS 和 效果(也没有存在必要),用法上,高度类似。不过服务器端的Ajax 和 事件有点费解。

服务器端的Ajax实现 事实上是在服务器上模拟一个浏览器,它的实现是依赖Zend_Http_Client。

这部分具体的内容需要参考详细文档。

PHP Support
Command Line Interface

Multi document support 多文档支持
可以同时操作多个文档,最后创建或选择的文档在pq()函数中默认使用。

// first three documents are wrapped inside phpQuery
$doc1 = phpQuery::newDocumentFile('my-file.html');
$doc2 = phpQuery::newDocumentFile('my-file.html');
$doc3 = phpQuery::newDocumentFile('my-other-file.html');
// $doc4 is plain DOMDocument
$doc4 = new DOMDocument;
$doc4->loadHTMLFile('my-file.html');
// find first UL list in $doc1
$doc1->find('ul:first')
  // append all LIs from $doc2 (node import)
  ->append( $doc2->find('li') )
  // append UL (with new LIs) into $doc3 BODY (node import)
  ->appendTo( $doc3->find('body') );
// this will find all LIs from $doc3
// thats because it was created as last one
pq('li');
// this will find all LIs inside first UL in $doc2 (context query)
pq('li', $doc2->find('ul:first')->get());
// this will find all LIs in whole $doc2 (not a context query)
pq('li', $doc2->find('ul:first')->getDocumentID());
// this will transparently load $doc4 into phpQuery::$documents
// and then all LIs will be found
// TODO this example must be verified
pq('li', $doc4);

基本上,phpQuery还是DOM操作部分比较常用,其它的比如Ajax等部分使用起来还是多有别扭。

永久链接:http://blog.ifeeline.com/211.html