网络爬虫的流程和原理
整个网络爬虫的流程可以分为如下的三个步骤:

整个爬虫的过程都可以使用 Python(本文使用 Python 3)来完成,每个步骤使用的模块大致如下:
- 获取网页:
requests、urllib、selenium(模拟浏览器) - 解析网页:
re正则表达式、BeautifulSoup、HTML 解析器lxml等 - 存储数据:存储至 txt、csv 等文件或是存储至 MySQL、MongoDB 等数据库
使用 requests 模块发起 HTTP 请求与抓取静态网页
使用 pip 命令安装requests模块。
| |
使用requests.get()可以向目标 URL 发送一个GET请求并返回页面内容与信息。
| |
此时我们就已经实现了一个静态网页的抓取。requests.get()方法返回的对象包含了关于本次请求的信息,通过它的一些实例变量和方法可以进行访问。下面列出了一些常用的变量和方法:
status_code:返回 HTTP 状态码headers:返回请求头encoding:返回编码类型text:返回响应内容(Unicode)content:返回响应内容(二进制数据)json():返回 JSON 响应内容url:返回网页 URL
上面只是一个所有参数都为默认时的请求。有时候我们可以对requests进行定制,使得请求符合我们的需求。
设置 URL 参数
可以使用一个字典用于保存参数名称与其对应的值,然后通过params参数传入requests.get()方法中。在下面的代码中,将值为value1的参数key1和值为value2的参数key2传入网页http://httpbin.org/get,发现 URL 已经正确编码:
| |
运行结果如下:
| |
定制请求头
同样使用字典存储自定义的请求头,然后通过headers参数传入。比较常用的一个用途是针对一些针对不同的设备返回不同内容的网站,可以自定义User-Agent值从而模拟不同类型的设备,从而获取不同的数据。除此之外,由于默认的User-Agent类似于python-requests/2.21.0,这种User-Agent很容易被服务器识别出来并进行反爬虫措施,此时可以修改成普通浏览器的User-Agent来顺利进行爬虫。以下是一个例子:
| |
发送其他类型的请求
事实上,requests 包可以发送所有类型的请求,如 POST、PUT、DELETE、HEAD、OPTIONS 等等,只需要将方法名改为请求类型即可。
| |
以 POST 请求为例:
| |
运行结果如下:
| |
设置超时
有时由于网络原因,服务器会长时间不返回内容,此时爬虫程序就会一直等待,这样就会影响爬虫的效率。因此,可以在请求的方法中设置 timeout 参数。其意义是,如果服务器在 timeout 秒内无应答,那么就会返回异常。
| |
运行结果:
| |
可以看到程序抛出了异常,因为在设定时间内,服务器没有返回内容。
抓取动态网页
首先需要了解一种异步更新技术:AJAX(Asynchronous JavaScript And XML,异步 JavaScript 和 XML)。这种技术可以在不重新加载网页的情况下对网页的某一部分进行更新,这样不仅节省了流量,而且减少了网页重载内容的下载。
但是问题来了:对于采用了这种技术的网页,有些信息并不会直接存在于HTML代码中,而是通过JavaScript代码来加载,这就使得爬虫比较麻烦。对于这种情况,可以通过以下两种方法实现:
解析真实地址抓取
有些网页中,虽然我们想要的数据并不在源代码中,但是我们也可以通过浏览器的“检查”功能来找到数据的真实地址,从而对数据进行爬取。
下面是某博客下的评论界面:

查看源代码,发现评论所在的位置只有一段JavaScript代码:

此时使用浏览器的“检查”,选择Network选项后刷新网页,就可以在下面看见加载这个网站加载的所有文件:

待评论加载出来之后,就可以在这些文件里面找到评论数据文件:

下一步,就可以直接使用requests请求这个链接了:
| |
运行结果是一段包含了json数据的字符串:

为了解析出我们想要的数据,我们需要先去除那些无效的部分,只提取json的那部分数据。处理json数据需要用到json包:
| |
输出结果如下:

这样就成功爬取了第一页评论内容,此时点击下一页同样可以在“检查”中找到。

对比两个网页链接,发现只有 offset 参数和 limit 参数是变化的。很容易看出,offset代表的是页数,limit代表的是一页里的评论数。所以只需修改offset的值,就可以批量爬取评论了。
以下是完整代码:
| |
使用 Selenium 模拟浏览器抓取
对于一些无法找到真实地址的网页,或者能找到真实地址但是由于加密或是其他原因导致无法批量爬取数据时,就可以使用 Selenium 库模拟浏览器来进行爬取。
Selenium 库是一个用于 Web 应用程序测试的工具,它直接运行在浏览器中,使用脚本控制浏览器进行操作。借助它来爬虫的原理是使用浏览器的渲染引擎,将JavaScript中加载出来的内容转为静态网页代码,这样我们就可以像爬取静态网页一样爬取动态网页了。
Selenium的安装和简单使用
Python 使用 pip 命令来安装 Selenium:
| |
Selenium 支持多种浏览器的调用,包括 IE、Firefox、Safari、Chrome、Opera 等。本文使用Chrome和Firefox进行操作。
首先使用 Selenium 打开浏览器,并打开一个网页:
| |
运行代码发现报错:
| |
为什么会这样呢?因为在新版的Selenium中,要令其顺利的控制浏览器,还需要安装一个driver。这个driver专门用于Selenium操控浏览器,不同的浏览器也有不同的driver,都可以在浏览器的官方网站找到。这里给出Chrome和Firefox的driver下载地址:chromedriver / geckodriver
安装好了之后,Windows系统需要将driver所在路径添加到系统环境变量PATH中,macOS系统只需要知道driver的存放路径即可。
接下来,在webdriver.Chrome()或webdriver.Firefox()方法中添加executable_path参数,填入driver所在的路径:
| |
这样就可以顺利打开网页了。可以看到在浏览器的地址栏有一行字“Chrome正受到自动测试软件的控制”,说明此时浏览器正在被Selenium控制。

使用Selenium来打开刚才的博客网站,在检查中就可以看到经过渲染后的HTML代码。此时评论数据都变成了HTML数据,我们就可以直接进行爬取。

使用Selenium选择和操作元素
除了打开网页外,Selenium还提供了很多选择元素的方法:
find_element_by_css_selector()通过CSS属性进行选择find_element_by_xpath()通过XPath进行选择find_element_by_id()通过元素的id选择find_element_by_name()通过元素的name选择find_element_by_link_text()通过超链接文字选择find_element_by_partial_link_text()通过部分超链接文字选择find_element_by_tag_name()通过元素的名称选择,如h1、pfind_element_by_class_name()通过元素的class选择
以上可以选择符合条件的单个元素。如果要选择多个元素,只需将element改为elements即可。
比如:find_elements_by_css_selector()
当然,还有一种更加简单粗暴的方法find_element() / find_elements()。使用此方法,只需将选择方法作为参数传入即可。
比如:find_element_by_id -> find_element("id","")
也可以使用Selenium来操作元素。下面列出了常用的操作方法:
clear清除元素内容send_keys()模拟按键输入click()单击元素is_selected()用于检查多选或者单选框是否被选中
举例:
| |
Selenium的高级操作
执行
JavaScript代码可以使用
driver.execute_script()方法来执行JavaScript代码。1driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # 下滑到页面底部禁止CSS的加载
Firefox:
1 2 3 4 5 6from selenium import webdriver fp = webdriver.FirefoxOptions() fp.set_preference("permissions.default.stylesheet", 2) driver=webdriver.Firefox(executable_path=r'/Users/hujiekang/Documents/geckodriver',options = fp) driver.get("https://www.bilibili.com")Chrome:
1 2 3 4 5 6 7from selenium import webdriver options = webdriver.chrome.options.Options() prefs = {'permissions.default.stylesheet':2} options.add_experimental_option("prefs", prefs) driver = webdriver.Chrome(executable_path='/Users/hujiekang/Documents/chromedriver',chrome_options=options) driver.get('https://www.bilibili.com/')加载网页效果:

禁止图片的显示
Firefox:
1 2 3 4 5 6from selenium import webdriver fp = webdriver.FirefoxOptions() fp.set_preference("permissions.default.image", 2) driver=webdriver.Firefox(executable_path=r'/Users/hujiekang/Documents/geckodriver',options = fp) driver.get("https://www.bilibili.com")Chrome:
1 2 3 4 5 6from selenium import webdriver options=webdriver.chrome.options.Options() prefs={"profile.managed_default_content_settings.images": 2} options.add_experimental_option("prefs",prefs) driver = webdriver.Chrome(executable_path=r'/Users/hujiekang/Documents/chromedriver',chrome_options=options) driver.get("https://www.bilibili.com")加载网页效果:

禁用
JavaScript的运行Firefox:
1 2 3 4 5 6 7from selenium import webdriver fp = webdriver.FirefoxOptions() fp.set_preference("javascript.enabled", False) driver=webdriver.Firefox(executable_path=r'/Users/hujiekang/Documents/geckodriver',options = fp) driver.get("https://www.baidu.com")Chrome:
1 2 3 4 5 6from selenium import webdriver options=webdriver.chrome.options.Options() prefs={"profile.managed_default_content_settings.javascript": 2} options.add_experimental_option("prefs",prefs) driver = webdriver.Chrome(executable_path=r'/Users/hujiekang/Documents/chromedriver',chrome_options=options) driver.get("https://www.baidu.com")可以看到,由于禁用了
JavaScript,百度返回了不带有JavaScript的网页。
因为很多时候,我们爬虫的数据只是一些文本,所以限制其他元素的加载可以明显提高网页加载速度,从而提高爬虫的效率。
更多信息,可以查看Selenium的官方文档。
页面的解析
爬取页面源代码之后,我们需要从源代码中提取出需要的数据。此时就需要解析页面。
使用正则表达式解析
正则表达式(英語:Regular Expression,常简写为regex、regexp或RE),又称正規表示式、正規表示法、規則運算式、常規表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器裡,正則表达式通常被用来检索、替换那些符合某个模式的文本。
| 模式 | 描述 | 模式 | 描述 |
|---|---|---|---|
. | 匹配除了换行符的任意字符 | \s | 匹配空白字符 |
* | 匹配前一个字符0次或多次 | \S | 匹配任何非空白字符 |
+ | 匹配前一个字符1次或多次 | \d | 匹配数字 |
? | 匹配前一个字符0次或1次 | \D | 匹配非数字 |
^ | 匹配字符串开头 | \w | 匹配字母和数字 |
$ | 匹配字符串结尾 | \W | 匹配非字母数字 |
() | 匹配括号内的表达式 | [] | 用来表示一组字符([]若以^开头,则匹配除去[]中字符的其他所有字符) |
Python官方文档中有关于正则表达式更详尽的介绍。如果想要测试或调试正则表达式,请访问此网站。
爬虫中常用的正则表达式匹配方法在re包中。以下是re包中的几个常用方法:
re.match()从字符串起始位置匹配一个模式,如果无匹配,则返回None。1 2 3 4 5import re m = re.match('www','www.baidu.com') # 从起始位置找到匹配 n = re.match('com','www.baidu.com') # 未从起始位置找到匹配 print(m) print(n)程序运行结果为:
1 2<re.Match object; span=(0, 3), match='www'> Nonere.search()扫描整个字符串,并返回第一个成功的匹配。
1 2 3 4 5import re m = re.search('com','www.baidu.com') n = re.match('com','www.baidu.com') print(m) print(n)程序运行结果为:
1 2<re.Match object; span=(10, 13), match='com'> Nonere.findall()找到字符串中的所有匹配。
1 2 3import re m = re.findall('[0-9]+','123456,234567,abcdef,345678,456789') print(m)程序运行结果为:
1['123456', '234567', '345678', '456789']findall()与match()和search()有一点不同:findall()返回的是一个列表,而后两个方法返回的是re.Match对象。re.compile()将正则表达式字符串编译为一个正则表达式对象。当一个正则表达式要多次使用时,使用对象会更加高效。
1 2 3 4import re word = '123456,234567,abcdef,345678,456789' p = re.compile('[0-9]+') print(p.search(word))程序运行结果为:
1<re.Match object; span=(0, 6), match='123456'>re.split()根据匹配分割字符串。 如果在正则表达式中捕获到括号,那么所有的组里的文字也会包含在列表里。如果参数
maxsplit非零, 最多进行maxsplit次分隔, 剩下的字符全部返回到列表的最后一个元素。1 2 3 4 5 6>>> re.split(r'\W+', 'Words, words, words.') ['Words', 'words', 'words', ''] >>> re.split(r'(\W+)', 'Words, words, words.') ['Words', ', ', 'words', ', ', 'words', '.', ''] >>> re.split(r'\W+', 'Words, words, words.', maxsplit = 1) ['Words', 'words, words.']
使用 BeautifulSoup 模块解析
安装BeautifulSoup
| |
BeautifulSoup支持多种解析器,下表列出了主要解析器的一些信息:
| 解析器 | 使用方法 | 优势 | 劣势 |
|---|---|---|---|
| Python标准库 | BeautifulSoup(html,"html.parser") | Python的内置标准库执行速度适中文档容错能力强 | 在Python3.2.2之前的版本容错能力差 |
lxml HTML解析器 | BeautifulSoup(html,"lxml") | 速度快文档容错能力强 | 需要安装C语言库 |
lxml XML解析器 | BeautifulSoup(html,"xml")BeautifulSoup(html,["lxml","xml"]) | 速度快唯一支持XML的解析器 | 需要安装C语言库 |
html5lib | BeautifulSoup(html,"html5lib") | 容错性最好以浏览器的方式解析文档生成HTML5格式的文档 | 速度慢不依赖外部扩展 |
BeautifulSoup的使用
获取网页数据之后,需要先将网页源代码转换为BeautifulSoup对象。转换之后可以使用soup.prettify()方法输出经过美化的HTML代码。
| |
部分输出如下图:

BeautifulSoup对象是一个树形的结构,它的每一个节点都是一个Python对象,所以使用BeautifulSoup获取网页内容就是一个提取对象内容的过程。
提取对象的方法大体上分为3种:
遍历文档树
遍历文档树就是对文档树的逐层访问。以百度为例,上面返回的
head部分代码如下:1 2 3 4 5 6 7 8 9<head> <meta content="text/html;charset=utf-8" http-equiv="content-type"/> <meta content="IE=Edge" http-equiv="X-UA-Compatible"/> <meta content="always" name="referrer"/> <link href="http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css" rel="stylesheet" type="text/css"/> <title> 百度一下,你就知道 </title> </head>若想获取标题内容,则需输入:
1soup.head.title.text输出:
'百度一下,你就知道'使用
.contents获取子节点的全部内容(返回list):1soup.body.div.contents # 查看body标签下的所有div标签输出:
1[' ', <div id="head"> <div class="head_wrapper"> <div class="s_form"> <div class="s_form_wrapper"> <div id="lg"> <img height="129" hidefocus="true" src="//www.baidu.com/img/bd_logo1.png" width="270"/> </div> <form action="//www.baidu.com/s" class="fm" id="form" name="f"> <input name="bdorz_come" type="hidden" value="1"/> <input name="ie" type="hidden" value="utf-8"/> <input name="f" type="hidden" value="8"/> <input name="rsv_bp" type="hidden" value="1"/> <input name="rsv_idx" type="hidden" value="1"/> <input name="tn" type="hidden" value="baidu"/><span class="bg s_ipt_wr"><input autocomplete="off" autofocus="" class="s_ipt" id="kw" maxlength="255" name="wd" value=""/></span><span class="bg s_btn_wr"><input class="bg s_btn" id="su" type="submit" value="百度一下"/></span> </form> </div> </div> <div id="u1"> <a class="mnav" href="http://news.baidu.com" name="tj_trnews">新闻</a> <a class="mnav" href="http://www.hao123.com" name="tj_trhao123">hao123</a> <a class="mnav" href="http://map.baidu.com" name="tj_trmap">地图</a> <a class="mnav" href="http://v.baidu.com" name="tj_trvideo">视频</a> <a class="mnav" href="http://tieba.baidu.com" name="tj_trtieba">贴吧</a> <noscript> <a class="lb" href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1" name="tj_login">登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a class="bri" href="//www.baidu.com/more/" name="tj_briicon" style="display: block;">更多产品</a> </div> </div> </div>, ' ', <div id="ftCon"> <div id="ftConw"> <p id="lh"> <a href="http://home.baidu.com">关于百度</a> <a href="http://ir.baidu.com">About Baidu</a> </p> <p id="cp">©2017 Baidu <a href="http://www.baidu.com/duty/">使用百度前必读</a> <a class="cp-feedback" href="http://jianyi.baidu.com/">意见反馈</a> 京ICP证030173号 <img src="//www.baidu.com/img/gs.gif"/> </p> </div> </div>, ' ']下面列出部分其他的属性:
.parent:获得父节点内容.parents:获得所有父节点内容.children:获得所有子标签的内容.descendants:对所有tag的子孙节点进行递归循环.name:获得标签名
搜索文档树
搜索文档树最常用的两个方法:
find()和find_all()。两个方法的用法类似,前者用于搜索单个节点,而后者用于搜索全部节点。下面还是以百度网页为例介绍这两个方法的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13import requests from bs4 import BeautifulSoup link = "http://www.baidu.com" r = requests.get(link) r.encoding = "utf-8" soup = BeautifulSoup(r.text, "lxml") links = soup.find_all("a",class_="mnav") for each in links: print(each['href']) divi = soup.find("div",id="lg") print(divi.contents)以上代码搜索了所有
class为mnav的a标签,并打印出了标签的href属性内容;还搜索了id为lg的div标签,并输出了标签内的所有内容。程序输出结果如下:1 2 3 4 5 6http://news.baidu.com http://www.hao123.com http://map.baidu.com http://v.baidu.com http://tieba.baidu.com [' ', <img height="129" hidefocus="true" src="//www.baidu.com/img/bd_logo1.png" width="270"/>, ' ']这两个方法的第一个参数用于查找标签,可传入字符串、正则表达式(需传入正则表达式对象)、列表、
True;第二个参数用于查找属性,如class(为了防止和Python内部关键字冲突,故写为class_)、id等等。如果需要搜索包含多个属性的标签,可通过一个字典传入多个属性。此外,若过滤条件是标签是否有这个属性,只需让该属性的值为True或False。CSS选择器CSS选择器方法soup.select()可以使用上面两种方式(遍历文档树、搜索文档树)来提取数据。首先可以逐层查找,标签之间用空格隔开:
1 2>>> soup.select(head title) [<title>百度一下,你就知道</title>]此外,也可以通过某标签的子标签进行直接遍历,标签间以
>隔开:1 2 3 4>>> soup.select("head > title") # 搜索head下所有的title标签(这里只有一个) [<title>百度一下,你就知道</title>] >>> soup.select("div > a") # 搜索div标签下的所有a标签 [<a class="mnav" href="http://news.baidu.com" name="tj_trnews">新闻</a>, <a class="mnav" href="http://www.hao123.com" name="tj_trhao123">hao123</a>, <a class="mnav" href="http://map.baidu.com" name="tj_trmap">地图</a>, <a class="mnav" href="http://v.baidu.com" name="tj_trvideo">视频</a>, <a class="mnav" href="http://tieba.baidu.com" name="tj_trtieba">贴吧</a>, <a class="bri" href="//www.baidu.com/more/" name="tj_briicon" style="display: block;">更多产品</a>]CSS选择器也支持直接搜索查找。- 按
class:soup.select(".mnav")/soup.select("[class~=mnav]") - 按
id:soup.select("#link1")/soup.select("a#link2") - 按多种
CSS选择器:soup.select("#link1,#link2") - 按属性:
soup.select('a[href]') - 按属性值:
soup.select('a[href="https://www.baidu.com"]') - 按语言设置:
soup.select('p[lang|=en]') - 按正则表达式:
1 2>>> soup.select('a[href^="http://www.baidu.com"') # 搜索链接以http://www.baidu.com/ 开头的a标签 [<a class="lb" href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1" name="tj_login">登录</a>, <a href="http://www.baidu.com/duty/">使用百度前必读</a>]
- 按
更多详细的介绍请查阅BeautifulSoup官方文档。
使用lxml解析
安装lxml
| |
lxml的使用
使用lxml提取网页源代码数据有三种方法:XPath选择器、CSS选择器、find()方法。后两种方法前面已介绍,这里介绍第一种方法。
XPath是一门在XML文档中查找信息的语言,它使用路径表达式来选择节点或节点集,也可以用在获取HTML数据中。获取XPath非常简单,在浏览器的“检查”页面,选中要获取数据的标签,右键单击“Copy”项里的“Copy XPath”项就可以把XPath复制到剪贴板。如下图所示,获取百度首页的title标签对应的XPath,得到的结果为/html/head/title。

当然,也可以根据XPath的路径表达式来获得元素的XPath。
下表对XPath路径表达式进行了描述:
| 表达式 | 描述 |
|---|---|
nodename | 选取此节点的所有子节点 |
/ | 从根节点选取 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑其位置 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
举几个例子加深印象:
| 路径表达式 | 结果 |
|---|---|
head | 选取head元素的所有子节点 |
/head | 选取根元素head |
head/title | 选取属于head子元素的所有title元素 |
//title | 选取所有title元素,无论其位置在哪 |
head//title | 选取head元素后代的所有title元素 |
//@lang | 选取所有lang属性 |
下面使用XPath获取title标签内容:
| |
运行结果如下:
| |
数据的存储
首先介绍基本的存储:将数据存储至文件中。
存储至txt文件
存储至txt非常简单,只需如下几行代码:
| |
运行程序,可以在桌面上发现多了一个test.txt,打开内容正是text字符串的内容。

这里介绍一下Python中的with关键字:
| |
其中的context是一个表达式,返回的是一个对象,var用来保存context表达式返回的对象,可以有单个或者多个返回值。with本身并没有异常捕获的功能,但是如果发生了运行时异常,它照样可以关闭文件释放资源。
with 语句实质是上下文管理。
- 上下文管理协议。包含方法
__enter__()和__exit__(),支持该协议对象要实现这两个方法。 - 上下文管理器,定义执行
with语句时要建立的运行时上下文,负责执行with语句块上下文中的进入与退出操作。 - 进入上下文的时候执行
__enter__()方法,如果设置as var语句,var变量接受__enter__()方法返回值。 - 如果运行时发生了异常,就退出上下文管理器。调用管理器
__exit__()方法。
open()函数的第一个参数是文件的路径,填入的路径既可以是绝对路径也可以是相对路径。为了防止反斜杠\转义,下面有三种方法来填写路径:
- 使用原始字符串:
r'C:\Users\you\desktop\test.txt' - 使用正斜杠
/:'C:/Users/you/desktop/test.txt' - 使用转义后的反斜杠
\\:'C:\\Users\\you\\desktop\\test.txt'
open()函数里面还有第二个参数,该参数用于设定打开文件的方式。还有几种打开文件的方式如下表:
| 读写方式 | 能否读写 | 若文件不存在 | 写入方式 |
|---|---|---|---|
| w | 写入 | 创建 | 覆盖写入 |
| w+ | 读取+写入 | 创建 | 覆盖写入 |
| r | 读取 | 报错 | 不可写入 |
| r+ | 读取+写入 | 报错 | 覆盖写入 |
| a | 写入 | 创建 | 追加写入 |
| a+ | 读取+写入 | 创建 | 追加写入 |
有时候,我们还需要读取文件内容,此时只需要把f.write(text)改为text = f.read()即可。
存储至csv文件
对csv文件进行读写和txt文件类似,只不过需要使用到csv包。我们创建一个test.csv文件,填入如下内容,保存在桌面:

下面的例子对test.csv进行读取:
| |
得到结果如下:

可以看到程序按行读取了csv文件,并每行用一个列表进行存储。
这里使用了UTF-8-sig字符编码,因为在Windows下用文本编辑器创建的文本文件,如果选择以UTF-8等Unicode格式保存,会在文件头(第一个字符)加入一个BOM(字节顺序标记,byte-order mark)标识。而UTF-8以字节为编码单元,它的字节顺序在所有系统中都是一样的,没有字节序的问题,也因此它实际上并不需要BOM。所以UTF-8会把BOM当成常规字符来读取并显示。下图是使用UTF-8编码的第一行输出:

写入csv文件与读取类似,使用的是csv.writer()方法:
| |
上述代码在之前创建的test.csv中追加了一行,运行后的文件如下:

存储至MySQL数据库
MySQL是一种使用SQL语言的关系数据库管理系统,它的安装很简单,此处不再介绍。接下来介绍使用Python来操作MySQL数据库,进行一些基础的数据库操作。为了方便操作,数据库已经提前建立好,名为scraping,并创建了一个名为top250_movie的数据表。具体操作代码如下(以下代码在MySQL自带的shell中运行):
| |
接下来,使用一个例子来演示Python对MySQL的操作。下面的代码会爬取豆瓣电影前250的相关信息(标题、外文标题、评分、主要演员、上映时间、国家、分类),并存储至top250_movie表中。对MySQL的操作需要使用到MySQLdb包。代码如下:
| |
页面的爬取使用了Selenium模拟浏览器,当然使用BeautifulSoup也可以实现。通过判断url的参数,可以实现自动翻页爬取。当数据爬取完毕后,使用cur.execute()方法来执行SQL语句,在这里是INSERT语句。为了将Python中的数据传入SQL语句中,需要把要传入的数据按顺序放于语句的后一个参数中,然后格式化的传入。
最终使用语句SELECT * FROM top250_movie查看爬取的结果:

详细信息可访问MySQLdb包的官方文档:https://mysqlclient.readthedocs.io/
存储至MongoDB数据库
MongoDB是一种非关系型数据库(NoSQL)。和传统的SQL数据库相比,NoSQL数据库中数据之间没有关系,所以读写性能非常高。而就爬虫的使用场景来看,当存储了上万条数据后,想要更改表的结构就会变得十分困难,所以使用NoSQL也是一个比SQL更优的选择。
MongoDB的下载、安装与初始化
下载MongoDB的官网链接:https://www.mongodb.com/download-center安装过程较为简单,不做过多介绍。
安装好MongoDB之后,有两种方式可以启动MongoDB。
第一种是直接以程序启动。进入MongoDB的安装目录(默认为C:/Program Files/MongoDB/Server/MongoDB的版本号)里面的bin文件夹,找到mongod.exe双击打开,发现一个窗口闪了一下就消失了。接下来再双击mongo.exe运行,MongoDB就成功启动了。mongod.exe是启动程序,所以应该先运行它,随后再打开主程序mongo.exe。下图使用了PowerShell运行,效果也是一样的。这种启动方法有一个缺点,就是当程序被关闭时,数据库就会断开,所以每次想使用时都要重新启动程序,比较麻烦。

第二种就解决了麻烦的问题。第二种方法使用Windows服务的方式打开,这样的话只要服务在运行,数据库就可以一直被连接而不会断开。
这个操作要在Windows下的命令提示符或是PowerShell下进行。以管理员身份运行cmd.exe或powershell.exe,输入cd MongoDB的安装目录/bin切换至MongoDB安装目录的bin文件夹。比如我的安装目录是C:/Program Files/MongoDB/Server/4.2,那么我就输入:
| |
接下来输入以下代码,来建立Windows服务:
| |
需要注意的是,--logpath后面的字符串为日志文件的存储位置,默认在安装目录下的log文件夹中,可以修改为任意位置。--dbpath后面的字符串则为数据库的存储位置,这里我把它建立在安装目录的data文件夹中。
服务建立好了,接下来只需要启动服务,MongoDB就已经在运行了。接下来直接双击mongo.exe就可以键入命令对数据库进行操作。为了方便,还可以把mongo.exe的路径添加至系统的PATH环境变量,这样直接在命令提示符窗口中输入mongo,就可以打开了。
| |

接下来就可以对数据库进行操作了。可以输入show dbs来查看现有的所有数据库:

NoSQL和SQL的一些区别
由于NoSQL和SQL有一定的区别,在部分定义上,MongoDB和SQL中的名称是不一样的,甚至MongoDB根本没有一些定义。下表进行了一些比较和解释:
SQL术语 | MongoDB术语 | 解释 |
|---|---|---|
database | database | 数据库 |
table | collection | 数据库表 |
row | document | 数据记录行 |
column | field | 数据字段 |
index | index | 索引 |
table joins | N/A | 表连接,MongoDB不支持(表间无联系) |
primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 |
除此之外,由于NoSQL的数据之间没有联系,MongoDB的数据行不需要设置相同的字段,而且相同的字段也不需要相同的数据类型。
使用Python操作MongoDB数据库
使用Python操作MongoDB数据库需要用到PyMongo库。同样使用pip安装:
| |
然后就可以使用Python操作MongoDB了。下列代码连接了数据库,并创建了一个名为scraping的数据库,在其中创建了一个名为movie的数据表。和SQL不同的是,创建数据库或数据表就是选择数据库或数据表。当发现数据库或数据表不存在时,则会自动创建一个并选择。
| |
接下来,把豆瓣TOP250电影的信息存入movie这个数据表中:
| |
程序运行结束后,就可以在mongo.exe中查看爬取的数据,具体命令如下:
| |
查看到的数据如下,MongoDB默认每次展示25条,输入it可以展示更多数据:

除了查询数据,下面列出了其他的一些操作命令:
db.collection.drop()从数据库中删除指定的数据表db.collection.dataSize()返回数据表的大小db.collection.deleteOne()删除数据表中的单条记录db.collection.deleteMany()删除数据表中的多条记录db.collection.findOne()执行查询并返回单条记录db.collection.findOneAndDelete()查找单条记录并将其删除db.collection.findOneAndReplace()查找单条记录并将其替换db.collection.findOneAndUpdate()查找单条记录并进行更新db.collection.insert()在数据表中创建一个新记录db.collection.insertMany()在数据表中插入几条新纪录db.collection.renameCollection()更改数据表的名称db.collection.update()修改数据表中的记录
更多MongoDB的操作以及PyMongo的使用,可以查阅它们的官方文档:PyMongo Documentation MongoDB Documentation
反爬虫问题
反爬虫的方式
不返回网页内容或延迟返回时间
这种方式主要使用以下三种方法来反爬虫:
- 通过IP的访问量反爬虫 若一个IP在一段时间内访问速度远大于正常人浏览网页的速度,服务端就会实施反爬虫,比如要求输入验证码或是直接禁止该IP访问。
- 通过
session的访问量反爬虫session意为“会话控制”,session对象存储特定用户会话的属性及配置。当用户在网页之间跳转时,session中的变量将一直存在,所以服务端也可以通过判断session的访问量来禁止爬虫。 - 通过User-Agent反爬虫
由于Python中
requests包发送HTTP请求的默认User-Agent为python-requests/x.x.x,服务端可以判断出这种非真正浏览器的User-Agent而予以封锁。当然也有在单个User-Agent访问量过大时对其进行封锁,但是这个方法很容易影响到其他正常的用户,所以一般不使用。
返回非目标网页
其具体表现为返回错误页、空白页以及爬取多页时只返回同一页。
设置登录才可查看和验证码
下面介绍处理登录表单和验证码的方法。
发送POST请求登录
打开某博客登录页面,使用BurpSuite抓包,用户名填写test,密码填写a12345,勾选记住登录信息后提交,在BurpSuite中可以看到POST方法传出的参数信息:

从中可以提取出我们想要的参数:用户名的参数为log;密码的参数为pwd;记住登录信息的参数为rememberme,值为forever;以及重定向到用户信息的界面等。借此,我们可以构造出一个POST数据的字典:
| |
下面创建一个session提交POST请求:
| |
打印的结果为200,说明登录成功。
使用cookies登录
在上面登录成功之后,会在本地产生cookies。cookies保存有之前登录的信息,所以可以直接通过调用cookies来登录。
将上面的代码添加几行,就可以把这次登录信息的cookies保存在Python源文件的目录:
| |
打开cookie文件,内容如下:

每一个cookie大概会定义4个参数:
Set-Cookie:
name:cookie的名称,一般被加密expires:cookie过期的时间path:cookie的路径domain:cookie的域名
接下来使用Python中的cookiejar包来加载cookies:
| |
上述代码打印的结果为200,说明登录成功。注意,这里请求的不是登录页面,而是登陆后的用户信息页面。
综合来看,就可以利用Python来对这个博客登录页面做一个完整的登录程序了。如果存在cookies,则直接使用cookies登录;否则使用用户名和密码登录。
代码如下:
| |
验证码的处理
处理验证码的原理很简单:通过requests获取到网页的源码,之后在源码中找到验证码图片网页链接并将其保存。保存之后有两种处理办法:人工处理和OCR处理。人工处理就是直接打开验证码图片,然后用户输入验证码后登录再继续;OCR则是将图片进行处理后通过OCR引擎识别出文本直接返回给程序。下面主要介绍OCR识别图片的方法。
OCR识别需要用到Python的pytesseract包、pillow包以及Tesseract-ocr软件。注意:需要修改源码pytesseract.py中的tesseract_cmd变量为tesseract.exe的路径,否则无法运行。如下图所示:

在进行OCR之前,需要对图片进行灰度和阈值化处理,这样可以减少识别字符的干扰,从而提高识别的准确率。
| |
处理效果如下:

接下来就可以使用pytesseract来识别出文字了。
| |
可以看到程序运行的结果和图片内容一致,说明OCR识别正确。

当然这种方法只适合背景不是特别复杂、字符的辨识度较高的情况,有些网站的验证码背景会添加很多不同颜色的色块、小点,或是使用辨识度不高的字母来干扰OCR,如5和S、o和0、1和I等情况,这些情况下,还是使用人工辨别正确率会高一些。
如何“反反爬虫”
修改User-Agent 可以使用以下代码来查看使用
requests发送请求的请求头:1 2 3import requests r = requests.get('https://www.baidu.com') print(r.request.headers)打印结果如下:
1{'User-Agent': 'python-requests/2.19.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}修改
User-Agent为正常浏览器的格式,使用一个字典来存储新的请求头信息,然后传入headers参数即可。1 2 3 4 5 6import requests # 自定义请求头 headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36'} #应用至requests请求 r = requests.get('https://www.baidu.com',headers = headers) print(r.request.headers)打印结果如下:
1{'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}修改爬虫的时间间隔
使用
time包里的time.sleep()方法可以使程序暂停运行指定时间。1 2 3 4 5import time t1 = time.time() time.sleep(5) t2 = time.time() print(t2-t1,"s")打印的结果为:
5.001166582107544 s为了能更真实的模拟真实用户的操作,不能让这个暂停的时间为一个固定且精确的值,于是可以使用随机数生成的办法来让程序暂停运行一个随机的时间。
1 2 3 4 5import time import random sleep_time = random.randint(0,5)+random.random() print(sleep_time,"s")random包用于提供随机数。上面使用了
random.randint()方法提供指定范围内的一个随机整数,random.random()方法用于生成一个0到1之间的随机数。 运行5次,结果如下:1 2 3 4 50.8260146189603876 s 4.848380768859137 s 3.8196842823676893 s 2.4634772662338826 s 3.5886359364884477 s使用代理 可以使用代理来让爬虫程序隐藏自己的真实IP:
1 2 3 4import requests proxies = {'http': 'http://xxx.xxx.xxx.xxx:xxxx'} r = requests.get('https://www.baidu.com', proxies = proxies)