同学跑去实习了...然后工作的时候要她用python写一个爬虫,爬取一万个可以用的用户昵称。(为什么他们都能找到工作啊QAQ)
然后,她找到了我...然后在我动笔的时候,发现之前写过的爬虫基本上忘完了...无奈下只好对着以前写的项目,重新找了下文章,现在写一篇文章重新集合下之前零散的知识点。
我这里写的内容只是针对自己的需求写的,如果想要彻底了解BeautifulSoup
的用法的话,可以参考下这篇文章: () => 文章 (看完这文章我都想敲一下Java8新增的lambda表达式了...真的超级炫酷的)
跳过基础内容讲解的话,点我
言归正传,开始吧。
python的爬虫,可能会用到两个包(过去是这样的),一个是BeautifulSoup
,一个是etree
。
我用我的理解简单说说这两个包吧(这里的话十有八九别信...)
BeautifulSoup:
- 一个抓取页面的插件,能够页面抓取出来,能对页面数据做简单的筛选
- 引入方法:
from bs4 import BeautifulSoup
- 安装方法:
pip install bs4
etree:
- 在3.7(应该是3.7)版本以前,都是特别好用的,因为里面可以用XPath直接锁定DOM元素,但是在未来的更新中,它对XPath的兼容性并不好,所以干脆砍掉了。注:在谷歌浏览器里可以直接复制出页面的XPath,所以个人感觉没有必要去记XPath语法,毕竟我们可不是因为玩爬虫而玩python的,要知道,python可是因为人工智能而一鸣惊人。
- 引入方式:
from lxml import etree
(这个词应该是Element Tree) - 安装方式:
pip install lxml
- 据说在4.1.1版本里可以通过
from lxml.html import etree
使用etree - 想要使用的话,可以用pip指定版本安装,安装过去的版本使用
因为我目前不是很想在这爬虫上折腾,所以这次就用了BeautifulSoup写完了本次的爬虫,这里就不对etree的用法做介绍了,想了解的话根据自己安装的lxml
版本换一篇文章,就不浪费时间了
先来说说BeautifulSoup的四种数据类型,为了以后做铺垫:
- Tag
- NavigatableString
- BeautifulSoup
- Comment
Tag => 就是页面里的标签;
NavigableString => 如果你想获取Tag
中的文字,你可以用TagName.string
获取里面的内容,获取的值便是NavigableString
类型的对象;
BeautifulSoup => 它是个功能更多的对象,你可以使用更多的方式获取子类的对象,获取方式很简单,举个栗子:
soup = BeautifulSoup(你就想象这里有很长很长很长的html代码吧, 'lxml') # 获取页面文档 这是个BeautifulSoup类型的数据
# 如果愿意的话,可以用 print(type(soup)) 检查下 soup 的数据类型,我没有测试
item = soup.find_all(name='a', attrs={'class': 'hover', 'target': '_self'})
# 这里用的 find_all 方法,意思就是找到之前获取的文档对象(soup)下的所有满足<a class='hover' target='_self'></a>的对象
Comment => 这个属性比较特殊了,它可以找到所有的注释内容...没用过
对于BeautifulSoup
对象有很多属性可以用,比如说:
- 靠标签名查找 =>
soup.select('div')
- 靠选择器查找 =>
soup.select('.top-bar')
- 靠id查找 =>
soup.select('#app')
- 组合的 =>
soup.select('#app .top-bar')
- 子类的 =>
soup.select('#app > .top-bar')
- 具体 =>
soup.select('div[data="NickName" class="name"]')
# 这里可以了解下Emmet语法,挺像的...方括号里的是属性,大括号里的是内容,中间不能有空格。
参考的文章 => 现在想想...我当初为什么没有用select来写...
基础东西讲完了...后面的感觉完全不用看了,如果你闲着没事干的话
正式开始上代码
靶机 (用靶机这个词是不是感觉特别装逼啊)
http://www.oicq88.com
思路
获取到页面元素
检查下文档页面后,发现它把昵称分成了很多类别,一共五十多个,点进去之后,会进入子域名,使用字符串拼接直接访问内部链接即可
每一个类别里有很多页数据,我们需要优先知道页面总数,才可以去遍历,至少不会出现角标越界的状况,无意间发现,在页面后面拼接的数字超出是不会报错的,并且能看到的页面是该网站的最后一页(我在说什么啊) | 其实还有个思路,爬完一页之后判断是否还有下一页,如果有,则继续向后遍历,没有则退出循环。
利用
BeautifulSoup
获取到所有的昵称内容(为什么不能前后台分离用,前台向后台请求json数据,这样我就能直接拿到所有的昵称了...也不知道这个是伪静态还是静态)IO流,文件的写入
大纲
拿到拿到页面元素
拿页面元素,很简单的
response = requests.get(url='http://www.oicq88.com')
不过我定义了个函数的说...
import requests # 别忘了导包,报错的话装包 pip install requests,pip版本过低的话更新下,会有提示的
# 模拟用户从浏览器登录
def get_html(url):
headers = {
'User-Agent': 'Mozilla/5.0(Macintosh; Intel Mac OS X 10_11_4)\
AppleWebKit/537.36(KHTML, like Gecko) Chrome/52 .0.2743. 116 Safari/537.36'
} # 模拟浏览器访问
response = requests.get(url, headers=headers) # 请求访问网站
html = response.content.decode() # 获取网页源码 因为编码问题,我们看不懂机器的语言,所以需要先解码
return html
然后为了方便,我在函数下面加了点全局使用的属性
base_url = 'http://www.oicq88.com' # 基本域名
file_name = 'result.txt' # 输出的文件名
currentItem = 0 # 当前的项目数,只是为了记录反馈用
params = []
拼接得到所有昵称类型
这里说下find和find_all的区别,find指找到满足条件的第一条,而find_all是找到满足条件的所有条目。
举个栗子:
params = [ "<a class='A'>item1</a>", "<a class='A'>item2</a>", "<a>item3</a>" ]
都使用(name='a', attrs={'class': 'A'})查找的情况下
find相当于找到 "<a class='A'>item1</a>"
find_all相当于找到 ["<a class='A'>item1</a>", "<a class='A'>item2</a>"]
然后,上代码
# 找到所有的子项目
for name in soup.find_all(name='a'): # 拿到所有 a 标签
child_path = name.get('href') # 拿到 href 属性里的内容
method = re.compile(r'/(.*?)/') # 定义正则方法,将不需要的地址筛选掉
flag = re.findall(method, child_path) # 正则删选之后的结果
if len(flag) == 1: # 规定长度为了防止角标越界的状况发生
if flag[0]:
params.append(child_path)
分析状况
目前而言,知道的有几个信息
- 昵称有五十多种类型
- 每一种昵称都有多个页面
- 每一页都有n条数据
从这里开始逐个击破,我们需要先遍历出每一种类型的每一页里的每一条数据
这里我们先从类型入手
拿到每种类型的页数
其实这里我完全可以少写个循环的...咳
# 昵称类型
for i in range(len(params)):
index = 411 # 这个数字还是必要的 你可以看看 http://www.oicq88.com/shanggan/97.htm 和 http://www.oicq88.com/shanggan/411.htm 之间是否有任何区别
soup = BeautifulSoup(get_html(base_url + params[i] + '%s.htm' % index), 'lxml') # 用一个巨大的数字,拿到一共有多少个页面
item = soup.find_all(name='a', attrs={'class': 'hover', 'target': '_self'}) # 这个就是用来拿到最后一页的内容的方法
index = int(item[0].text) # 将最后一页的内容强转为int类型的数据,方便遍历每一页
# 后面这两个就是给个反馈,用户体验更加爽快
print('Current module is => ' + params[i] + 'And ...')
print('\nThe page count is => ' + str(index) + '\n')
遍历每一页,并将数据写入文件中
这里可以说下,soup.find之后的值被for in
遍历出来的,就不是BeautifulSoup
对象了,所以不能用同样的方式去查看子类内容了
不过子类内容可以直接再次被遍历(这个坑卡的时间有点久,而且写博客的时候我发现这个问题能有n种方式解决...)
# 分页!
for page in range(1, index):
print('\ncurrent page => ' + str(page) + '\n')
# 昵称项目 / 页
soup = BeautifulSoup(get_html(base_url + params[i] + str(page) + '.htm'), 'lxml')
try:
for ul in soup.find(name='ul', attrs={'class': 'list'}):
for ls in ul:
for p in ls:
# 写入文件
try:
with codecs.open(file_name, "a", 'utf-8') as f: # 这个用的是 a 方法,意味着直接在文件后面补充,不删除之前内容(重写),如果用w的话,可能会让字符串超出范围,从而抛出异常
f.write(p)
f.write('\n')
currentItem += 1
print('The ' + str(currentItem) + ' => Done')
except IOError:
print(IOError)
finally:
f.close()
except ValueError:
print('The null item not can be iterable...')
到此,差不多就这样了,简单记录下,代码直接copy就可用,前提是环境装齐
对于python,还是要经常抛出下错误,免得各种问题阻断了进程...不然爬一半炸了,时间都白费了
- 最好让爬取的内容更可控
- 有一套日志系统记录抛出的错误(现在想想把except里的内容改成文件流操作就行了)
- 好像也没什么了