Python 利用 BeautifulSoup 爬取网站获取新闻流

0. 引言

  介绍下 Python 用 Beautiful Soup 周期性爬取 xxx 网站获取新闻流;

 

图 1 项目介绍

 

1. 开发环境

  Python:      3.6.3

  BeautifulSoup:    4.2.0 , 是一个可以从HTML或XML文件中提取数据的Python库*

  ( BeautifulSoup 的中文官方文档:https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/ )

 

2. 介绍

  首先需要知道什么是 HTML ( Hypertext Markup Language,超文本标记语言 ) :

  HTML 是用来描述网页的一种语言*:

    •   HTML 指的是超文本标记语言 (Hyper Text Markup Language)
    •   HTML 不是一种编程语言,而是一种标记语言 (markup language)
    •   标记语言是一套标记标签 (markup tag)
    •   HTML 使用标记标签来描述网页

  

  代码实现主要分为三个模块:

  1. 计时 / second cnt

    因为是周期性爬取,所以需要计时器来控制;

  2. 设置代理 / set proxy

    为了应对网站的反爬虫机制,需要切换代理;

  3. 爬虫 / web spider

    利用 requests 请求网站内容,然后 beautifulsoup 提取出所需信息;

 

  利用 requests.get() 向服务器发送请求 >>> 拿到 html 内容 >>> 交由 beautifulsoup 用来解析提取数据;

 

  先看一个 requests 和 beautifulsoup 爬虫的简单例子:

  requests :

  >>> requests.get(html, headers=headers, proxies=proxies)
  >>> # headers 和 proxies 是可选项 / optional

  发送请求给网站,然后拿到返回的 HTML 内容,转换为 beautifulsoup 的对象,bs负责去提取数据;

 1 from bs4 import BeautifulSoup
 2 import requests
 3 html = "https://www.xxx.com"
 4 headers = {
 5     'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36'
 6 }
 7 proxies = "114.231.xx.xx:xxxx"
 8 
 9 resp = requests.get(html, headers=headers, proxies=proxies)
10 resp.encoding = 'utf-8'
11   
12 bsObj = BeautifulSoup(resp.content, "lxml")

 

本文以爬取  https://www.ithome.com/blog/  为例,把页面上的新闻爬取下来,过滤掉多余信息,拿到 新闻时间 & 新闻标题  

图 2 网站界面

 

拿到网站的 html 代码之后,如何剥离出自己想要的数据,参考图 3 的流程:

 

  图 3 从 html 中 剥离数据的流程

 

首先看看 bsObj = BeautifulSoup(resp.content, "lxml") 拿到的 目标网站 ( https://www.ithome.com/blog/ ) 的 HTML 代码,这个HTML代码也就是浏览器打开网页然后F12得到的当前网页的代码;

HTML代码描述了网页的排版布局文本图片等内容,浏览器就是提供渲染HTML代码的功能;

拿到的 xxxx.com 的网页HTML代码:

...
<
div class="block"> <h2> <a href="https://www.ithome.com/html/android/380353.htm" target="_blank">魅族黄章评价滑盖智能手机:穷途末路倒退的设计,中看不中用</a><span class="state tody">今日 14:53</span></h2> <div class="memo"> <p>昨晚荣耀Magic2、小米MIX 3手机相继亮相,并且都采用了升降式或者滑盖式屏幕设计。刚刚魅族黄章对于滑盖设计表达了自己的看法:滑盖可以说是穷途末路倒退的设计。</p> </div> </div> </li> <li> <a class="list_thumbnail" href="https://www.ithome.com/html/discovery/380352.htm" target="_blank"> <img src="//img.ithome.com/newsuploadfiles/thumbnail/2018/8/380352_240.jpg"/> </a> <div class="block"> <h2> <a href="https://www.ithome.com/html/discovery/380352.htm" target="_blank">俄罗斯计划“世界级”研究中心,拟克隆猛犸象</a><span class="state tody">今日 14:48</span></h2> <div class="memo"> <p>俄罗斯科学家计划用保存的冰河时代遗骸的DNA,在一个耗资450万英镑的新侏罗纪公园中心克隆猛犸象</p> </div> </div> </li> <li> <a class="list_thumbnail" href="https://lapin.ithome.com/html/digi/380351.htm" target="_blank"> <img src="//img.ithome.com/newsuploadfiles/thumbnail/2018/8/380351_240.jpg"/> </a> <div class="block"> <h2>
...

 

然后我们就需要在这 HTML 代码中借助 find() 函数 提取出我们所需要的数据;

关于 find() 函数的参数:

# Author:   coneypo

# 返回查找到的内容
find(tag=None, attrs={}, recursive=True, text=None, **kwargs)

# 返回查找到的内容列表
findAll(tag=None, attrs={}, recursive=True, text=None, limit=None, **kwargs)

# tag           标签参数        tag='p'
# attrs         属性参数        {'href':"www.xxx.com"}
# recursive     递归参数        True: 递归向下查询/ False: 只查询一级标签
# text          文本参数        text="hello world"
# limit         范围限制参数    保留查询结果的前n项 / findall(limit=1) 等于 find()
# kwargs        关键词参数      id="header1"

 

关于 find() 函数的使用:

# 1. find(tagname)        # 查找名为 <p> 的标签
find('p')

# 2. find(list)           # 查找列表中的所有标签,<p>,<body>
find(['p', 'body'])

# 3. find(dict)           # 查找字典中标签名和值匹配的标签 <a href="www.xx.com" target="_blank">
find({'target': "_blank", 'href': "wwww.xx.com"})

# 4. find('id'='xxx')     # 寻找id属性为xxx的<h1 id="myHeader">Hello World!</h1>
find('id'='myHeader')

 

图 4 find() 函数的匹配

 

接下来就是利用 find() 一层层筛选 HTML 代码;

首先定位到 <div class="block" >;

>>> block = bsObj.find_all("div", {"class": "block"})
<div class="block">
  <h2>
    <a href="https://www.ithome.com/html/android/380353.htm" target="_blank"> 魅族黄章评价滑盖智能手机:穷途末路倒退的设计,中看不中用</a>
    <
span class="state tody">今日 14:53</span>
  </
h2> <div class="memo">   <p>昨晚荣耀Magic2、小米MIX 3手机相继亮相,并且都采用了升降式或者滑盖式屏幕设计。刚刚魅族黄章对于滑盖设计表达了自己的看法:滑盖可以说是穷途末路倒退的设计。</p> </div>

 

接下来就是要提取出 新闻标题 title 和 新闻时间 time;

注意 “block” 是用 find_all() 拿到的,返回的是一个 list 列表,遍历这个列表,去找 title & time;

对于单个的 "block",比如 block[i]:

提取新闻时间:

>>> block[i].find('span', {'class': "state tody"})
<span class="state tody">今日 14:53</span>

然后

>> block[i].find('span', {'class': "state tody"}).get_text()

得到 time 内容:

今日 14:53

 
提取新闻标题:

>>> block[i].find('a', {'target': "_blank"})
<a href="https://www.ithome.com/html/android/380353.htm" target="_blank">魅族黄章评价滑盖智能手机:穷途末路倒退的设计,中看不中用</a>

然后

>> block[i].find('a', {'target': "_blank"}).get_text()

得到 title 内容:

魅族黄章评价滑盖智能手机:穷途末路倒退的设计,中看不中用

 

这样就可以拿到了 新闻标题 titile 和 新闻时间 time了; 

 

3. 代码实现

1. 计时器

这里是一个秒数计数功能模块,利用 datetime 这个库,获取当前时间的秒位,和开始时间的秒位进行比对,如果发生变化,sec_cnt 就 +=1;

sec_cnt 输出 1,2,3,4,5,6...是记录的秒数,可以由此控制抓包时间;

>>> time = datetime.datetime.now()   # 获取当前的时间,比如:2018-08-31 14:32:54.440831
>>> time.second()             # 获取时间的秒位

second_cnt.py:

 1 # Author:   coneypo
 2 # Created:  08.31
 3 
 4 import datetime
 5 
 6 # 开始时间
 7 start_time = datetime.datetime.now()
 8 tmp = 0
 9 # 记录的秒数
10 sec_cnt = 0
11 
12 while 1:
13     current_time = datetime.datetime.now()
14 
15     # second 是以60为周期
16     # 将开始时间的秒 second / 当前时间的秒 second 进行对比;
17     # 如果发生变化则 sec_cnt+=1;
18     if current_time.second >= start_time.second:
19         if tmp != current_time.second - start_time.second:
20             # print("<no 60>+  ", tmp)
21             sec_cnt += 1
22             print("Time_cnt:", sec_cnt)
23             # 以 10s 为周期
24             if sec_cnt % 10 == 0:
25                 # do something
26                 pass
27         tmp = current_time.second - start_time.second
28 
29     # when get 60
30     else:
31         if tmp != current_time.second + 60 - start_time.second:
32             sec_cnt += 1
33             print("Time_cnt:", sec_cnt)
34 
35             # 比如以 10s 为周期
36             if sec_cnt % 10 == 0:
37                 # do something
38                 pass
39         tmp = current_time.second + 60 - start_time.second

 

2. set_proxy / 设置代理

这个网站可以提供一些国内的代理地址: http://www.xicidaili.com/nn/ ;

因为现在网站可能会有反爬虫机制,如果同一个 IP 如果短时间内大量访问该站点,就会被拦截;

所以我们需要设置代理,爬这个代理网站页面,拿到的 proxy 地址传,给 requests.get 的 proxies 参数:

>>> requests.get(html, headers=headers, proxies=proxies)

两个函数:

  get_proxy():      从代理网站爬取代理地址,存入本地的"proxies.csv"中;

  get_random_proxy():  从本地的"proxies.csv"中,随机选取一个代理proxy;

 

注意,xicidali.com也有反爬虫机制,亲身体验短时间大量爬数据ip会被block,而且我们一次爬取可以拿100个代理地址,所以只需要爬一次代理网站就行了,存到本地的csv中;

要代理从本地的csv中随机获取proxy地址;

csv可以定期的用 “get_proxy()” 进行更新;

 

get_proxy.py :

 1 # Author:   coneypo
 2 # Created:  08.31
 3 # Updated:  09.01
 4 # set proxy
 5 # save to csv
 6 
 7 from bs4 import BeautifulSoup
 8 import requests
 9 import csv
10 import pandas as pd
11 import random
12 
13 path_proxy_csv = "proxies.csv"
14 
15 
16 # 从 xicidaili.com 爬取代理地址,存入本地csv中
17 def get_proxy():
18     headers = {
19         'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36'
20     }
21     proxy_url = 'http://www.xicidaili.com/nn/'
22     resp = requests.get(proxy_url, headers=headers)
23     soup = BeautifulSoup(resp.text, 'lxml')
24     ips = soup.find_all('tr')
25     proxy_list = []
26     for i in range(1, len(ips)):
27         ip_info = ips[i]
28         tds = ip_info.find_all('td')
29         proxy_list.append(tds[1].text + ':' + tds[2].text)
30 
31     if len(proxy_list) == 0:
32         print("可能被屏蔽了")
33     else:
34         print("拿到的代理个数:", len(proxy_list))
35         with open(path_proxy_csv, 'w', newline="") as csvfile:
36             writer = csv.writer(csvfile)
37             for i in range(len(proxy_list)):
38                 print(proxy_list[i])
39                 writer.writerow([proxy_list[i]])
40 
41 
42 # get_proxy()
43 
44 
45 # 从代理的 csv 中随机获取一个代理
46 def get_random_proxy():
47     column_names = ["proxy"]
48     proxy_csv = pd.read_csv("proxies.csv", names=column_names)
49 
50     # 新建一个列表用来存储代理地址
51     proxy_arr = []
52 
53     for i in range(len(proxy_csv["proxy"])):
54         proxy_arr.append(proxy_csv["proxy"][i])
55 
56     # 随机选择一个代理
57     random_proxy = proxy_arr[random.randint(0, len(proxy_arr) - 1)]
58 
59     print(random_proxy)
60     return random_proxy
61 
62 # get_random_proxy()

 

 

 3. get_news_from_xxx / 爬虫

由计时器的 sec_cnt 控制爬虫的周期  >> 每次先去 “proxies.csv” 拿代理 >> 然后交给 requests 去拿数据 >> 拿到 HTML 之后交给 beautiful 对象 >> 然后过滤出新闻流:

伪代码如下:

1 while 1:
2      if 周期到了:
3         获取代理地址;
4         requests 给目标网站发送请求;
5         获取到的HTML内容交给BeautifulSoup:
6             过滤出新闻标题和时间;        

 

 get_news_from_xxx.py 完整的代码

  1 # Author:   coneypo
  2 # Created:  08.31
  3 # Updated:  09.01
  4 # web spider for xxx.com
  5 
  6 from bs4 import BeautifulSoup
  7 import requests
  8 import random
  9 import datetime
 10 import csv
 11 import pandas as pd
 12 
 13 
 14 headers = {
 15     'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36'
 16 }
 17 
 18 
 19 # 从 xicidaili.com 爬取代理地址,存入本地csv中
 20 def get_proxy():
 21     headers = {
 22         'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36'
 23     }
 24     proxy_url = 'http://www.xicidaili.com/nn/'
 25     resp = requests.get(proxy_url, headers=headers)
 26     soup = BeautifulSoup(resp.text, 'lxml')
 27     ips = soup.find_all('tr')
 28     proxy_list = []
 29     for i in range(1, len(ips)):
 30         ip_info = ips[i]
 31         tds = ip_info.find_all('td')
 32         proxy_list.append(tds[1].text + ':' + tds[2].text)
 33 
 34     if len(proxy_list) == 0:
 35         print("可能被代理网站屏蔽了", '\n')
 36     else:
 37         print("拿到的代理个数:", len(proxy_list), '\n')
 38         with open("proxies.csv", 'w', newline="") as csvfile:
 39             writer = csv.writer(csvfile)
 40             for i in range(len(proxy_list)):
 41                 # print(proxy_list[i])
 42                 writer.writerow([proxy_list[i]])
 43 
 44 
 45 # 执行一次就可以了;
 46 # 执行一次可以更新本地代理地址;
 47 get_proxy()
 48 
 49 
 50 # 从存放代理的 csv 中随机获取一个代理
 51 def get_random_proxy():
 52     column_names = ["proxy"]
 53     proxy_csv = pd.read_csv("proxies.csv", names=column_names)
 54 
 55     # 新建一个列表用来存储代理地址
 56     proxy_arr = []
 57 
 58     for i in range(len(proxy_csv["proxy"])):
 59         proxy_arr.append(proxy_csv["proxy"][i])
 60 
 61     # 随机选择一个代理
 62     random_proxy = proxy_arr[random.randint(0, len(proxy_arr)-1)]
 63 
 64     print("当前代理:", random_proxy)
 65     return random_proxy
 66 
 67 
 68 # 爬取网站
 69 def get_news():
 70     html = "https://www.ithome.com/blog/"
 71     resp = requests.get(html, headers=headers, proxies=get_random_proxy())
 72     resp.encoding = 'utf-8'
 73 
 74     # 网页内容
 75     bsObj = BeautifulSoup(resp.content, "lxml")
 76     # print(bsObj)
 77 
 78     # 分析
 79     block = bsObj.find_all("div", {"class": "block"})
 80     # print(block)
 81 
 82     current_titles = []
 83     current_times = []
 84 
 85     # analysis every block
 86     for i in range(len(block)):
 87         # get Time
 88         time_classes = ["state tody", "state other"]
 89         for time_class in time_classes:
 90             tmp = block[i].find_all('span', {'class': time_class})
 91             if tmp:
 92                 current_time = (block[i].find('span', {'class': time_class})).get_text()
 93                 current_times.append(current_time)
 94 
 95         # get Title
 96         if block[i].find_all('a', {'target': "_blank"}):
 97             current_title = (block[i].find('a', {'target': "_blank"})).get_text()
 98             current_titles.append(current_title)
 99 
100     for i in range(len(current_times)):
101         print(current_times[i], current_titles[i])
102     return current_times, current_titles
103 
104 
105 get_news()
106 
107 # 计时
108 start_time = datetime.datetime.now()
109 tmp = 0
110 sec_cnt = 0
111 
112 while 1:
113     current_time = datetime.datetime.now()
114 
115     # second 是以60为周期
116     # 将开始时间的秒second / 当前时间的秒second 进行对比
117     if current_time.second >= start_time.second:
118         if tmp != current_time.second - start_time.second:
119             # print("<no 60>+  ", tmp)
120             sec_cnt += 1
121             print("Time_cnt:", sec_cnt)
122             if sec_cnt % 10 == 0:
123                 get_news()
124                 print('\n')
125         tmp = current_time.second - start_time.second
126 
127     # when get 60
128     else:
129         if tmp != current_time.second + 60 - start_time.second:
130             sec_cnt += 1
131             print("Time_cnt:", sec_cnt)
132 
133             if sec_cnt % 10 == 0:
134                 get_news()
135                 print('\n')
136         tmp = current_time.second + 60 - start_time.second

 

最终的输出 log:

 1 拿到的代理个数: 100 
 2 
 3 当前代理: 111.155.124.73:8123
 4 今日 13:53 你应该跳过三星Galaxy Note 9等待S10的5个理由
 5 今日 13:49 索尼CEO回应联机争议:PS上最好玩,为何要跨平台?
 6 今日 13:43 顺丰冷运/买6送4,阳澄湖鲜活六月黄大闸蟹10只旗舰店58元
 7 今日 13:35 华为Mate 9、P10系列GPU Turbo升级已全面开放
 8 今日 13:35 自如熊林:没有意愿“对付”谁,是自如的责任定承担
 9 今日 13:33 《暗黑4》?暴雪尚有一个未公布的3A级动作游戏
10 今日 13:12 经典重生:Palm新Logo曝光
11 今日 13:07 安卓系统各版本最新份额:“奥利奥”接近15%,6.0、7.0占大头
12 今日 12:49 新研究称:世界低估了中国对全球科学的贡献
13 今日 12:48 韩春雨接受校方调查结果:论文存缺陷,研究过程不严谨
14 08月31日 中粮出品/宁夏中卫原产,悦活优选100枸杞蜂蜜454g×2瓶39.9元
15 今日 12:35 手机QQ iOS版v7.7.8正式版更新:新增多款高颜值滤镜
16 今日 12:23 网游总量调控,游戏暴利时代或终结
17 今日 12:12 直降30元,网易云音乐氧气耳机开学季大促99元
18 今日 12:08 美团点评IPO定价区间敲定:每股60-72港币
19 今日 12:05 索尼HX99、HX95卡片相机发布:大变焦、能拍4K视频
20 今日 11:30 自如回应“阿里员工租甲醛房去世”:需客观全面调查
21 今日 11:30 夏末清仓,森马企业店纯棉印花T恤19.9元包邮
22 今日 11:27 1198元!vivo Y81s正式开售:千元机也有高颜值
23 今日 11:24 猪的器官能移植给人类吗?科学家研究出了测试程序
24 今日 11:16 小米8/MIX 2S开始推送MIUI 10稳定版:全新系统UI
25 今日 11:11 2030年8亿人将被机器人取代,但这些职业竟还能保住饭碗!
26 08月31日 直降400元,汪峰FIIL随身星Pro降噪耳机799元新低开抢
27 今日 11:01 生存游戏《人渣》现纳粹纹身引批评,开发商移除并致歉
28 今日 10:56 雷柏外设开学季大促:机械键盘低至94元
29 今日 10:46 苹果AR开发的下一个动作:Maps地图应用
30 今日 10:46 屏幕边框已死,笔记本设计迎来新时代
31 今日 10:45 可口可乐CEO喝够了可乐,于是51亿美元买下COSTA咖啡
32 今日 10:41 放弃悟空问答,今日头条的第一次战略性撤退
33 今日 10:40 狂甩不掉,ZTM M8防水运动蓝牙耳机旗舰店29.9元(40元券)
34 今日 10:36 MagicScroll可变形平板电脑问世:采用卷轴式柔性屏
35 今日 10:34 等谷歌牌手表的可以散了,Pixel Watch今年不会有
36 今日 10:27 佳能全幅无反相机EOS R规格、照片全泄露了
37 今日 10:07 IFA2018:主打设计!微星发布P65 Creator笔记本电脑
38 今日 10:01 翻版“药神”:代购印度抗癌药后加价销售,12人被判刑
39 今日 9:56 359元, 西部数据My Passport 1T移动硬盘开学季大促新低
40 今日 9:43 媒体:ofo已拖欠云鸟、德邦等多家物流供应商亿元欠款
41 今日 9:40 巴菲特:苹果若收购特斯拉,我会支持
42 08月31日 双星旗舰店网面运动鞋69→39元、啄木鸟弹力牛仔裤109→59元
43 今日 9:31 NASA称国际空间站打补丁成功,气压保持稳定
44 今日 9:29 疑似华为Mate 20 Pro真机现身!三摄设计大变样
45 今日 9:25 一波曝光之后,谷歌Pixel 3/3 XL已现身FCC网站
46 今日 9:24 《赛博朋克2077》可独行出击可双人行动,还可以买房
47 今日 9:19 2899元,索尼 PlayStation 4 Pro 游戏主机秒杀新低
48 今日 9:17 河北科大:未发现韩春雨团队主观造假
49 今日 9:12 米粉平均月收入7485元?网友:算上雷军了吧
50 08月28日 3000mAh大容量,耐时旗舰店8节5号/7号锂铁电池19.9元
51 今日 8:56 网传顺丰3亿条数据在暗网出售,官方回应:非顺丰数据
52 今日 8:49 出版署严控网游总量,腾讯网易股价大跌
53 今日 8:46 为什么接你电话的客服总是解决不了问题?
54 Time_cnt: 1
55 Time_cnt: 2
56 Time_cnt: 3
57 Time_cnt: 4
58 Time_cnt: 5
59 Time_cnt: 6
60 Time_cnt: 7
61 Time_cnt: 8
62 ...

 

你就可以每隔 10s 刷新下该网站上面的新闻流;

刷新周期在 line 87:

>>> if sec_cnt % 10 == 0:

 

# 请尊重他人劳动成果,转载或者使用源码请注明出处:http://www.cnblogs.com/AdaminXie

# 项目源码上传到了我的 GitHub,如果对您有帮助,欢迎 Star 支持下:https://github.com/coneypo/Web_Spider

# 请不要利用爬虫从事恶意攻击或者违法活动

posted @ 2018-08-31 16:07 coneypo 阅读(...) 评论(...) 编辑 收藏