在人生苦短我用Python,本文助你快速入门这篇文章中,学习了Python的语法知识。如今咱们就拿Python作个爬虫玩玩,若是中途个别API忘了能够回头看看,别看我,我没忘!(逃html
学习网络爬虫以前,有必要了解一下如何使用Python进行网络编程。既然说到网络编程,对于一些计算机网络的基础知识最好也有所了解。好比HTTP,在这里就不讲计算机基础了,贴出我以前的一篇博客。感兴趣的能够看看图解HTTP常见知识点总结。python
网络编程是Python比较擅长的领域,内置了相关的库,第三方库也很丰富。下面主要介绍一下内置的urllib库和第三方的request库。web
urllib是Python内置的HTTP请求库,其使得HTTP请求变得很是方便。首先经过一个表格列出这个库的内置模块:正则表达式
模块 | 做用 |
---|---|
urllib.request | HTTP请求模块,模拟浏览器发送HTTP请求 |
urllib.error | 异常处理模块,捕获因为HTTP请求产生的异常,并进行处理 |
urllib.parse | URL解析模块,提供了处理URL的工具函数 |
urllib.robotparser | robots.txt解析模块,网站经过robots.txt文件设置爬虫可爬取的网页 |
下面会演示一些经常使用的函数和功能,开始以前先import上面的几个模块。编程
这个函数的做用是向目标URL发送请求,其主要有三个参数:url目标地址、data请求数据、timeout超时时间。该函数会返回一个HTTPResponse对象,能够经过该对象获取响应内容,示例以下:json
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")
,这个是获取时间。浏览器
虽然可使用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())
该模块中定义了两个常见的异常:URLEEror和HTTPError,后者是前者的子类。示例以下:cookie
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库是基于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对象,除了能够获取响应码,它还有如下这些属性:
其余的函数就不一一演示,等须要用到的时候你们能够查文档,也能够直接看源码。好比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爬虫开发》、各类文档~