在人生苦短我用Python,本文助你快速入门这篇文章中,学习了Python的语法知识。现在我们就拿Python做个爬虫玩玩,如果中途个别API忘了可以回头看看,别看我,我没忘!(逃
网络编程
学习网络爬虫之前,有必要了解一下如何使用Python进行网络编程。既然说到网络编程,对于一些计算机网络的基础知识最好也有所了解。比如HTTP,在这里就不讲计算机基础了,贴出我之前的一篇博客。感兴趣的可以看看图解HTTP常见知识点总结。
网络编程是Python比较擅长的领域,内置了相关的库,第三方库也很丰富。下面主要介绍一下内置的urllib库和第三方的request库。
urllib库
urllib是Python内置的HTTP请求库,其使得HTTP请求变得非常方便。首先通过一个表格列出这个库的内置模块:
模块 | 作用 |
---|---|
urllib.request | HTTP请求模块,模拟浏览器发送HTTP请求 |
urllib.error | 异常处理模块,捕获由于HTTP请求产生的异常,并进行处理 |
urllib.parse | URL解析模块,提供了处理URL的工具函数 |
urllib.robotparser | robots.txt解析模块,网站通过robots.txt文件设置爬虫可爬取的网页 |
下面会演示一些常用的函数和功能,开始之前先import上面的几个模块。
urllib.request.urlopen函数
这个函数的作用是向目标URL发送请求,其主要有三个参数:url目标地址、data请求数据、timeout超时时间。该函数会返回一个HTTPResponse对象,可以通过该对象获取响应内容,示例如下:
response = urllib.request.urlopen("https://www.baidu.com/")
print(response.read().decode("utf8")) # read()是读取响应内容。decode()是按指定方式解码
可以看到我们使用这个函数只传入了一个URL,没传入data的话默认是None,表示是GET请求。接着再演示一下POST请求:
param_dict = {"key":"hello"} # 先创建请求数据
param_str = urllib.parse.urlencode(param_dict) # 将字典数据转换为字符串,如 key=hello
param_data=bytes(param_str,encoding="utf8") # 把字符串转换成字节对象(HTTP请求的data要求是bytes类型)
response = urllib.request.urlopen("http://httpbin.org/post",data=param_data) #这个网址专门测试HTTP请求的
print(response.read())
timeout就不再演示了,这个参数的单位是秒。怎么请求弄明白了,关键是要解析响应数据。比如响应状态码可以这么获取:response.status
。获取整个响应头:response.getheaders()
,也可以获取响应头里面某个字段的信息:response.getheader("Date")
,这个是获取时间。
urllib.request.Request类
虽然可以使用urlopen函数非常方便的发送简单的HTTP请求,但是对于一些复杂的请求操作,就无能为力了。这时候可以通过Request对象来构建更丰富的请求信息。这个类的构造方法有如下参数:
参数名词 | 是否必需 | 作用 |
---|---|---|
url | 是 | HTTP请求的目标URL |
data | 否 | 请求数据,数据类型是bytes |
headers | 否 | 头信息,可以用字典来构建 |
origin_req_host | 否 | 发起请求的主机名或IP |
unverifiable | 否 | 请求是否为无法验证的,默认为False。 |
method | 否 | 请求方式,如GET、POST等 |
url = "http://httpbin.org/get"
method = "GET"
# ...其他参数也可以自己构建
request_obj = urllib.request.Request(url=url,method=method) # 把参数传入Request的构造方法
response = urllib.request.urlopen(request_obj)
print(response.read())
urllib.error异常处理模块
该模块中定义了两个常见的异常:URLEEror和HTTPError,后者是前者的子类。示例如下:
url = "https://afasdwad.com/" # 访问一个不存在的网站
try:
request_obj = urllib.request.Request(url=url)
response = urllib.request.urlopen(request_obj)
except urllib.error.URLError as e:
print(e.reason) # reason属性记录着URLError的原因
产生URLError的原因有两种:1.网络异常,失去网络连接。2.服务器连接失败。而产生HTTPError的原因是:返回的Response urlopen函数不能处理。可以通过HTTPError内置的属性了解异常原因,属性有:reason记录异常信息、code记录响应码、headers记录请求头信息。
requests库
requests库是基于urllib开发的HTTP相关的操作库,相比urllib更加简洁、易用。不过requests库是第三方库,需要单独安装才能使用,可以通过这个命令安装:pip3 install requests
。
使用urllib中的urlopen时,我们传入data代表POST请求,不传入data代表GET请求。而在requests中有专门的函数对应GET还是POST。这些请求会返回一个requests.models.Response
类型的响应数据,示例如下:
import requests
response = requests.get("http://www.baidu.com")
print(type(response)) #输出 <class 'requests.models.Response'>
print(response.status_code) # 获取响应码
print(response.text) # 打印响应内容
上面的例子调用的是get函数,通常可以传入两个参数,第一个是URL,第二个是请求参数params。GET请求的参数除了直接加在URL后面,还可以使用一个字典记录着,然后传给params。对于其他的请求方法,POST请求也有个post函数、PUT请求有put函数等等。
返回的Response对象,除了可以获取响应码,它还有以下这些属性:
- content:二进制数据,如图片视频等
- url:请求的url
- encoding:响应信息的编码格式
- cookies:cookie信息
- headers:响应头信息
其他的函数就不一一演示,等需要用到的时候大家可以查文档,也可以直接看源码。比如post函数源码的参数列表是这样的:def post(url, data=None, json=None, **kwargs):
。直接看源码就知道了它需要哪些参数,参数名是啥,一目了然。不过接触Python后,有个非常不好的体验:虽然写起来比其他传统面向对象语言方便很多,但是看别人的源码时不知道参数类型是啥。不过一般写的比较好的源码都会有注释,比如post函数开头就会说明data是字典类型的。
urllib库中可以用Request类的headers参数来构建头信息。那么最后我们再来说一下requests库中怎么构建headers头信息,这在爬虫中尤为重要,因为头信息可以把我们伪装成浏览器。
我们直接使用字典把头信息里面对应的字段都填写完毕,再调用对应的get或post函数时,加上headers=dict就行了。**kwargs
就是接收这些参数的。
网络编程相关的API暂时就讲这些,下面就拿小说网站和京东为例,爬取上面的信息来练练手。
用爬虫下载小说
在正式写程序之前有必要说说爬虫相关的基础知识。不知道有多少人和我一样,了解爬虫之前觉得它是个高大上、高度智能的程序。实际上,爬虫能做的我们人类也能做,只是效率非常低。其爬取信息的逻辑也很朴实无华:通过HTTP请求访问网站,然后利用正则表达式匹配我们需要的信息,最后对爬取的信息进行整理。虽然过程千差万别,但是大体的步骤就是这样。其中还涉及了各大网站反爬虫和爬虫高手们的反反爬虫。
再者就是,具体网站具体分析,所以除了必要的后端知识,学习爬虫的基本前提就是起码看得懂HTML和会用浏览器的调试功能。不过这些就多说了,相信各位大手子都懂。
第一个实战我们就挑选一个简单点的小说网站:https://www.kanunu8.com/book3/6879/。 先看一下页面:
我们要做的就是把每个章节的内容都爬取下来,并以每个章节为一个文件,保存到本地文件夹中。
我们首先要获取每个章节的链接。按F12打开调式页面,我们通过HTML代码分析一下,如何才能获取这些章节目录?当然,如何找到章节目录没有严格限制,只要你写的正则表达式能满足这个要求即可。我这里就从正文这两个字入手,因为章节表格这个元素最开头的是这两字。我们来看一下源码:
我们要做的就是,写一个正则表达式,从正文二字开头,以</tbody>
结尾,获取图中红色大括号括起来的这段HTML代码。获取到章节目录所在的代码后,我们再通过a
标签获取每个章节的链接。注意:这个链接是相对路径,我们需要和网站URL进行拼接。
有了大概的思路后,我们开始敲代码吧。代码并不复杂,我就全部贴出来,主要逻辑我就写在注释中,就不在正文中说明了。如果忘了正则表达式就去上一篇文章里回顾一下吧。
import requests
import re
import os
"""
传入网站的html字符串
利用正则表达式来解析出章节链接
"""
def get_toc(html,start_url):
toc_url_list=[]
# 获取目录(re.S代表把/n也当作一个普通的字符,而不是换行符。不然换行后有的内容会被分割,导致表达式匹配不上)
toc_block=re.findall(r"正文(.*?)</tbody>",html,re.S)[0]
# 获取章节链接
# 啰嗦一句,Python中单引号和双引号都可以表示字符串,但是如果有多重引号时,建议区分开来,方便查看
toc_url = re.findall(r'href="(.*?)"',toc_block,re.S)
for url in toc_url:
# 因为章节链接是相对路径,所以得和网址进行拼接
toc_url_list.append(start_url+url)
return toc_url_list
"""
获取每一章节的内容
"""
def get_article(toc_url_list):
html_list=[]
for url in toc_url_list:
html_str = requests.get(url).content.decode("GBK")
html_list.append(html_str)
# 先创建个文件夹,文章就保存到这里面,exist_ok=True代表不存在就创建
os.makedirs("动物庄园",exist_ok=True)
for html in html_list:
# 获取章节名称(只有章节名的size=4,我们根据这个特点匹配),group(1)表示返回第一个匹配到的子字符串
chapter_name = re.search(r'size="4">(.*?)<',html,re.S).group(1)
# 获取文章内容(全文被p标签包裹),并且把<br />给替换掉,注意/前有个空格
text_block = re.search(r'<p>(.*?)</p>',html,re.S).group(1).replace("<br />","")
save(chapter_name,text_block)
"""
保存文章
"""
def save(chapter_name,text_block):
# 以写的方式打开指定文件
with open(os.path.join("动物庄园",chapter_name+".txt"),"w",encoding="utf-8") as f:
f.write(text_block)
# 开始
def main():
try:
start_url = "https://www.kanunu8.com/book3/6879/"
# 获取小说主页的html(decode默认是utf8,但是这个网站的编码方式是GBK)
html = requests.get(start_url).content.decode("GBK")
# 获取每个章节的链接
toc_url_list = get_toc(html,start_url)
# 根据章节链接获取文章内容并保存
get_article(toc_url_list)
except Exception as e:
print("发生异常:",e)
if __name__ == "__main__":
main()
最后看一下效果:
拓展:一个简单的爬虫就写完了,但是还有很多可以拓展的地方。比如:改成多线程爬虫,提升效率,这个小项目很符合多线程爬虫的使用场景,典型的IO密集型任务。还可以优化一下入口,我们通过main方法传入书名,再去网站查找对应的书籍进行下载。
我以多线程爬取为例,我们只需要稍微修改两个方法:
# 首先导入线程池
from concurrent.futures import ThreadPoolExecutor
# 我们把main方法修改一下
def main():
try:
start_url = "https://www.kanunu8.com/book3/6879/"
html = requests.get(start_url).content.decode("GBK")
toc_url_list = get_toc(html,start_url)
os.makedirs("动物庄园",exist_ok=True)
# 创建一个有4个线程的线程池
with ThreadPoolExecutor(max_workers=4) as pool:
pool.map(get_article,toc_url_list)
except Exception as e:
print("发生异常:",e)
map()
方法中,第一个参数是待执行的方法名,不用加()。第二个参数是传入到get_article
这个方法的参数,可以是列表、元组等。以本代码为例,map()
方法的作用就是:会让线程池中的线程去执行get_article
,并传入参数,这个参数就从toc_url_list
依次获取。比如线程A拿了``toc_url_list`的第一个元素并传入,那么线程B就拿第二个元素并传入。
既然我们知道了map()
方法传入的是一个元素,而get_article
原来接收的是一个列表,所以这个方法也需要稍微修改一下:
def get_article(url):
html_str = requests.get(url).content.decode("GBK")
chapter_name = re.search(r'size="4">(.*?)<',html_str,re.S).group(1)
text_block = re.search(r'<p>(.*?)</p>',html_str,re.S).group(1).replace("<br />","")
save(chapter_name,text_block)
通过测试,在我的机器上,使用一个线程爬取这本小说花了24.9秒,使用4个线程花了4.6秒。当然我只测试了一次,应该有网络的原因,时间不是非常准确,但效果还是很明显的。
爬取京东商品信息
有了第一个项目练手,是不是有点感觉呢?其实也没想象的那么复杂。下面我们再拿京东试一试,我想达到的目的是:收集京东上某个商品的信息,并保存到Excel表格中。这个项目中涉及了一些第三方库,不过大家可以先看我的注释,过后再去看它们的文档。
具体问题具体分析,在贴爬虫代码之前我们先分析一下京东的网页源码,看看怎么设计爬虫的逻辑比较好。
我们先在京东商城的搜索框里输入你想收集的商品,然后打开浏览器的调式功能,进入到Network,最后再点击搜索按钮。我们找一下搜索商品的接口链接是啥。
图中选中的网络请求就是搜索按钮对应的接口链接。拿到这个链接后我们就可以拼接URL,请求获取商品信息了。我们接着看商品搜索出来后,是怎么呈现的。
通过源码发现,每个商品对应一个li标签。一般商城网站都是由一些模板动态生成的,所以看上去很规整,这让我们的爬取难度也降低了。
我们点进一个看看每个商品里又包含什么信息:
同样相当规整,最外层li的class叫gl-item,里面每个div对应一个商品信息。知道这些后,做起来就相当简单了,就用这些class的名称来爬取信息。我还是直接贴出全部代码,该说的都写在注释里。贴之前说说每个方法的作用。search_by_keyword
:根据传入的商品关键词搜索商品。get_item_info
:根据网页源码获取商品信息。skip_page
:跳转到下一页并获取商品信息。save_excel
:把获取的信息保存到Excel。
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from pyquery import PyQuery
from urllib.parse import quote
import re
from openpyxl import Workbook
from fake_useragent import UserAgent
# 设置请求头里的设备信息,不然会被京东拦截
dcap = dict(DesiredCapabilities.PHANTOMJS)
# 使用随机设备信息
dcap["phantomjs.page.settings.userAgent"] = (UserAgent().random)
# 构建浏览器对象
browser = webdriver.PhantomJS(desired_capabilities=dcap)
# 发送搜索商品的请求,并返回总页数
def search_by_keyword(keyword):
print("正在搜索:{}".format(keyword))
try:
# 把关键词填入搜索链接
url = "https://search.jd.com/Search?keyword=" + \
quote(keyword)+"&enc=utf-8"
# 通过浏览器对象发送GET请求
browser.get(url)
# 等待请求响应
WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".gl-item"))
)
pages = WebDriverWait(browser, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > em:nth-child(1) > b"))
)
return int(pages.text)
except TimeoutException as e:
print("请求超时:"+e)
# 根据HTML获取对应的商品信息
def get_item_info(page):
# 获取网页源代码
html = browser.page_source
# 使用 PyQuery解析网页源代码
pq = PyQuery(html)
# 获取商品的li标签
items = pq(".gl-item").items()
datas = []
# Excel中的表头,如果当前是第一页信息就是添加表头
if page==1:
head = ["商品名称", "商品链接", "商品价格", "商品评价", "店铺名称", "商品标签"]
datas.append(head)
# 遍历当前页所有的商品信息
for item in items:
# 商品名称,使用正则表达式将商品名称中的换行符\n替换掉
p_name = re.sub("\\n", "", item.find(".p-name em").text())
href = item.find(".p-name a").attr("href") # 商品链接
p_price = item.find(".p-price").text() # 商品价钱
p_commit = item.find(".p-commit").text() # 商品评价
p_shop = item.find(".p-shop").text() # 店铺名称
p_icons = item.find(".p-icons").text()
# info代表某个商品的信息
info = []
info.append(p_name)
info.append(href)
info.append(p_price)
info.append(p_commit)
info.append(p_shop)
info.append(p_icons)
print(info)
# datas是当前页所有商品的信息
datas.append(info)
return datas
# 跳转到下一页并获取数据
def skip_page(page, ws):
print("跳转到第{}页".format(page))
try:
# 获取跳转到第几页的输入框
input_text = WebDriverWait(browser, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > input"))
)
# 获取跳转到第几页的确定按钮
submit = WebDriverWait(browser, 10).until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > a"))
)
input_text.clear() # 清空输入框
input_text.send_keys(page) # 在输入框中填入要跳转的页码
submit.click() # 点击确定按钮
# 等待网页加载完成,直到页面下方被选中并且高亮显示的页码,与页码输入框中的页码相等
WebDriverWait(browser, 10).until(
EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-num > a.curr"), str(page))
)
# 获取商品信息
datas = get_item_info(page)
# 如果有数据就保存到Excel中
if len(datas) > 0:
save_excel(datas, ws)
except TimeoutException as e:
print("请求超时:", e)
skip_page(page, ws) # 请求超时,重试
except Exception as e:
print("产生异常:", e)
print("行数:", e.__traceback__.tb_lineno)
# 保存数据到Excel中
def save_excel(datas, ws):
for data in datas:
ws.append(data)
def main():
try:
keyword = "手机" # 搜索关键词
file_path = "./data.xlsx" # 文件保存路径
# 创建一个工作簿
wb = Workbook()
ws = wb.create_sheet("京东手机商品信息",0)
pages = search_by_keyword(keyword)
print("搜索结果共{}页".format(pages))
# 按照顺序循环跳转到下一页(就不爬取所有的数据了,不然要等很久,如果需要爬取所有就把5改成pages+1)
for page in range(1, 5):
skip_page(page, ws)
# 保存Excel表格
wb.save(file_path)
except Exception as err:
print("产生异常:", err)
wb.save(file_path)
finally:
browser.close()
if __name__ == '__main__':
main()
从main方法开始,借助着注释,即使不知道这些库应该也能看懂了。下面是使用到的操作库的说明文档:
selenium:Selenium库是第三方Python库,是一个Web自动化测试工具,它能够驱动浏览器模拟输入、单击、下拉等浏览器操作。中文文档:https://selenium-python-zh.readthedocs.io/en/latest/index.html。部分内容还没翻译完,也可以看看这个:https://zhuanlan.zhihu.com/p/111859925。selenium建议安装低一点的版本,比如
pip3 install selenium==2.48.0
,默认安装的新版本不支持PhantomJS了。PhantomJS:是一个可编程的无界面浏览器引擎,也可以使用谷歌或者火狐的。这个不属于Python的库,所以不能通过pip3直接安装,去找个网址http://phantomjs.org/download.html下载安装包,解压后,把所在路径添加到环境变量中(添加的路径要到bin目录中)。文档:https://phantomjs.org/quick-start.html
openpyxl:Excel操作库,可直接安装,文档:https://openpyxl.readthedocs.io/en/stable/。
pyquery:网页解析库,可直接安装,文档:https://pythonhosted.org/pyquery/
拓展:可以加上商品的选择条件,比如价格范围、销量排行。也可以进入到详情页面,爬取销量排行前几的评价等。
今天就说到这里了,有问题感谢指出。如果有帮助可以点个赞、点个关注。接下来会学更多爬虫技巧以及其他的后端知识,到时候再分享给大家~
参考资料:《Python 3快速入门与实战》、《Python爬虫开发》、各种文档~