小偷实例

这时间里相关部门就开始研究其他的购物网,比如当当,卓越。

他们想要一份以excel形式存储的包含上述网站的商品信息以便统计、分析。当然卓越、当当没有什么理由把数据都给我们,于是我们想到了“偷”。于是我把周杰伦的七里香放进cd-rom驱动器开始了“盗窃”。

一切从www.baidu.com开始,在搜索的文本框里输入“asp 小偷”很多的信息就被检索出来了。我进入了一个叫“ASP小偷(远程数据获取)程序入门教程”的标题:
http://www.pconline.com.cn/pcedu/empolder/wz/asp/0505/628553.html

总结了文章的内容得出结论,“偷”其实就是通过微软的XMLHTTP这个COM组件进行的。还好XMLHTTP这个词并不陌生。

我又在baidu上检索到“xmlHTTP技术资料"这条信息。
http://www.youren.com/Article/programme/xml/200505/4768.html

XMLHTTP能干很多事情,但是在这里我们要它干的只是:将指定的网页的HTML代码读出。
如果通过程序读出对方(当当、卓越)的相应HTML代码就好办了,因为商品数据就存储在那庞杂的代码中。

这很简单,我接着写了如下函数:

function getBody(infopageurl)
'功能:取得指定网址的html代码
'参数:infopageurl 网页地址
if infopageurl<>"" then
dim xmlHttp
set xmlHttp=server.createobject("MSXML2.XMLHTTP")‘声明XMLHTTP对象
xmlHttp.open "GET",infopageurl,false
xmlHttp.send’上面这两就句就是调用的形式,调用后程序会堵塞在send这句,直到内容被返回。
getBody=BytesToBstr(xmlhttp.responsebody,"GB2312")‘然后通过xmlhttp.responsebody属性将返回内容读出,这里用到一个BytesToBstr函数将在后面说明。
set xmlHttp=nothing
end if
end function

关于BytesToBstr,开始返回的结果没有经过这个函数处理,结果返回的汉字是一堆乱码。什么原因就不用深究了。我到www.chinaz.com下载了几个”小偷’的程序,发现里面不约而同的都含有这个函数。大概就是用adodb.stream对结果字符进行了转换。我把它加到我的函数里结果就正常了。

Function BytesToBstr(body,Cset)
dim objstream

set objstream = Server.CreateObject("adodb.stream")

objstream.Type = 1

objstream.Mode = 3

objstream.Open

objstream.Write body

objstream.Position = 0

objstream.Type = 2

objstream.Charset = Cset

BytesToBstr = objstream.ReadText

objstream.Close

set objstream = nothing
End Function

html代码取回来了,我们怎么从中取得需要的数据?答案是正则表达式。
这里是关于正则表达式的参考
http://www.51windows.net/pages/vbscript/html/reconIntroductionToRegularExpressions.htm

正则表达达式就象是一种简单的语言。它用一些特殊的字符模糊地描述一个字符串。然后可以通过程序去检查一个字符串中是否包含你描述的那种字符串,还可以将你描述的字符从一个字符串中取出,这里我们就是利用这个功能来获取需要的数据。

举些例子比较容易理解:
* 匹配前面的子表达式零次或多次。例如,zo*(正则表达式) 能匹配 z 、 zoo、zooooo、zoooooooooo
+ 匹配前面的子表达式一次或多次。例如,zo+ 能匹配 zo 以及 zoo,但不能匹配 "z"。
d 匹配一个数字字符。等价于 [0-9]。 例如, d+ 可以匹配 1,123,9876
. 匹配除 "n" 之外的任何单个字符。

? 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 "oooo",'o+?' 将匹配单个 "o",而 'o+' 将匹配所有 'o'。

下面要做的就是分析商品数据在html代码中存放的特征,然后写出正则表达式将其取出。
例如,joyo商品的市场价格数据在html代码中是这样存储的“市场价:15.00元”,要做的是把以“市场价:”开头,“元”结尾之间的数值取出。下面函数就实现了这个功能:

function Topic(sHtmlcode)
'功能:返回joyo.com商品页面的市场价格数值
'参数:shtmlcode 商品页的html代码
Set regEx = New RegExp'建立使用正则表达式的对象
regEx.IgnoreCase =true
regEx.Global = True
regEx.Pattern ="市场价:(.+?)元"设置表达式
Set Matches = regEx.Execute(shtmlcode)'执行表达式将所有符合描述的字符传存放到一个集合里。
for each sMatch in Matches
Topic=sMatch.SubMatches(0)'submatches(0)带表第一个子匹配,既小括号中的数值
exit for
next
set regEx=nothing
end function

其他的数据也用同样的方法取得。写正则表达式是一个很有趣而且很复杂的工作。期间我遇到写不出来的,就去CSDN.net的论坛去提问,总是可以得到帮助。

还有一点要思考的是:是不是这个表达式对所有的商品页面都适用。如果所有的商品都是用一个asp程序显示那就可以,但是joyo.com不是,它的不同分类中html代码的格式是不同的,所以我们要写几个正则表达式来分别匹配不同形式的数据(比如有些页面中市场价格是这样存储的“市场价:15.00元")。

有了这些函数我们就可以用给定的商品网页地址取得相应的数据了。但是这些网站的商品一般都有万余种,如何获取这些地址就不是那么容易了。这个过程因站而异,这也是真正需要“小偷”智慧的工作之一。

拿joyo.com进行分析。进入首页之后我发现里面有个”产品搜索“,选择“所有类别”,关键字置空,点击那个”GO!"检索出了21684条记录。这应该就是joyo.com的所有商品信息了,来的太容易我简直不敢相信。

检索页上包含有10条商品的连接,用正则取得后,再取得转向后10条记录(即下一页)的连接。如此循环就会得出所有商品信息。我用access建了一个表来存放它们。编好了程序挂到机器上就睡觉去了。

这是一个检索结果页的连接:
http://www.joyo.com/ProdSearch/prodsearch.asp?kind=&limitBefore=10&vname=&sorttype=undefined&uid=u5swcqykpzsis0qaiwg79sg5u
其中limitBefore=10 当前页第一条记录前有10条记录,即这一页的第一条是总的第11条记录。

第二天兴奋地来到机器上一看,发现记录只有700多条。很郁闷,程序我没加容错处理因为我发现vbscript的错误处理比vb中的还要简略。似乎连on error goto LABEL都不可以使用。我在检索页点来点去希望找出错误的根源,结果发现当其中limitBefore大于750的时候后面就没有记录了。原来是这样。joyo.com的程序员没有让我们这么容易的得到所有数据。这条路是不能走了。

下面我开始分析检索结果中商品信息页面的连接:
http://www.joyo.com/shop/shop_product.asp?uid=u5swcqykpzsis0qaiwg79sg5u&prodid=bkbk507440
经分析 prodid=bkbk507440 是这个产品的唯一标识,bkbk表示该产品的所属分类“图书”

这是一些其他的主要分类标识:
bkbk 图书
bkmu 音乐
itit 数码
bkbh 日用
itrj 数码产品
bkgm 游戏
bkys 影视

只要我遍历所有的prodid然后找出存在的商品页面就可以了。然而怎么判断该prodid是否存在呢,我在地址栏上胡乱打了一个prodid返回了出错结果页面。

http://www.joyo.com/errormsg.asp?uid=u5swcqykpzsis0qaiwg79smuw&err=GET%C7%EB%C7%F3%B5%C4%C9%CC%C6%B7ID%B2%BB%B4%E6%D4%DA%A3%AC%B2%D9%D7%F7%CE%DE%B7%A8%BC%CC%D0%F8%A3%A1

出错页上有一张红色的“警钟”图案,我估计正常的页面上是不可能包含此图片的。于是以此作为判断prodid是否存在的依据。遍历所有prodid,如果在返回结果html中发现“警钟”图片则不做处理,否则就从页面中取出需要的信息。

遍历6位的prodid是个漫长的过程,粗略的估算,如果每个操作用时1秒的话,则最少需要999999次循,环即999999秒(999999/3600=277多小时)。这显然是太长了,于是我又做了一些优化,发现第一位数字只在0-5之间变化,第二为数字只在0-1之间变化。这样大大的缩短了时间(519999/3600=144多小时),实际的时间更乐观。

经过几天的调试终于把图书分类下载完了,接下来我又接着优化程序,做一个更有效率的“小偷”。

//**********************************

附录:

xmlHTTP技术:
----------------------------------------------------------
一、数据库远程管理技术

基于互联网的广域网现代应用中的一个重要环节是数据库远程监控。首先简单回顾一下互联网上的数据库远程管理技术的发展过程和方式:

早期通过编写CGI-BIN程序模块进行数据库远程管理。但CGI-BIN的运行速度慢,维护很不方便,现在已经基本被弃用。

这几年使用组件对象模型(Component Object Model, COM)的应用非常多,效果也很好。但如果使用的是第三方服务器(笔者的网站就是建立在第三方的虚拟主机上),服务器方往往因为保密或其它商业原因不允许用户注册自己的组件。

近年来由微软公司推出的.NET平台和SUN公司的J2EE平台都是非常高档的数据库远程管理与服务平台。都能提供优质的多层(n-Tier)应用服务。
其中,.NET的简单对象访问协议(Simple Object Access Protocol, SOAP)使用超文本传输协议(Hypertext Transfer Protocol, HTTP)和扩展标记语言(Extensible Markup Language, XML)技术实现跨系统(例如Windows - Linux)的通讯服务方式已经广为开发商接受和使用。许多大型应用,例如企业资源计划(Enterprise resource planning, ERP)等都建立在这样的大型平台之上。
但对于中小型应用,比如一个网站的建设和维护,这种大型应用平台就显得有些尾大不掉,开销也过于庞大。

曾经在互联网技术和Java技术方面一度落后的微软公司在XML应用开发则走在了前头。她的XML解析器(MSXML)中的XMLHTTP协议是一个非常方便实用的客户/服务通讯管道。综合运用XMLHTTP以及ActiveX数据对象(ActiveX Data Objects, ADO/ADOX)可以简单方便地实现数据库远程管理。

本文介绍如何综合运用XMLHTTP和ADO/ADOX进行远程数据库管理。

二、数据库远程管理体系

数据库远程管理的任务流程是:
1、客户端向服务端发出数据库结构和数据的查询或修改指令。
2、服务端接受并执行有关指令并向客户端返回结果。
3、客户端接受并显示服务端返回的指令执行结果。

实现数据库远程管理的二个主要关键环节是:
1、客户端与服务端之间的指令上传和结果下传的数据通道,由XMLHTTP协议实现。
2、服务端前沿与数据库之间的指令传送和结果返回,由起着中间层作用的ADO/ADOX接口完成。
三、XMLHTTP的使用

顾名思义,XMLHTTP是个传送XML格式数据的超文本传输协议。

实际上,XMLHTTP的数据传输过程更为灵活一些:
它上传的指令可以是XML格式数据,也可以是字符串,流,或者一个无符号整数数组。还可以是URL的参数。
它下达的结果可以是XML格式数据,也可以是字符串,流,或者一个无符号整数数组。
详情可参阅文末链接。

客户端调用XMLHTTP的过程很简单,只有5个步骤:
1、创建XMLHTTP对象
2、打开与服务端的连接,同时定义指令发送方式,服务网页(URL)和请求权限等。
客户端通过Open命令打开与服务端的服务网页的连接。与普通HTTP指令传送一样,可以用"GET"方法或"POST"方法指向服务端的服务网页。
3、发送指令。
4、等待并接收服务端返回的处理结果。
5、释放XMLHTTP对象

XMLHTTP方法:
Open bstrMethod, bstrUrl, varAsync, bstrUser, bstrPassword
bstrMethod:数据传送方式,即GET或POST。
bstrUrl:服务网页的URL。
varAsync:是否同步执行。缺省为True,即同步执行,但只能在DOM中实施同步执行。
应用中一般将其置为False,即异步执行。
bstrUser:用户名,可省略。
bstrPassword:用户口令,可省略。

Send varBody
varBody:指令集。可以是XML格式数据,也可以是字符串,流,或者一个无符号整数数组。也可以省略,让指令通过Open方法的URL参数代入。

setRequestHeader bstrHeader, bstrValue
bstrHeader:HTTP 头(header)
bstrValue:HTTP 头(header)的值
如果Open方法定义为POST,可以定义表单方式上传:
xmlhttp.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"

XMLHTTP属性:
onreadystatechange:在同步执行方式下获得返回结果的事件句柄。只能在DOM中调用。
responseBody:结果返回为无符号整数数组。
responseStream:结果返回为IStream流。
responseText :结果返回为字符串。
responseXML:结果返回为XML格式数据。

下面是本文附件源程序中的一个应用示例:
Function GetResult(urlStr)
Dim xmlHttp
Dim retStr

Set xmlHttp = CreateObject("Msxml2.XMLHTTP") '创建对象
On Error Resume Next '出错处理
xmlHttp.Open "POST", urlStr, False '用POST方式打开连接,异步执行。
xmlHttp.setRequestHeader "Content-Type", "application/x-www-form-urlencoded" '上传表单
xmlHttp.Send '发送指令

If Err.Number = 0 Then '如果连接正确
retStr = xmlHttp.responseText '等待并获得服务端返回的结果字符串
Else
retStr = "Url not found" '否则返回出错信息
End If
Set xmlHttp = nothing '释放对象 GetResult = retStr '返回结果
End Function

GetResult()函数带入一个服务网页的URL参数,把上传的指令安放在URL后面的参数上,如:
urlStr = "server.asp?cmd=" & cmd
& "&db=" & db & "table=" & table
cmd:执行方式,例如查询,修改,删除等等。
db:服务端数据库名
table:服务端表名

然后提交指令,等待并接收返回的处理结果。结果以字符串方式返回。

最后由函数调用者处理并显示结果。