超贴心的,手把手教你写爬虫

人生苦短我用Python,本文助你快速入门这篇文章中,学习了Python的语法知识。如今咱们就拿Python作个爬虫玩玩,若是中途个别API忘了能够回头看看,别看我,我没忘!(逃html

网络编程

​ 学习网络爬虫以前,有必要了解一下如何使用Python进行网络编程。既然说到网络编程,对于一些计算机网络的基础知识最好也有所了解。好比HTTP,在这里就不讲计算机基础了,贴出我以前的一篇博客。感兴趣的能够看看图解HTTP常见知识点总结python

​ 网络编程是Python比较擅长的领域,内置了相关的库,第三方库也很丰富。下面主要介绍一下内置的urllib库和第三方的request库。web

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对象,能够经过该对象获取响应内容,示例以下: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"),这个是获取时间。浏览器

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,后者是前者的子类。示例以下: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库

​ 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爬虫开发》、各类文档~

相关文章
相关标签/搜索