我发现现在不用标题党的套路还真不好吸引人,最近在做相关的事情,从而稍微总结出了一些文字。我一贯的想法吧,虽然才疏学浅,但是还是希望能帮助需要的人。博客园实在不适合这种章回体的文章。这里,我贴出正文的前两个部分,算个入口吧。
为了防止在看完了之后觉得其实这不是我的兴趣范围,我先说一下这8个commit都涉及啥,粗略的涉及都有,爬取京东图书编程书籍的名称,标题,价格,好评率。然后涉及如何写log以及多进程。
完整的部分可以在https://rogerzhu.gitbooks.io/python-commit-by-commit/content/ 这里看到。代码呢,可以在https://github.com/rogerzhu/relwarcDJ 这里获取。
python 爬虫 commit by commit(一)
"F12才是爬虫开发的最好的朋友" -- by 我自己
既然叫commit by commit,那就要按照自己给自己定下的规矩来写。在把代码clone到本地之后,你可以用git reset --hard 6fda96eae来退回到代码的第一个版本。别担心回不去后面的版本,这commit都在github都能看到,即使你不知道一些奇技淫巧的git命令也没啥,大胆干。
首先,我觉得我应该说这个commit我想干嘛,第一个commit,我是想作为熟悉的门槛,所以这个commit最开始我的本意是想获得京东图书编程语言第一页上面的书名,链接。
对于这个commit,当你输入如下命令开始运行时:
你应该能看到如下的结果:
前面已经扯了两篇了,那么从这篇开始步入技术的正轨了,其实从骨子里我是很讨厌那种教程里敲半个小时代码,最后发现就是一个输出了一个星号组成的图案。我觉得,入门级别的代码得用不超过10分钟的时间干出一点你能看得到,有成就感的正事才能吸引大部分的注意力。可惜啊,C++在这方面确实很难做到,而python在这方面绝对是擅长。所以,第一个commit虽然我的comment是ugly commit,但是绝对能干活。
既然是入门级别的文章,那么就从最基本的部分开始,当你浏览一个网页的时候,实际上,你在浏览什么?实际上你在浏览的是服务器传回来的一系列文件,这一系列文件由浏览器解析,然后呈现给你。比如我想看看京东图书编程语言下面的所有图书,我只要用鼠标一点一点的点到我想要的地方就可以看到我需要的网页。
但是作为一个程序员,GUI并算是一个高效的交互方式,一个简单的例子,对于文本可以一目十行,GUI除非你眼睛传感器异于常人或者大脑CPU比一般人要性能好,不然很难做到。对于爬虫,他不会关心GUI,它的食物只有一种,各类带格式的文件。所以,我们需要看到界面背后的源码。市面上只要你能见到的浏览器,在右键菜单里一定会有让你看到源码的菜单。但是,在现代网页越来越丰富的情况下,一个页面的源码文件实在是太丰富了,按照我最开始的说法,我想找到书名和价格,咋办?不能用ctrl+f吧,低效不是程序员的作为。在这个时候,职业的本能应该驱使你去寻找工具。
开心的是,主流浏览器都带有这种工具,而且获取这一组工具的方法都是只要简单的按下F12就可以了,我敢保证,当你按下这个键的时候,你有一种打开了新世界的感觉。比如我用的火狐,按下F12之后在最左边,你会看到这样一个图标:
点击一下这个图标再移到界面上,你会发现你可以以矩形的方式选择页面上的元素。根据人的本能,点击一下,你会发现图标下面的html会自动定义到选中的元素!这样,拿到什么信息,你只要负责选择就好了,浏览器自带的工具会自动帮你定位。比如,我想要的图书的名字和价格,我选中某一格的图书,就会看到这样的输出:
html是一种格式化并且是带有层级的语言,这样就会自然引申出一个问题,当我选取一个元素时候,到底采用怎样的粒度?比如说,就以这个图书的名字来说,他是在一个列表(li)元素中的一个div中的一个text中的,那么完全可以直接选取这个text,第二个是通过父级别一点一点的选取。这其实就是一个数据结构大小取舍的问题,而写程序,我觉得要考虑到扩展性和人思维的自然认知性,以便于升级和维护。所以,我一般都是从我自己最自然的认知出发,当我的眼睛看到这个网页时,我的呆脑,哦不,是大脑会自然把每个图的一个缩略图,名称和价格组成的这个方块归类为一个小组,于是,我选择的粒度就是遵从我的内心。
那么我就用上面说的小箭头选取到我决定的方块,可以得到标识这每一方块的元素是<li>。而在这个HTML中,有无数的li,我们怎么能定位到我们需要的这个li呢?这里,让我不得不想起一个谚语,叫赠人玫瑰手有余香。在前端程序员在开发他们的网页时,他们需要对元素进行标识,这样他们才能在代码中方便的写出想要的逻辑。而这个行为,给爬虫程序员们提供了便利,你可以用他们归类的标识来定位你需要的元素,当然,我这里说的是在代码里。而beautifulsoup这个包可以非常的方便的让你完成这件事情,你可以选择用id,class等等来找到你需要的元素。而在这里,如果你按照我说的使用箭头工具的话,会很容易的看到在这个网页中gl-item这样的class来标识每一个列表块。那么剩下的就是按照已经发现的,翻译成为程序语言了。
在第一个commit里面,代码一共22行,我都忍不住用截图的方式展示一下以便于说明。
这个代码前7行都是shebang,coding的设置和import包。这里你不知道shebang也一点也不影响你对于这一系列问斩的阅读理解。所以说,正文从第九行看起就行了。
首先python提供了非常方便的方法获取网页的源码,我以前最开始的时候使用C++写过爬虫,怎么形容呢?如果python爬虫的给力程度是他娘的意大利炮,那么c++就是纯物理攻击了。你只需要使用urllib中的request.urlopen就能直接获取到网页源码。而需要的参数仅仅是网页的url。一如第九行所示。
当有了源码之后,按照前面介绍的逻辑,就是寻找对应的元素了,而这个时候就是BeautifulSoup包上场的时候了,把得到的源码字符串作为参数传给BeautifulSoup库,你就会得到一个强大的方便解析的BeautifulSoup对象。而在BeautifulSoup中,使用findAll你就可以找到全部的带有某种标识的某种元素。比如说,在我们要爬取的页面上,有很多的书,而我们又知道每个书所存在的块是以gl-item的class来标识的列表,那么只要对findAll传入元素名称和标识规则就行了。而BeautifulSoup还提供一个find函数,用来找到第一个符合标识的对象。
既然已经得到需要的一大块了,剩下的就是在这一大块中找到自己想要的信息,比如我想要的书名和链接地址。其实这后面的过程就是前面描述的过程的重复。大致就是找到页面->按下F12->使用选择工具->找到对应的元素块。但是程序员嘛,都很懒,能少动几下鼠标是几下,所以,如果一个块中元素规模不大的并且基本都相像的情况下,我会使用这样的一种办法:把一大块的html片段输出到一个文件里。如果你觉得我说的有点绕了,那么其实我想表达的就是第12行语句的意思,虽然我这里用的是print,但是你可以使用重定向的功能将这个输出到一个文件中,也就是"> item.txt"类似的语句。而如果你查看这个commit的目录结构,你就会看到这么一个文件。如果好奇心仍驱使你打开它,那么你就可以看到一个li中的所有内容。这样就省去了前面那四个步骤的烦恼,而且你可以反复查看,而不用反复的打开浏览器。
当然,这是在我下面的循环还没有写出来的时候先输出的。
谈到这个while循环,在这里你可以完全忽略,或者说你可能会揣测这到底有什么深意。其实没啥深意,就是为了后面用的,而且还是比较后面的commit中才会用到。我只是有点懒,懒得删除。实际上,这个程序的第15,16以及22行完全可以删除,对于最后的结果完全没有任何影响。
而这里的for循环是肯定必要的。python的语法,按照其cookbook上说,已经非常接近自然语言了,从有的方面看真的是这样的,比如说第17行,表示是依次取出allItem中的所有元素,对于每一个元素就是一个li块,剩下的只要从这些li块中再继续寻找需要的信息就可以了。比如,书的标题实在class为p-name的div元素之中。而在这个页面上,真正的标题文字是放在强调标签<em>之中。这都不能难住强大的BeautifulSoup库,其对象可以像访问结构中成员一般一层一层的找到需要的元素。如果想要获得某个标签中的文字,只需要使用get_text函数就可以获得。用代码说话的话就是18,19行。
而有的时候我们不是要获取某个标签中的元素,而是要获取某个标签中的属性怎么办?BeautifulSoup用近乎完全符合自然思维的方式实现了这一点。比如超链接,一般都是在<a>标签中href属性之中,那么href就是a这个成员(字典)的一个关键词,通过这个关键词,你就可以取得其中的值,一如你看到的href="xxx"一样,典型的key,value结构。也就是程序的第20行,通过这样的方式,就可以取得每个图书的链接。
剩下来,就是你怎么呈现这个数据的部分了,我这里就简单大方而又明了的输出,keep it simple,stupid。
这里,第一个commit就结束了,去掉不需要while循环,一共就19行代码,在环境配好的情况下,无脑敲完不需要5分钟,运行python myGAND.py,你就可以看到京东图书编程语言第一页的书名和链接打印在控制台或者文件中。说实话,如果是C++,你可能还在写各种字符串解析函数的过程中。
python 爬虫 commit by commit(二)
“程序员是最不会伪装但是又是最会伪装的群体”--by 我自己
在运行了第一个commit的程序我估计也就三分钟之内,你会觉得索然无味。图书的标题和链接到底有什么用呢?当然,我也有同样的疑问,所以,我决定在第二个commit中爬取这个首页我觉得我最关心的信息,那就是钱——图书的价格。
对于这个commit,当你输入如下命令开始运行时:
你应该能看到如下的结果:
有了第一个commit中的三板斧,我感觉我已经信心与感觉并存,动力与技术齐飞了。于是我熟练的使用了选取工具,选到了价格的方框。火狐的工具给我显示了,价格是在class名为p-price的div之内的。照葫芦画瓢一般的,使用BeautifulSoup的find,直接找到每个li中的这个div,熟练的保存好文件,开始用python运行。现实狠狠的给我了一个耳光,无论我怎么输出,这个价格都是拿不到的。
为什么?我对着屏幕思考了3分钟,毕竟如果思考再长的时间的话那只能说明我的拖延症犯了。我重新回到我需要的页面上,刷新了下页面,会看到价格信息会比其他的信息后出来,我又试了几次,这不是偶然,每次都不是同时出来的。这个时候凭借着我对web编程的一点粗浅的了解,我已经知道了,至少价格这个信息不是和html信息一起返回的。用在任何软件语言里都有的概念,这里一点存在有回调——callback。其实我在初学c++的时候对于这个概念不是很理解,但是如果你是第一次听到这个概念,在这里就特别形象,在某一件事情做完之后,又回头调用了一个什么接口或者文件等等来取得结果。
到了这一步,就需要一点大胆猜测小心求证的哲学了,当然,还有得知道一点webapi的基本概念。其实简单的说,就是调用一个url来获得返回的结果,这个url中可以使用&传入参数,而结果是一个文件的方式传到客户端。而继续前面所说的赠人玫瑰,手有余香的逻辑,你要爬取的这个网站的程序员们也要考虑维护问题,加上业界对于某些反复会出现的东西一定会有一套约定俗成的模式。说了这么多,到底想表达什么?既然我说webapi一般都是以文件的方式返回结果,那么怎么看到这些从服务器返回的文件呢?很完美的事,这件事,又可以使用F12来解决。
当你按下F12的时候会有很多tab,其中有一个叫network,这个下面会记录客户端与服务器端交互的所有内容。
既然是所有,那么确实有点多,而且在大多数情况下,他会在不停的滚动,让人很难操作。
这个时候只要你稍微网上看一点,就会发现,这些工具一定都会带有搜索功能的,毕竟,任何没有搜索功能的列表都是耍流氓。那么这个时候就到了大胆发挥猜想的时候了,按照我前面的说的,写程序的人为了维护一定会有某种比较共通的模式,既然价格是靠回调取出来的,那么不妨试试callback作为关键词?或者这个是价格,用price作为关键词?我选择用callback,没啥原因,只是因为我脑海里第一反应是想用这个。于是我得到如下的结果,但是很明显,一个网站上不可能只有一个callback。
但是这已经少多了,最差的结果一个一个暴力寻找,找什么呢?找返回值,也就是右边有的response的tab,找什么返回值?因为每个callback当然都有返回值。当然找价格的数字了,既然你都能看到价格是多少钱,那么response中含有这个价格当然就是你需要的webapi的地址啦。而很明显,所有的callback返回的都是json字符串,如果你实在没有听过json,也没有关系,最简单的你可以把他理解成是一个带有格式的文本,这个文本的格式就是以逗号隔开的key,value字符串。于是我就这样暴力寻找,还真知道一个respons里面带有p:正确价格的字符串。这个时候可以再回头自己验证一下,怎么验证?我的方法就是看看这个请求的url,验证的方法还是本着良心程序员一定会把接口设计的另外一个程序员一看就懂的模式,吹的大一点,代码即文档。如果你看下这个请求的url,很明显,有个关键词告诉你,啊!这就是你要找的,那就是price,你可以在request URL中看到。你还可以再大胆的进一步,在这条记录上右键,所有的F12工具都有copy url的功能,拷贝下这个地址,放在浏览器上,回车,你会发现,你可以看到一条返回的json字符串。
仔
细看看这些分会的字符串,虽然都是缩写,但是大概都能猜到是什么意思,比如p后面是价格,id后面是标识,至于op和m的意思,我猜是什么会员价和原价?不过没关系,这里面已经有了我们想要的信息了。那么想拿到价格的方法也很简单了,按照前面了的路子,只要访问这个网址然后拿到输出传给BeautifulSoup对象,就能完成解析了。但是,我们目前的想法是以一个书目,也就是一个list为一个Item,这个json字符串似乎一次性传回了很多个条目的价格。当然,可以通过字符串处理然后选取合适的容器来取出每个图书Item的价格。但这和我们的程序设计逻辑不搭,这东西就和写文章一样,行文逻辑不一致,会让读的人感到非常困难。放到代码上就是难以维护,那么,有什么办法可以一条一条的取出价格就好了。
这个时候,不妨回头看看获取到这个json返回值的url,因为webapi,参数就在url上,真正的谜底就在谜面上。你想想,我们想获取一个条目的价格,那么如果你写程序,一定是把这个条目的标识传进去,然后获取到价格。而我们现在使用的这个url有点长。
但仔细一看,很多都不知道是什么意思,职业的敏感让我把眼光放在skuIds这个参数上面,再看看传回来的参数,很明显,每一条单独的json条目都对应了这个传进去的一个Id。这个时候大胆尝试的念头又在我心中泛起,试试看只传进去一个参数。在浏览器中输入这条修改后的地址。
啊!你会发现就返回了一条skuIds的记录!再试试把Id改为其他的,发现也能行!并且每次都能得到正确的结果!所以说,勇敢尝试是成功的第一步。这个时候就可以使用这个URL了。但是这个URL中还有很多不知道干啥的参数,作为一个强迫症患者,试试看全部删除这些不需要的参数,就留下一个SkuIds试试。如果你真的试过,会发现不行。会给你返回一个error "pdos_captcha"。这看起来是步子迈大了,扯着蛋了。但是不多打一个字的惰性促使着我想看看能不能少一点,于是先从第一个callback=JQueryxxx的加起。
你会惊奇的发现,成了!但是你如果你多试几次,你可能会发现,你会失败!为什么?这就是在网络爬虫中的一个重要问题。如果一个网站的任意url可以被人任意的访问,那么势必会造成很多问题,不然验证码有不会被发明出来了。当你不能访问的时候,大多数时候因为对方网站的某种反爬虫机制已经将你的某种标识标记为机器人,然后给你返回一个错误的或者是无法获取的信息。
(好奇心重的人在这里可能会问,这个JQuery后面的数字是干啥的?我怎么知道这个数字从哪来?每一个书调用的callback=JQueryxxx的数字都不一样吗?那我要在哪里搞到这个数字?绝对的好问题,但是,如果你是那种手贱的人,你会发现这个数字任意改成啥都能得到结果,我改成了1,一样可以有结果。如果你对这个技术有兴趣的话,可以看这里。)
但是我们是真机器人啊!我们要做的是爬虫啊!所以如何在爬虫程序中把自己伪装成类人类上网就很重要。办法很多,其实总结出来,我个人感觉就记住两个关键词就行了,伪装和暂停。
先说伪装,怎么把机器人伪装成人呢?人是通过浏览器上网的,大多数现代人类肯定是这样上网的,你要用curl命令,这也算是一股清流了。那么回到F12上来,使用F12你是可以看到request的,而在request中间,你可以看到每个http都是有头部,这个头部里面包含了很多信息,比如使用了什么版本的浏览器,有的网站会要求在头部有某些特定的字段或者特定的信息,服务器端只有验证了这些信息才会返回正确的信息。如果你有一点了解http协议的话,其实只要我们能按照这里的格式构造出一个和浏览器访问一摸一样的http请求头,那么就可以模拟浏览器去访问页面,那服务器端是不可能分得清是人还是机器人的。
那么python如何做到这一点?作为一个对爬虫十分友好的语言,做到这一步也很简单,只要你把构造好的头部作为参数传进相应的函数,就可以完美的做到这一点。至于这些浏览器的头部信息怎样构造?搜索呀!这种模板网上一大堆,整个过程一如代码10-15行所示:
伪装成浏览器一般只是爬虫程序伪装的一部分,另外一部分是使用不同的代理IP。因为计算机程序访问一个网站资源的速率远远大于人类,这一点很明显很容易被服务器端所识别。而同一IP孜孜不倦的访问某一个网址,除非你是某网站的超级粉丝,不然一般正常人不会有这样的行为。而解决这个的办法,是随机选取一些IP,然后伪装成这些IP去访问网站。原理和伪装成浏览器头部差不多,说实话并不复杂。而我在我的爬虫程序中并没有用代理IP,原因很简单,就像我在前言里说的,我这并不是一个完全的商业化爬虫。当然,这就造成了你使用这个爬虫的时候有可能会导致返回错误,但是我可以说一个我用的方法,简单快速而又方便,用你的手机当热点,然后运行这些爬虫,一般都不会因为IP问题而封杀。你要问我为啥知道这样做,这就多亏了我做过网络编程方面的经验了。你可以把这个当做练习,当然也是因为我懒,实话,不过如果有幸我的这组文章能被广泛阅读而又有人要求看看如何使用代理IP的话,我会加上的。
剩下的就是我说的另外一个类别的招数了,因为人类的反应速度有限,你不可能在毫秒级别的不停的点击网页链接,所以一些超级简单的sleep同样也可以让你唬住服务器。怎么样让服务器觉得你不是机器人的方法说句不负责任的话,主要还是靠灵活运用,这里面颇有一种与人斗,其乐无穷的感觉。
基本上这个commit里面的理论扯淡部分就完了,那么接下来自然就是动手干代码了。
我写软件的设计,如果随意思考也能叫设计的话,逻辑就是一定要符合人的自然思维。而在貌似在第一个commit之内,我们缺少一个图书类目的唯一标示,而在前面价格的json字符串中,又明显有ID样的表示,所以,自然而然的我就想在我爬到的数据形成的结构中来这么一个玩意儿。在price中,可以看到这个id叫dataSku,虽然不知道这个缩写是什么意思,但是并不妨碍我去寻找这么一个东西。在返回的HTML中,有很多地方可以取到这个Id,比如,在第一个commit里取得的项目连接中就有。但是我一直本着别的程序员也得使自己的程序保持一致性的原则,我还是选取了一个叫p-o-btn focus J_focus这样的class名的一个超链接元素。这个超链接元素中有个key就叫data-sku,看起来抗迭代性最强。也就是代码第27,28行所示。
而接下来的代码就是我获取价格的逻辑了,从33行到40行,python的另一个完美之处就是其字典数据结构对json字符串完美对接,在去除掉不需要的字符之后,直接就可以得到完美的json字符串,通过key直接就能取得value。
再接着后面,我选择把爬到的数据结构都放到一个字典里面,但是这样做其实是有利有弊的。在我这种程序中,其实没啥比较好优势,甚至对于交互性上,是不方便的。但是如果你对爬到的数据还有比较多的后续处理,先将其存储到一个结构中是比较好的做法。
到这里,这个commit真的是没啥好讲的了。让我继续扯淡下一个commit吧。