写点什么

requests-html 库初识 + 无资料解 BUG 之 I/O error : encoder error,Python 爬虫第 30 例

作者:梦想橡皮擦
  • 2021 年 11 月 20 日
  • 本文字数:4287 字

    阅读完需:约 14 分钟

本篇博客是《爬虫 120 例》的第 30 例,新学习一个爬虫框架 requests-html,该框架作者就是 requests 的作者,所以盲猜就很好用啦。

知识铺垫工作

requests-html 模块安装使用 pip install requests-html 即可,官方手册查询地址:https://requests-html.kennethreitz.org/,官方并没有直接的中文翻译,在检索过程中,确实发现了一版中文手册,在文末提供。


先看一下官方对该库的基本描述:


  • Full JavaScript support!(完全支持 JS,这里手册还重点标记了一下,初学阶段可以先忽略)

  • CSS Selectors (a.k.a jQuery-style, thanks to PyQuery).(集成了 pyquery 库,支持 css 选择器)

  • XPath Selectors, for the faint at heart.(支持 XPath 选择器)

  • Mocked user-agent (like a real web browser).(mock UA 数据,这点不错)

  • Automatic following of redirects.(自动跟踪重定向)

  • Connection–pooling and cookie persistence.(持久性 COOKIE)

  • The Requests experience you know and love, with magical parsing abilities.(额,这最后一点,各位自己领悟吧)


Only Python 3.6 is supported. 仅支持 Python 3.6 ,实测发现 3.6 以上版本依旧可以。


对于该库的简单使用,代码如下所示:


from requests_html import HTMLSessionsession = HTMLSession()
r = session.get('https://python.org/')
print(r)
复制代码


首先从 requests_html 库导入 HTMLSession 类,然后将其实例化之后,调用其 get 方法,发送请求,得到的 r 输出为 <Response [200]>,后续即可使用内置的解析库对数据进行解析。


由于该库是解析 html 对象,所以可以查看对应的 html 对象包含哪些方法与与属性。


通过 dir 函数查阅。


print(dir(r.html))# 输出如下内容:['__aiter__', '__anext__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__','__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__','__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__','__weakref__', '_async_render', '_encoding', '_html', '_lxml', '_make_absolute', '_pq', 'absolute_links', 'add_next_symbol','arender', 'base_url', 'default_encoding', 'element', 'encoding', 'find', 'full_text', 'html', 'links', 'lxml', 'next','next_symbol', 'page', 'pq', 'raw_html', 'render', 'search', 'search_all', 'session', 'skip_anchors', 'text', 'url', 'xpath']
复制代码


该函数只能输入大概内容,细节还是需要通过 help 函数查询,例如:


html 对象的方法包括


  • find:提供一个 css 选择器,返回一个元素列表;

  • xpath:提供一个 xpath 表达式,返回一个元素列表;

  • search: 根据传入的模板参数,查找 Element 对象;

  • search_all:同上,返回的全部数据;


html 对象的属性包括


  • links:返回页面所有链接;

  • absolute_links:返回页面所有链接的绝对地址;

  • base_url:页面的基准 URL;

  • htmlraw_htmltext:以 HTML 格式输入页面,输出未解析过的网页,提取页面所有文本;


有了上述内容铺垫之后,在进行 Python 爬虫的编写就会变的容易许多,requests-html 库将通过 3~4 个案例进行学习掌握,接下来进入第一个案例。

目标站点分析

本次要采集的目标网站为:http://www.world68.com/top.asp?t=5star&page=1,目标站点描述为【全球名站】。



在获取数据源发送请求前,忽然想起可以动态修改 user-agent,查阅该库源码发现,它只是使用了 fake_useragent 库来进行操作,并无太神奇的地方,所以可用可不用该内容。


DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8'
def user_agent(style=None) -> _UserAgent: """Returns an apparently legit user-agent, if not requested one of a specific style. Defaults to a Chrome-style User-Agent. """ global useragent if (not useragent) and style: useragent = UserAgent()
return useragent[style] if style else DEFAULT_USER_AGENT
复制代码


其余内容相对比较简单,页码规则如下:


http://www.world68.com/top.asp?t=5star&page=1http://www.world68.com/top.asp?t=5star&page=2
复制代码


累计页数直接在底部进行了展示,可以设计为用户手动输入,即 input 函数实现。


目标数据存储网站名网站地址即可,基于此,开始编码。

编码时间

首先通过单线程实现 requests-html 的基本逻辑,注意到下述代码非常轻量,


from requests_html import HTMLSession
session = HTMLSession()
page_size = int(input("请输入总页码:"))for page in range(1, page_size + 1):
world = session.get(f'http://www.world68.com/top.asp?t=5star&page={page}') world.encoding = 'gb2312' # world.html.encoding = "gb2312" # print(world.text) print("正在采集数据", world.url) title_a = world.html.find('dl>dt>a') for item in title_a: name = item.text url = item.attrs['href'] with open('webs.txt', "a+", encoding="utf-8") as f: f.write(f"{name},{url}\n")
复制代码


上述代码重点部分说明如下:


  • world.encoding,设置了网页解析编码;

  • world.html.find('dl>dt>a') 通过 css 选择器,查找所有的网页标题元素;

  • item.text 提取网页标题内容;

  • item.attrs['href'] 获取元素属性,即网站域名。


运行效果如下所示,获取到的 3519 个站点,就不在提供了,简单运行 1 分钟代码,即可得到。



由于上述代码太少了,完全不够今日代码量,我们顺手将其修改为多线程形式。


import requests_htmlimport threadingimport timeimport fcntl

class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self)
def run(self): global page, lock, page_size while True: lock.acquire(True) if page >= page_size: lock.release() break else: page += 1 lock.release() requests_html.DEFAULT_ENCODING = "gb18030" session = requests_html.HTMLSession()
print("正在采集第{}页".format(page), "*" * 50) try: page_url = f'http://www.world68.com/top.asp?t=5star&page={page}' world = session.get(page_url, timeout=10) print("正在采集数据", world.url) # print(world.html) title_a = world.html.find('dl>dt>a') print(title_a) my_str = ""
for item in title_a: name = item.text url = item.attrs['href'] my_str += f"{name.encode('utf-8').decode('utf-8')},{url}\n"
with open('thread_webs.txt', "a+", encoding="utf-8") as f: fcntl.flock(f.fileno(), fcntl.LOCK_EX) # 文件加锁 f.write(f"{my_str}")
except Exception as e: print(e, page_url)

if "__main__" == __name__: page_size = int(input("请输入总页码:")) page = 0 thread_list = []
# 获取开始时间 start = time.perf_counter()
lock = threading.Lock() for i in range(1, 5): t = MyThread() thread_list.append(t) for t in thread_list: t.start() for t in thread_list: t.join() # 获取时间间隔 elapsed = (time.perf_counter() - start) print("程序运行完毕,总耗时为:", elapsed)
复制代码


在正式进行编码之后,发现存在比较大的问题,编码问题,出现如下错误:


encoding error : input conversion failed due to input error, bytes 0x81 0xE3 0xD3 0xAAencoding error : input conversion failed due to input error, bytes 0x81 0xE3 0xD3 0xAAencoding error : input conversion failed due to input error, bytes 0x81 0xE3 0xD3 0xAAI/O error : encoder error
复制代码


该错误在执行单线程时并未发生,但是当执行多线程时,异常开始出现,本问题在互联网上无解决方案,只能自行通过 requests-html 库的源码进行修改。


打开 requests_html.py 文件,将 417 行左右的代码进行如下修改:


def __init__(self, *, session: Union['HTMLSession', 'AsyncHTMLSession'] = None, url: str = DEFAULT_URL, html: _HTML, default_encoding: str = DEFAULT_ENCODING, async_: bool = False) -> None:  # 修改本部分代码    # Convert incoming unicode HTML into bytes.    # if isinstance(html, str):    html = html.decode(DEFAULT_ENCODING,'replace')
super(HTML, self).__init__( # Convert unicode HTML to bytes. element=PyQuery(html)('html') or PyQuery(f'<html>{html}</html>')('html'), html=html, url=url, default_encoding=default_encoding )
复制代码


代码 if isinstance(html, str): 用于判断 html 是否为 str,但是在实测过程中发现 html<class 'bytes'> 类型,所以数据没有进行转码工作,故取消相关判断。


除此以外,通过输出 world.html.encoding 发现网页的编码不是 GB2312 ,而是 gb18030,所以通过下述代码进行了默认编码的设置。


requests_html.DEFAULT_ENCODING = "gb18030"
复制代码


按照如上内容进行修改之后,代码可以正常运行,数据能正确的采集到。


本案例还新增了代码运行时长的计算,具体如下:


# 获取开始时间start = time.perf_counter()# 执行代码的部分# 获取时间间隔elapsed = (time.perf_counter() - start)print("程序运行完毕,总耗时为:", elapsed)
复制代码


完整的代码运行效果如下所示:


收藏时间

代码仓库地址:https://codechina.csdn.net/hihell/python120,去给个关注或者 Star 吧。


数据没有采集完毕,想要的可以在评论区留言交流


发布于: 2 小时前阅读数: 5
用户头像

爬虫 100 例作者,蓝桥签约作者,博客专家 2021.02.06 加入

6 年产品经理+教学经验,3 年互联网项目管理经验; 互联网资深爱好者; 沉迷各种技术无法自拔,导致年龄被困在 25 岁; CSDN 爬虫 100 例作者。 个人公众号“梦想橡皮擦”。

评论

发布
暂无评论
requests-html库初识 + 无资料解BUG之 I/O error : encoder error,Python爬虫第30例