此前我爬取了zhihu的相关问答,为了更好地收集信息,原本的打算是爬取百度资讯的内容,但在对页面进行分析后发现,在百度资讯的搜索结果中,百家号的页面格式比较统一,便于爬取;而其他媒体网站的页面格式比较繁杂,并且在百度特定关键词搜索结果中,像腾讯新闻、新浪新闻等比较具有可信度,并且格式较为统一的页面,数量其实非常少,所以从百度资讯的媒体网站爬取数据是不太现实的。此时我发现,新浪具有新闻的检索功能,来源较为广泛,并且新浪作为转载网站,为页面提供了较为统一清晰的格式。所以最终我决定爬取百度百家号+新浪新闻信息。
百家号:具有统一格式,便于爬取
媒体网站:格式复杂,来源多,不便爬取
新浪新闻:可检索,来源广,且格式统一
在爬取百度百家号时需要注意的事项及代码:
百度百家号搜索页面与百度网页页面不同,其中url链接是直接存在于页面中的,不需要再次请求。其中,百家号检索页面的请求url可以被表示为:
base_url = 'https://www.baidu.com/s?medium=2&tn=news&word={}&pn={}'
其中word后的关键字为我们需要的搜索关键字的url编码,pn后的值为(待检索页码-1)*10
可用以下方式补全url:
import urllib.parse
url = self.base_url.format(urllib.parse.quote(key), num)
记得在爬取zhihu时我们定义了专门的middleware来引入selenium,在爬百家号和新浪新闻时,我们不需要用到这部分,在设置对应爬虫的custom_setting时,注释掉即可:
custom_settings = {
'DEFAULT_REQUEST_HEADERS': header,
'DOWNLOAD_DELAY': 0.5,
'DOWNLOADER_MIDDLEWARES': {
# 'baiduCrawler.middlewares.BaiducrawlerDownloaderMiddleware': 543,
},
'MONGO_URI': '127.0.0.1',
'MONGO_DB': 'baijiahaoSpider',
'ITEM_PIPELINES': {
'baiduCrawler.pipelines.BaiducrawlerPipeline': 300,
},
# 'BREAKING_POINT': True
}
爬虫的总体结构还是很简单的,新建一个class BaijiahaoSpider(),对domain,数据库,setting,url初始化后,这里要提的是spider文件中导入的数据库只是为了过滤之前已经获得的数据,而item的存储是通过pipeline和item实现的。这个类包含五个函数:
- __init__():从命令行读取查询关键词与页码范围
- baijiahao_log():logger函数,在文件和控制台输出信息
- start_requests():对每个关键词和页码组成的url发起请求
- parse(response):对爬取到的百度页面进行解析,获得百家号的链接并进行请求;如果未爬取到百度的检索结果,则重新请求百度链接,对每个关键词已在数据库中存在的url进行过滤。
- parse_baijiahao(response):对百家号文章页面进行解析
下面贴上spider的具体代码:
#baijiahao.py
# -*- coding: utf-8 -*-
import scrapy
import urllib.parse
import requests
import re
import datetime
from ..items import baijiahaoItem
import pymongo
import hashlib
import os
import pickle
import logging
class BaijiahaoSpider(scrapy.Spider):
name = 'baijiahao'
allowed_domains = ['baidu.com']
# start_urls = ['http://baidu.com/']
header = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6',
'Accept-Encoding': 'gzip, deflate, br',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
}
custom_settings = {
'DEFAULT_REQUEST_HEADERS': header,
'DOWNLOAD_DELAY': 0.5,
'DOWNLOADER_MIDDLEWARES': {
# 'baiduCrawler.middlewares.BaiducrawlerDownloaderMiddleware': 543,
},
'MONGO_URI': '127.0.0.1',
'MONGO_DB': 'baijiahaoSpider',
'ITEM_PIPELINES': {
'baiduCrawler.pipelines.BaiducrawlerPipeline': 300,
},
# 'BREAKING_POINT': True
}
# 连接数据库,对比数据
mongo_uri = custom_settings['MONGO_URI']
mongo_db = custom_settings['MONGO_DB']
client = pymongo.MongoClient(host=mongo_uri, port=27017)
db = client[mongo_db]
# page = 2
# keys = ['电网', '停电']
base_url = 'https://www.baidu.com/s?medium=2&tn=news&word={}&pn={}'
# 更新的新闻数量
new_news = 0
def __init__(self, keywords=None, pages=1, *args, **kwargs):
super(BaijiahaoSpider, self).__init__(*args, **kwargs)
self.keys=keywords.split(',')
self.page = int(pages)
def baijiahao_log(self):
'''
logger函数,在文件和控制台输出信息
:return:
'''
# 创建logger,如果参数为空则返回root logger
logger = logging.getLogger("baijiahaoLogger")
logger.setLevel(logging.DEBUG) # 设置logger日志等级
# 这里进行判断,如果logger.handlers列表为空,则添加,否则,直接去写日志
if not logger.handlers:
# 创建handler
fh = logging.FileHandler("baijiahaolog.log", encoding="utf-8")
ch = logging.StreamHandler()
# 设置输出日志格式
formatter = logging.Formatter(
fmt="%(asctime)s %(name)s %(filename)s %(message)s",
datefmt="%Y/%m/%d %X"
)
# 为handler指定输出格式
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 为logger添加的日志处理器
logger.addHandler(fh)
logger.addHandler(ch)
return logger # 直接返回logger
def start_requests(self):
logger = self.baijiahao_log()
logger.info(
'---------------------------------baijiahao Spider started {}----------------------------------------'.format(
datetime.date.today().strftime('%Y-%m-%d')
)
)
# start_urls= []
for key in self.keys:
# self.fangwenDict[key] = []
for i in range(self.page):
num = i * 10
# start_url = self.search_url.format(urllib.parse.quote(i), 0, 20, 0)
# self.header['referer']='https://www.zhihu.com/search?q={}&type=content&range=3m'.format(urllib.parse.quote(i))
# self.header['referer'] = 'https://www.zhihu.com'
url = self.base_url.format(urllib.parse.quote(key), num)
# print(url)
yield scrapy.Request(
url=url,
callback=self.parse,
dont_filter=True,
meta={"keyword": key, 're_time': 1},
)
# print('start finish')
# break
# break
def parse(self, response):
'''
对爬取到的百度页面进行解析,获得百家号的链接
如果未爬取到百度的检索结果,则重新请求百度链接
:param response:
:return:
'''
logger = self.baijiahao_log()
key = response.meta.get('keyword')
re_time = response.meta.get('re_time')
logger.info("【{}】【start url】:{}".format(key, response.url))
hrefs = response.xpath(".//div[@id='content_left']/div/div[@class='result']/h3/a/@href").extract()
if len(hrefs) < 5 and re_time == 1:
#re_time表示只重复请求一次
yield scrapy.Request(response.url, dont_filter=True,
callback=self.parse, meta={"keyword": key, 're_time': 2}, )
colItem = baijiahaoItem()
b_collection = self.db[colItem.collection]
for hre in hrefs:
logger.info("【{}】【sub url】:{}, 【start url】:{}".format(key, hre, response.url))
# hre = self.convert_url(hre)
pattern = 'https://baijiahao.baidu.com/'
if b_collection.count_documents({'url': hre, 'keyword': key}) == 0 and re.match(pattern, hre) != None:
self.new_news = self.new_news + 1
yield scrapy.Request(hre, dont_filter=True,
meta={"keyword": key},
callback=self.parse_baijiahao) ##不同关键词可能有同一问题
# break
logger.info('【keyword】:{}, 【total new news】:{}'.format(key, self.new_news))
# print('parse finish')
def parse_baijiahao(self, response):
logger = self.baijiahao_log()
html_str = response
b_item = baijiahaoItem()
keyword = response.meta.get('keyword')
b_item['url'] = response.url
b_item['keyword'] = keyword
b_item['article_title'] = html_str.xpath('.//div[@class="article-title"]/h2/text()').extract_first()
b_item['author_name'] = html_str.xpath('.//p[@class="author-name"]/text()').extract_first()
dt = html_str.xpath('.//span[@class="date"]/text()').extract_first()
try:
if re.search('([0-9]+-[0-9]+-[0-9]+)', dt):
dt = '20' + re.search('([0-9]+-[0-9]+-[0-9]+)', dt).group(1)
elif re.search('([0-9]+-[0-9]+)', dt):
dt = str(datetime.datetime.now().year) + '-' + re.search('([0-9]+-[0-9]+)', dt).group(1)
else:
dt = 'unk'
except:
dt = 'unk'
b_item['publish_time'] = dt
b_item['account_authentication'] = html_str.xpath(
'.//span[@class="account-authentication"]/text()').extract_first()
b_item['article_text'] = ''.join(html_str.xpath('.//span[@class="bjh-p"]/text()').extract()).strip()
logger.info('【keyword】: {}, 【add news】:{}'.format(keyword, b_item['article_title']))
yield b_item
这部分的item定义如下:
#items.py
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import scrapy
from scrapy import Field
class baijiahaoItem(scrapy.Item):
'''
百家号item
'''
collection = 'baijiahao'
keyword = Field()
url = Field()
article_title = Field()
author_name = Field()
publish_time = Field()
account_authentication = Field()
article_text = Field()
pipeline中连接数据库的操作如下:
#pipelines.py
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
import pymongo
class BaiducrawlerPipeline(object):
'''
连接mongodb数据库
'''
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(mongo_uri=crawler.settings.get('MONGO_URI'), mongo_db=crawler.settings.get('MONGO_DB'))
def open_spider(self, spider):
self.client = pymongo.MongoClient(host=self.mongo_uri, port=27017)
self.db = self.client[self.mongo_db]
def process_item(self, item, spider):
self.db[item.collection].insert(dict(item))
return item
def close_spider(self, spider):
self.client.close()
通过以下命令运行爬虫:
#此前通过命令行运行单个爬虫,并传入参数时的命令
scrapy crawl 爬虫名称 -a keywords=电网,停电 -a pages=2
在爬取新浪新闻需要注意的事项及代码:
在新浪新闻的爬取中,过完全可以模仿上面的步骤,只有几处小小的改动:
- 搜索页面url:
base_url = 'https://search.sina.com.cn/?q={}&c=news&from=channel&col=&range=all&source=&country=&size=10&stime=&etime=&time=&dpc=0&a=&ps=0&pf=0&page={}'
补全方式:
#注意,此处num直接代表页码,不需要乘10 url = self.base_url.format(urllib.parse.quote(key), num)
- 我发现新浪新闻的搜索结果会出现一篇文章在几个页面内重复出现的情况,有时url是相同的,完全是同个页面,也有时url不同,但文章完全一致,这是由于新浪内部有许多不同的地方网站和主题网站,它们常常转载同一篇文章。因此,在爬取新浪新闻时,我们需要进一步去重。在爬取百家号页面时,我已经添加了数据库内容去重的代码,但在爬取新浪新闻时还可能存在一种现象:第一个url下的页面还未完全解析完,因此还未进入数据库,第二个完全相同的url页面也已经开始解析,最终导致两条信息重复入库。问题的解决方法有两条:一、为每个关键词设置一个已访问url的列表,这样在开始请求该url之前就将url添加到列表中,每个url与列表中元素对比,判断是否有重复,而不必等到该url对应的item入库后才可过滤,这样就解决了在一次爬取过程中爬取重复链接的问题;二、在解析每个新闻页面获得item的时候,对新闻的title进行数据库去重,这样可以有效过滤不同地方网站对同一篇文章进行搬运,导致url不同而误入库的情况。
除去以上两点,新浪新闻的爬取与百度百家号的爬取是完全类似的,再修改下xpath或其他页面元素的定位,代码是完全可以通用的,这里就不再贴出啦~到本篇为止,本次爬取特定主题新闻的任务就基本完成了,接下来就是收集更多信息,然后对数据进行清洗、处理与利用啦!希望今后还可以继续记录本项目的推进过程!