zoukankan      html  css  js  c++  java
  • 超贴心的,手把手教你写爬虫

    人生苦短我用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:
            # 商品名称,使用正则表达式将商品名称中的换行符
    替换掉
            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爬虫开发》、各种文档~

  • 相关阅读:
    BZOJ 3910: 火车
    POJ 1436.Horizontally Visible Segments-线段树(区间更新、端点放大2倍)
    洛谷 P3380 【模板】二逼平衡树(树套树)-线段树套splay
    计蒜客 38228. Max answer-线段树维护单调栈(The Preliminary Contest for ICPC China Nanchang National Invitational I. Max answer 南昌邀请赛网络赛) 2019ICPC南昌邀请赛网络赛
    CODEVS 4655 序列终结者-splay(区间更新、区间翻转、区间最值)
    计蒜客 38229.Distance on the tree-1.树链剖分(边权)+可持久化线段树(区间小于等于k的数的个数)+离散化+离线处理 or 2.树上第k大(主席树)+二分+离散化+在线查询 (The Preliminary Contest for ICPC China Nanchang National Invitational J. 2019ICPC南昌邀请赛网络赛)
    洛谷 P2042 [NOI2005]维护数列-Splay(插入 删除 修改 翻转 求和 最大的子序列)
    牛客网 桂林电子科技大学第三届ACM程序设计竞赛 G.路径-带条件的树的直径变形-边权最大,边数偶数的树上的最长路径-树形dp
    牛客网 桂林电子科技大学第三届ACM程序设计竞赛 D.寻找-树上LCA(树上a到b的路径上离c最近的点)
    牛客网 桂林电子科技大学第三届ACM程序设计竞赛 C.二元-K个二元组最小值和最大-优先队列+贪心(思维)
  • 原文地址:https://www.cnblogs.com/lbhym/p/14279546.html
Copyright © 2011-2022 走看看