社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

人见人爱的最新 Python 爬虫利器

编程派 • 4 年前 • 282 次点击  

文 | 过了即是客

编辑 | EarlGrey

推荐 | 编程派公众号(ID:codingpy)

Python上有一个非常著名的HTTP库——requests,相比大家都听说过,用过的人都说好!现在requests库的作者又发布了一个新库,叫做requests-html,看名字也能猜出来,这是一个解析HTML的库,而且用起来和requests一样爽,下面就来介绍一下它。

安装

安装requests-html非常简单,一行命令即可做到。需要注意一点就是,requests-html只支持Python 3.6及更新的版本,所以使用老版本的Python的同学需要更新一下Python版本了。看了下源代码,因为requests-html广泛使用了一个Python 3.6中的新特性——类型注解。

  1. pip install requests-html

基本使用

获取网页

requests-html和其他解析HTML库最大的不同点在于HTML解析库一般都是专用的,所以我们需要用另一个HTTP库先把网页下载下来,然后传给那些HTML解析库。而requests-html自带了这个功能,所以在爬取网页等方面非常方便。

下面的代码获取了糗事百科上面的文字段子页面,返回的对象r是 requests.Reponse类型,更确切的说是继承自前者的 requests_html.HTMLResponse类型。这里其实和requests库的使用方法差不多,获取到的响应对象其实其实也没啥用,这里的关键就在于 r.html这个属性,它会返回 requests_html.HTML这个类型,它是整个 requests_html库中最核心的一个类,负责对HTML进行解析。我们学习requests_html这个库,其实也就是学习这个HTML类的使用方法。

  1. from requests_html import HTMLSession


  2. session = HTMLSession()

  3. r = session.get('https://www.qiushibaike.com/text/' )

  4. // 查看页面内容

  5. print(r.html.html)

获取链接

linksabsolute_links两个属性分别返回HTML对象所包含的所有链接和绝对链接(均不包含锚点)。

  1. # 获取链接

  2. print(r.html.links)

  3. print(r.html.absolute_links)

结果为下(因为结果太长,所以我随便取了一点,看个意思就行):

  1. {'/article/104353012', '/article/120616112', '/users/32331196/'}

  2. {'https://www.qiushibaike.com/imgrank/', 'https://www.qiushibaike.com/article/120669516', 'https://www.qiushibaike.com/article/120682041'}

获取元素

request-html支持CSS选择器和XPATH两种语法来选取HTML元素。首先先来看看CSS选择器语法,它需要使用HTML的find函数,该函数有5个参数,作用如下:

  • selector,要用的CSS选择器;

  • clean,布尔值,如果为真会忽略HTML中style和script标签造成的影响(原文是sanitize,大概这么理解);

  • containing,如果设置该属性,会返回包含该属性文本的标签;

  • first,布尔值,如果为真会返回第一个元素,否则会返回满足条件的元素列表;

  • _encoding,编码格式。

下面是几个简单例子:

  1. # 首页菜单文本

  2. print(r.html.find('div#menu', first=True).text)

  3. # 首页菜单元素

  4. print(r.html.find('div#menu a'))

  5. # 段子内容

  6. print(list(map(lambda x: x.text, r.html.find('div.content span'))))

结果如下,因为段子太多,所以随便选了两个:

  1. 热门 24小时 热图 文字 穿越 糗图 新鲜

  2. [<Element 'a' href='/' rel=('nofollow',)>, <Element 'a' href='/hot/'>, <Element 'a' href= '/imgrank/'>, <Element 'a' id='highlight' href='/text/'>, <Element 'a' href='/history/'>, <Element 'a' href='/pic/'>, <Element 'a' href='/textnew/'>]

  3. ['有一次,几位大城市的朋友来家里玩,我招待他们吃风干羊肉做臊子的饸饹面,这是我们老家最具特色的美食!饭快熟的时候,老婆让我在园子里摘点“芫荽 ”,朋友问我,“芫荽”是什么东东?我给他们翻译解释说:我们本地土话叫“芫荽”,你们城里人讲普通话叫香菜,他们还大笑了一场。\n前天下雨没事儿干,翻看新华字典,突然发现“芫荽”才是香菜的学名,Tm香菜才是土话!而且我们地方方言就这两个字发音还特别标准!', '昨天晚上跟老婆吵架,他抓起我的手机就摔了。我立马摔了他的,结果我的还能用,他的坏了。高潮是人家立刻出门买了个新的!我艹,是不是中计了??', '小姨要去高铁站,我看着大大小小的箱子说:坐公交车要转车,转来转去要一个多小时,太不方便了,不如我开车送你吧。\n小姨迟疑了一下,同意了。\n我准时把小姨送到了高铁站,正好赶上检票。\n小姨高兴地说:自己开车就是方便,不过幸好你妈聪明,让我们提前两个多小时就出发了!'

然后是XPATH语法,这需要另一个函数xpath的支持,它有4个参数如下:

  • selector,要用的XPATH选择器;

  • clean,布尔值,如果为真会忽略HTML中style和script标签造成的影响(原文是sanitize,大概这么理解);

  • first,布尔值,如果为真会返回第一个元素,否则会返回满足条件的元素列表;

  • _encoding,编码格式。

还是上面的例子,不过这次使用XPATH语法:

  1. print(r.html.xpath("//div[@id='menu']", first=True).text)

  2. print(r.html.xpath("//div[@id='menu']/a"))

  3. print(r.html.xpath("//div[@class='content']/span/text()" ))

输出和上面那个几乎一样,之所以说是“几乎”,因为第三个输出会多出几个换行符,不知道什么原因。需要注意的一点是如果XPATH中包含 text()@href这样的子属性,那么结果相应的会变成简单的字符串类型,而不是HTML元素。

  1. ['\n\n\n我一份文件忘家里了,又懒得回家取,就给小姨子发短信息: 帮我把文件送来,晚上我谢谢你。等半天也没送来文件,我只好打个车回家自己拿,到家一进屋,我就发现气氛不对劲,老婆铁青着脸,两手掐着腰,小姨子站旁边对我怒目而视。']

元素内容

糗事百科首页LOGO的HTML代码如下所示:

  1. class="logo" id="hd_logo">
  2. href="/">

    糗事百科

我们来选取这个元素:

  1. e = r.html.find("div#hd_logo", first=True)

要获取元素的文本内容,用text属性:

  1. print(e.text)

  2. # 糗事百科

要获取元素的attribute,用attr属性:

  1. print(e.attrs)

  2. # {'class': ('logo',), 'id': 'hd_logo'}

要获取元素的HTML代码,用html属性:

  1. print(e.html)

  2. #

  3. #

    糗事百科

  4. #

要搜索元素的文本内容,用search函数,比如说我们现在想知道是糗事什么科:

  1. print(e.search("糗事{}科")[0])

  2. # 百

最后还有前面提到的两个链接属性:

  1. print(e.absolute_links)

  2. print(e.links)

  3. # {'https://www.qiushibaike.com/'}

  4. # {'/'}

进阶用法

这一部分我懒得找例子了,所以用官网上的例子。

JavaScript支持

有些网站是使用JavaScript渲染的,这样的网站爬取到的结果只有一堆JS代码,这样的网站requests-html也可以处理,关键一步就是在HTML结果上调用一下render函数,它会在用户目录(默认是 ~/.pyppeteer/)中下载一个chromium,然后用它来执行JS代码。下载过程只在第一次执行,以后就可以直接使用chromium来执行了。唯一缺点就是chromium下载实在太太太太太太慢了,没有科学上网的同学可能无法使用该功能了。

  1. >>> r = session.get('http://python-requests.org/')


  2. >>> r.html.render()

  3. [W:pyppeteer.chromium_downloader] start chromium download.

  4. Download may take a few minutes.

  5. [W:pyppeteer.chromium_downloader] chromium download done.

  6. [W:pyppeteer.chromium_downloader] chromium extracted to: C:\Users\xxxx\.pyppeteer\local-chromium\571375

  7. >>> r.html.search('Python 2 will retire in only {months} months!')['months']

  8. ''

render函数还有一些参数,顺便介绍一下(这些参数有的还有默认值,直接看源代码方法参数列表即可):

  • retries: 加载页面失败的次数

  • script: 页面上需要执行的JS脚本(可选)

  • wait: 加载页面钱的等待时间(秒),防止超时(可选)

  • scrolldown: 页面向下滚动的次数

  • sleep: 在页面初次渲染之后的等待时间

  • reload: 如果为假,那么页面不会从浏览器中加载,而是从内存中加载

  • keep_page: 如果为真,允许你用 r.html.page访问页面

比如说简书的用户页面上用户的文章列表就是一个异步加载的例子,初始只显示最近几篇文章,如果想爬取所有文章,就需要使用scrolldown配合sleep参数模拟下滑页面,促使JS代码加载所有文章。

智能分页

有些网站会分页显示内容,例如reddit。

  1. >>> r = session.get('https://reddit.com')

  2. >>> for html in r.html:

  3. ... print(html)

  4. 'https://www.reddit.com/'>

  5. 'https://www.reddit.com/?count=25&after=t3_81puu5'>

  6. 'https://www.reddit.com/?count=50&after=t3_81nevg'>

  7. 'https://www.reddit.com/?count=75&after=t3_81lqtp'>

  8. 'https://www.reddit.com/?count=100&after=t3_81k1c8'>

  9. 'https://www.reddit.com/?count=125&after=t3_81p438' >

  10. 'https://www.reddit.com/?count=150&after=t3_81nrcd'>

这样的话,请求下一个网页就很容易了。

  1. >>> r = session.get('https://reddit.com')

  2. >>> r.html.next()

  3. 'https://www.reddit.com/?count=25&after=t3_81pm82'

直接使用HTML

前面介绍的都是通过网络请求HTML内容,其实requests-html当然可以直接使用,只需要直接构造HTML对象即可:

  1. >>> from requests_html import HTML

  2. >>> doc = """"""


  3. >>> html = HTML(html=doc)

  4. >>> html.links

  5. {'https://httpbin.org'}

直接渲染JS代码也可以:

  1. # 和上面一段代码接起来

  2. >>> script = """

  3. () => {

  4. return {

  5. width: document.documentElement.clientWidth,

  6. height: document.documentElement.clientHeight,

  7. deviceScaleFactor: window.devicePixelRatio,

  8. }

  9. }

  10. """

  11. >>> val = html.render(script=script, reload=False)


  12. >>> print(val)

  13. {'width': 800, 'height': 600, 'deviceScaleFactor': 1}


  14. >>> print (html.html)

  15. </head>/httpbin.org">

自定义请求

前面都是简单的用GET方法获取请求,如果需要登录等比较复杂的过程,就不能用get方法了。HTMLSession类包含了丰富的方法,可以帮助我们完成需求。下面介绍一下这些方法。

自定义用户代理

有些网站会使用UA来识别客户端类型,有时候需要伪造UA来实现某些操作。如果查看文档的话会发现 HTMLSession上的很多请求方法都有一个额外的参数 **kwargs,这个参数用来向底层的请求传递额外参数。我们先向网站发送一个请求,看看返回的网站信息。

  1. from pprint import pprint

  2. r = session.get('http://httpbin.org/get')

  3. pprint(json.loads(r.html.html))

返回的结果如下:

  1. {'args': {},

  2. 'headers': {'Accept': '*/*',

  3. 'Accept-Encoding': 'gzip, deflate',

  4. 'Connection': 'close',

  5. 'Host': 'httpbin.org',

  6. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) '

  7. 'AppleWebKit/603.3.8 (KHTML, like Gecko) '

  8. 'Version/10.1.2 Safari/603.3.8'},

  9. 'origin': '110.18.237.233',

  10. 'url': 'http://httpbin.org/get'}

可以看到UA是requests-html自带的UA,下面换一个UA:

  1. ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0'

  2. r = session.get( 'http://httpbin.org/get', headers={'user-agent': ua})

  3. pprint(json.loads(r.html.html))

可以看到UA确实发生了变化:

  1. {'args': {},

  2. 'headers': {'Accept': '*/*',

  3. 'Accept-Encoding': 'gzip, deflate',

  4. 'Connection': 'close',

  5. 'Host': 'httpbin.org',

  6. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) '

  7. 'Gecko/20100101 Firefox/62.0'},

  8. 'origin': '110.18.237.233' ,

  9. 'url': 'http://httpbin.org/get'}

当然这里仅仅是换了一个UA,如果你有需要可以在header中修改其他参数。

模拟表单登录

HTMLSession带了一整套的HTTP方法,包括get、post、delete等,对应HTTP中各个方法。比如下面我们就来模拟一下表单登录:

  1. # 表单登录

  2. r = session.post('http://httpbin.org/post', data={'username': 'yitian', 'passwd': 123456})

  3. pprint(json.loads(r.html.html))

结果如下,可以看到forms中确实收到了提交的表单值:

  1. {'args': {},

  2. 'data': '',

  3. 'files': {},

  4. 'form': { 'passwd': '123456', 'username': 'yitian'},

  5. 'headers': {'Accept': '*/*',

  6. 'Accept-Encoding': 'gzip, deflate',

  7. 'Connection': 'close',

  8. 'Content-Length': '29',

  9. 'Content-Type': 'application/x-www-form-urlencoded',

  10. 'Host': 'httpbin.org',

  11. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) '

  12. 'AppleWebKit/603.3.8 (KHTML, like Gecko) '

  13. 'Version/10.1.2 Safari/603.3.8'},

  14. 'json': None,

  15. 'origin': '110.18.237.233',

  16. 'url': 'http://httpbin.org/post'}

如果有上传文件的需要,做法也是类似的。如果了解过requests库的同学可能对这里的做法比较熟悉,没有错,这其实就是requests的用法。requests-html通过暴露 **kwargs的方法,让我们可以对请求进行定制,将额外参数直接传递给底层的requests方法。所以如果有什么疑问的话,直接去看requests文档就好了。

爬虫例子

文章写完了感觉有点空洞,所以补充了几个小例子。不得不说requests-html用起来还是挺爽的,一些小爬虫例子用scrapy感觉有点大材小用,用requests和BeautifulSoup又感觉有点啰嗦,requests-html的出现正好弥补了这个空白。大家学习一下这个库,好处还是很多的。

爬取简书用户文章

简书用户页面的文章列表就是一个典型的异步加载例子,用requests-html的话可以轻松搞定,如下所示,仅仅5行代码。

  1. r = session.get('https://www.jianshu.com/u/7753478e1554')

  2. r.html.render(scrolldown=50, sleep=.2)

  3. titles = r.html.find('a.title')

  4. for i, title in enumerate(titles):

  5. print(f'{i+1} [{title.text}](https://www.jianshu.com{title.attrs["href"]})')

当然这个例子还有所不足,就是通用性稍差,因为文章列表没有分页机制,需要一直往下拉页面,考虑到不同的用户文章数不同,需要先获取用户总文章数,然后在计算一下应该下滑页面多少次,这样才能取得较好的效果。这里仅仅简单获取一些我自己的文章,就不往复杂写了。

爬取天涯论坛

以前经常在天涯论坛上追一些帖子,现在正好写一个爬虫,把连载的好帖子一次性爬下来弄成一个文件。

  1. # 爬取天涯论坛帖子

  2. url = 'http://bbs.tianya.cn/post-culture-488321-1.shtml'

  3. r = session.get(url)

  4. # 楼主名字

  5. author = r.html.find('div.atl-info span a', first=True).text

  6. # 总页数

  7. div = r.html.find('div.atl-pages', first=True)

  8. links = div.find('a')

  9. total_page = 1 if links == [] else int (links[-2].text)

  10. # 标题

  11. title = r.html.find('span.s_title span', first=True).text


  12. with io.open(f'{title}.txt', 'x', encoding='utf-8') as f:

  13. for i in range(1, total_page + 1):

  14. s = url.rfind('-')

  15. r = session.get(url[:s + 1] + str(i) + '.shtml')

  16. # 从剩下的里面找楼主的帖子

  17. items = r.html.find(f'div.atl-item[_host={author}]')

  18. for item in items:

  19. content: str = item.find('div.bbs-content', first=True).text

  20. # 去掉回复

  21. if not content.startswith('@'):

  22. f.write(content + "\n")

爬完之后,看了一下,700多k的文件,效果不错。

原文:https://blog.csdn.net/u011054333/article/details/81055423

回复下方「关键词」,获取优质资源


回复关键词「 pybook03」,立即获取主页君与小伙伴一起翻译的《Think Python 2e》电子版

回复关键词「pybooks02」,立即获取 O'Reilly 出版社推出的免费 Python 相关电子书合集

回复关键词「书单02」,立即获取主页君整理的 10 本 Python 入门书的电子版



豆瓣 9.1 分,中文版销量 30 多万,零基础也能用这本书学会 Python

你想要的 IT 电子资源,这里可能都有


Python 或将超越 C、Java,成为最受欢迎的语言

Python 容器使用的 5 个技巧和 2 个误区

如何写出优雅的 Python 函数?

题图:pexels,CC0 授权。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/36647
 
282 次点击