在 Mma 中的函数 URLFetch 看来,爬取数据,如探囊取物般容易,不需要技术!

背景

  1. 最近打算用 Mma 做个英文助手(背单词用),爬下了 Google 翻译的很多数据
  2. 通过身份证查询出生地、手机归属地查询,IP所在地查询,GIS信息查询,查询书籍信息……
  3. 闲时,会逛逛各种论坛(数学、软件、音乐、读书、问答、摄影、成人……),特别是在摄影和成人论坛中往往会有大量漂亮的套图,于是就想把它们收到硬盘里。
  4. ……(应该还有吧,以后想到一个补一个)

1、Google 翻译

在众多在线翻译工具中,我最喜欢 Google 翻译。
原因一:其准确性和权威性,在地球村貌似无人可望其项背;
原因二,是主观癖好,我非常反感广告。

从 Google 翻译中爬下的数据:几乎 DictionaryLookup[] 中所有单词的基本信息(主要有:译文,定义,译文频率,同义词,常见词组,例句)。

再结合 WordData 中的单词信息,基本上就够用了。也许还会包含 MDict 论坛中提供的丰富的词典数据。

方法

首先,通过浏览器的“查看元素”(也有叫“审查元素”)功能,找到获取数据的API。

  1. 打开 Google 翻译,在网页中任意位置右键>查看元素,然后切换到“网络”选项卡;
  2. 如果有请求记录,可以点一下“清除”,不点也无所谓;
  3. 然后查一个单词(比如 about),注意这时请求记录的变化,为了方便查看,点下面的“XHR”过滤一下;
  4. 如果你刚刚只查了一个单词,这时应该只有两条记录,一个是 GET 方法的请求,一个是 POST 方法的请求。
    ● 点选 GET 方法的请求,在右边的消息头中可以看到请求的地址(http://translate.google.cn/translate_a/single?client=t&sl=en&tl=zh-CN&hl=zh-CN&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&dt=at&ie=UTF-8&oe=UTF-8&otf=1&ssel=0&tsel=0&kc=6&tk=521885|232363&q=about),这就是我们要想找的 API 之一了,只要把其中的”about“换成你想查到的其它单词就OK了。在右边的响应中可以看到服务器返回的数据,可见返回的是单词的基本信息,还有对应的英文例句。
    ● 点选 POST 方法的请求,在右边的消息头中同样得到请求地址(http://translate.google.cn/translate_a/t?client=mt&sl=en&tl=zh-CN&hl=zh-CN&v=1.0&format=html&tk=521885|232363),但 POST 的方法的请求与 GET 方法有所不同,POST 方法可以发送数据不是通过 URL 地传递,而是另附一个包。为了看清其形式,可以点击右边的”编辑和重发“,哈哈,一目了然,在最下面的”请求主体“中发现,例句中的空格都被替换成了”%20“,我们也照做就OK了。
 定义查询单词基本信息的函数
googleWord[word_] := 
 URLFetch["http://translate.google.cn/translate_a/single?client=t&sl=\
en&tl=zh-CN&hl=zh-CN&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&\
dt=t&dt=at&ie=UTF-8&oe=UTF-8&otf=1&srcrom=1&ssel=0&tsel=0&kc=1&tk=\
520310|498193&q=" <> word]

googleWord["about"]

这个太简单了,以至于都没有必要使用 URLFetch 函数,用 Import 都可以搞定。

不要高兴的太早,此 API 返回的数据格式真令人头疼。很多 API 都支持返回 XML 格式的数据,Google 翻译提供的 API 貌似也是支持的,而且 Google 也提供了查询的 API ,只是它的说明在墙那头,下周回公司了查一查再补吧(也许我会忘记)。还是回来,顺着原思路一头走下去吧,Mma 处理字符串的功能非常强大,只要数据有规律就好。

其实刚开始的时候,处理里它还挺愁的,以为只要把中括号换成花括号就好了。在新科学论坛上问了此问题,在“苹果”的帮助下,才注意到返回的数据中有时候会包含转义字符,这会大大降低 ToExpression 函数的效率。

还是直接说结果,在返回的字符串中,转义字符有两类,一类是 HTML 语言的转义字符,比如 “<”,“>”等;另一类是汉字或特殊字符的转义,统一使用 UTF8 编码,比如用”\u95f2\u4e91\u8c37“表是汉字”闲云谷“。下面给出重新定义的函数:

googleWord[word_] := 
 Module[{rs = 
    URLFetch[
     "http://translate.google.cn/translate_a/single?client=t&sl=en&tl=\
zh-CN&hl=zh-CN&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&\
dt=at&ie=UTF-8&oe=UTF-8&otf=1&srcrom=1&ssel=0&tsel=0&kc=1&tk=520310|\
498193&q=" <> StringReplace[word, " " -> "%20"], {"Content", 
      "StatusCode"}]}, 
  If[rs[[2]] == 200, 
   Return[ToExpression[
     StringReplace[
      StringReplace[
       rs[[1]], {"[" -> "{", "]" -> "}", ",," -> ",0,", 
        "\\u" ~~ x : RegularExpression["\\s*\\w{4,4}"] :> 
         FromCharacterCode[
          FromDigits[StringReplace[x, RegularExpression["\\s"] -> ""],
            16], "UTF8"]}], {",," -> ",0,", "{{," -> "{{0,", 
       ",{," -> ",{0,", WordBoundary ~~ "{," -> "{0,", 
       ",}," -> ",0},", ",}}" -> ",0}}", 
       ",}" ~~ WordBoundary -> ",0}", 
       "&" ~~ xs : Shortest[__] ~~ ";" /; StringLength[xs] < 10 :> 
        ImportString["&" ~~ xs ~~ ";", "HTML"]}]]], googleWord[word]]]

googleWord["about"]

除了上面提到的原因,这里定义的函数 googleWord 还多了一个功能:当频繁查询时,由于网络原因难免会查询失败,当遇到此情况时,函数 googleWord 会重新发送请求,直到查询成功为止。

 定义查询例句翻译的函数

还是先给一个单简的例子,方便阅读主体内容:

googleSentence[Sentence_] := 
 URLFetch["http://translate.google.cn/translate_a/t?client=mt&sl=e=zh-\
CN&hl=zh-CN&ie=UTF-8&oe=UTF-8&v=1.0&format=html&tk=520310|498193", 
  "Method" -> "POST", 
  "BodyData" -> 
   "&q=" <> 
    StringRiffle[StringReplace[Sentence, " " -> "%20"], "&q="]]

googleSentence["Good health is above wealth."]

完整的函数定义:

googleSentence[strs_] := 
 Module[{rs = 
    URLFetch[
     "http://translate.google.cn/translate_a/t?client=mt&sl=e=zh-CN&\
hl=zh-CN&ie=UTF-8&oe=UTF-8&v=1.0&format=html&tk=520310|498193", \
{"Content", "StatusCode"}, "Method" -> "POST", 
     "BodyData" -> 
      "&q=" <> 
       StringRiffle[StringReplace[#, " " -> "%20"] & /@ (strs), 
        "&q="]]},
  If[rs[[2]] == 200, 
   Return[Transpose[{strs, 
      ToExpression[
        StringReplace[
         rs[[1]], {"[" -> "{", "]" -> "}", 
          "\\u" ~~ x : RegularExpression["\\s*\\w{4,4}"] :> 
           FromCharacterCode[
            FromDigits[
             StringReplace[x, RegularExpression["\\s"] -> ""], 16], 
            "UTF8"], 
          "&" ~~ xs : Shortest[__] ~~ ";" /; StringLength[xs] < 10 :> 
           ImportString["&" ~~ xs ~~ ";", "HTML"]}]][[;; 
         Length[strs]]]}]], googleSentence[strs]]]

googleSentence[{"Good health is above wealth.", 
  "Wasting time is robbing oneself."}]

由于单词的查询是互不相干的,运行这个函数时,Mma 绝大多数时间都是在等待服务器传回数据,所以使用并行命令可以成倍提高速度。在良好的网络中,用4核电脑,速度大概是原来的3.5倍以上。

推荐函数:ParallelMap

另外,如果在你的系统上运行结果有乱码,可以使用下面的函数转换。

FromCharacterCode[ToCharacterCode["这里是你的乱码"], "UTF-8"]

2、通过身份证查询出生地

百度 API 商店中搜一下”身份证“,就会得到好多查询身份证信息的 API ,选一个免费的 API, 看一下例子,了解一下格式,然后可以用 Mma 进行批量查询了:

find[id_] := 
 URLFetch["http://api.46644.com/idcard?appkey=\
1307ee261de8bbcf83830de89caae73f&idcard=" <> ToString[id]]

find[130322]

这里选用此 API 是由于只输入身份证前6位它就可以给出出生地信息。不过,此 API 不能返回 XML 格式的数据,只支持 JSON 格式,还需要简单处理一下。

关于相应的 API 也可以去其它网站去找,同样是用”查看元素“ 功能即可。其它(手机归属地查询,IP所在地查询,GIS信息查询,查询书籍信息……)API 自己去发现吧,好玩的很!

3、批量下载套图

我主要使用火狐浏览器和 Chrome 浏览器,推荐个火狐浏览器的扩展插件——Image Picker,这个插件可以非常方便下面页面中的图片,可是它并不完美,比如当论坛为图片增加了绽放功能时,Image Picker 无法保存原图(也许是由于我玩的不精)。这时又该 Mma 上场了,怎么那么兴奋呢,原因有三:

  1. URLFetch 函数支持 Cookies,也支持使用账号和密码,这分明就是个浏览器嘛(看看这个用 Mma 作的网站);
  2. 很多浏览器的 Cookies 文件都是 SQLite 数据库文件,而 Mma 还支持 SQLite(前面是官网的链接,教程点这儿)!
  3. 通过此案例,结识了 SQLite 数据库——超轻量级数据库啊!Windows 版只有几百KB,而且不用安装不用配置,只要了解命令行和基本 SQL 语句即可上手。

有前两点,就可以开心快乐地玩耍了

下面以 WIndows10 系统中火狐浏览器的 Cookies 为例。Chrome 浏览器默认参数设置点这里(特别要注意读取时间时需要变换,而且Value字段是加密的)

读取 Cookies 中信息并从网络抓取图片

首先,在火狐浏览器中登录新科学论坛,这样本地就存有此论坛该用户的登录状态的 Cookies 信息了,然后再执行下面的命令:

Module[{},
 Label[bigen];
 outputdir = "D:/";(*输出目录*)
 url = InputString[
 Column[{Style["请选择图片格式(至少选择一种):", Bold, 18], 
 Row[{" ", 
 Grid[{{Checkbox[Dynamic[jpg]], 
 " jpg"}, {Checkbox[Dynamic[gif]], 
 " gif"}, {Checkbox[Dynamic[png]], 
 " png"}, {Checkbox[Dynamic[bmp]], " bmp"}}, 
 Alignment -> Left]}], Style["\n请输入需要抓取图片的网址:", Bold, 18]}], 
 "http://"];
 url = StringReplace[url, " " -> ""];
 imgtype = {".jpg", ".gif", ".png", 
 ".bmp"}[[Flatten[Position[{jpg, gif, png, bmp}, True]]]];
 If[url =!= $Canceled && 
 Length[imgtype] == 0 && ! StringContainsQ[url, "."] && ! 
 URLExistsQ[url], Goto[bigen];, If[url === $Canceled, Goto[end]];];
 domain[url_] := 
 StringReplace[URLParse[url]["Domain"], 
 RegularExpression["^.*\\."] ~~ 
 x : RegularExpression["[^\\.]+\\.[^\\./$]+"] :> x];(*二级域名*)
 cookiesName = 
 "cookies.sqlite";(*火狐浏览器的Cookies文件名,Google浏览器是Cookies*)
 (*==================对于同一网站这堆等号之间的部分不需要每次都执行==================*)
 cookiesDirectory = 
 DirectoryName[
 First[FileNames[
 cookiesName, {"C:/Users/" <> $UserName <> 
 "/AppData/*/Mozilla/*"}, Infinity]]];(*Cookies所在目录*)
 Needs["DatabaseLink`"];
 conn = OpenSQLConnection[JDBC["SQLite", cookiesName], 
 "Location" -> cookiesDirectory, 
 "RelativePath" -> True];(*以SQLite数据库方式连接Cookies文件*)
 sql = "Select 
 host
 ,path
 ,case isSecure when 0 then 'False' else 'True' end secure
 ,datetime(expiry,'unixepoch','localtime') expiry
 ,name
 ,value
 From moz_cookies
 Where host like '%" <> domain[url] <> 
 "';";(*针对火狐的SQL,Google浏览器的SQL有所不同,请参见:http://forensicswiki.org/\
wiki/Google_Chrome#Cookies*)
 rs = SQLExecute[conn, sql];(*结果集*)
 CloseSQLConnection[conn];(*关闭数据库*)
 myCookies = {"Domain" -> #[[1]], "Path" -> #[[2]], 
 "Secure" -> #[[3]], "Expires" -> #[[4]], "Name" -> #[[5]], 
 "Value" -> #[[6]]} & /@ rs;(*转换成URLFetch需要的Cookes格式*)
 (*==================对于同一网站这堆等号之间的部分不需要每次都执行==================*)
 html = URLFetch[url, 
 "Cookies" -> myCookies];(*核心命令:返回HTML文本,图片地址就在这里了*)
 charset = 
 If[MemberQ[{"gbk", "GBK"}, 
 First[Flatten[
 StringCases[html, 
 "<meta" ~~ x : Shortest[__] ~~ "/>" :> 
 StringCases[x, 
 "charset=" ~~ y : RegularExpression["\\w+"] :> y]]]]], 
 "CP936", "UTF8"];(*确定论坛所用字符集,国内一般就两种:GBK和UTF8*)
 imgs = If[StringTake[#, 7] == "http://", #, 
 "http://" <> URLParse[url]["Domain"] <> "/forum/" <> #] & /@ 
 Flatten[StringCases[#, {"src=\"" ~~ x : Shortest[__] ~~ "\"" :> x, 
 "zoomfile=\"" ~~ x : Shortest[__] ~~ "\"" :> x}] & /@ 
 Flatten[StringCases[html, 
 "<img" ~~ Shortest[__] ~~ 
 "/>"]]];(*对于不同的论坛,其中的"/forum/"有时是没有的,但要保留一个斜杠*)
 imgs = Select[
 imgs, ! StringContainsQ[#, "size=small"] && ! 
 StringContainsQ[#, "size=middle"] && ! 
 StringContainsQ[#, "/common/"] && ! 
 StringContainsQ[#, "/smiley/"] && 
 MemberQ[imgtype, StringTake[#, -4]] &];(*直接的图片地址*)
 title = FromCharacterCode[
 ToCharacterCode[
 StringRiffle[
 StringReplace[
 Reverse[Flatten[
 StringSplit[
 StringCases[html, 
 "<title>" ~~ x : Shortest[__] ~~ "<" :> x], 
 " - "]]][[2 ;;]], " " -> ""], "_"]], 
 charset];(*贴子标题,用以保存图片时创建目录用*)
 dir = outputdir <> title;
 If[! DirectoryQ[dir], CreateDirectory[dir]];
 ParallelMap[
 Export[dir <> "/" <> 
 First[StringCases[#, 
 x : Except["/"] .. ~~ RegularExpression["$"] :> x]], 
 Import[#]] &, imgs];(*下载图片*)
 Label[end];]

由于很多论坛需要登录之后才可以查看图片和附件,所以在 Mma 中也需要登录。而论坛程序一般都是通过本地的 Cookies 信息来验证用户是否登录,如果 Cookies 中已经存有登录信息而且还没有过期,那么就可以省掉通过用户名+密码的方式验证了,这就是此法可行的原因。

另外,程序中已经过滤掉了表情、头像和每页都有的图片。针对不同的网站,还要小小地修改一下。

最后,还可以从缓存中直接取图,那样的速度比此法快太多了,在火狐浏览器中输入“about:cache”可以查看缓存信息。火狐浏览器的缓存文件都存在 “C:\Users\%USERNAME%\AppData\Local\Mozilla\Firefox\Profiles\****.default\cache2\entries”里面了。想想用不上 URLFetch 还是不写在这儿了。

其中 %USERNAME% 是此时你在电脑上登录的用户名,**** 没仔细查过,貌似不同的电脑不一样。在 Mma 可以这样写,以快速打开该文件夹(还是在 Windows 10 中):

SystemOpen[
 First[FileNames[
 "C:\\Users\\" <> $UserName <> 
 "\\AppData\\Local\\Mozilla\\Firefox\\Profiles\\*.default\\cache2\\\
entries"]]]