赞
踩
由于本人是初学者,有说的不对的地方敬请指正。
首先,要找一个教程,B站上有很多,不要看书或者看博客,因为视频能展示更多信息,很多会踩的坑是很难用文字一一表述出来的,那样会使文章显得臃肿,而在视频里可能就是半分钟的debug,而且视频能帮你快速建立对新知识的整体认识。举例来说,在学习的前期,我们组的每一个人都以为爬虫可以一套代码爬很多网站,在中期,其他组(前端、后端)的同学说数据库里新闻太少、博物馆信息太少,我们也得和他们解释,爬虫没有那么智能,对于不同的网站(通常页面结构不同)需要不同的爬虫代码,想爬一个网站就得先查看它的页面结构。你看,这么简单的道理,对于新手来说确实是一个认知上的障碍。下图中,一个个html标签组成了这个网页,不同的网页用的标签是不同的。
第二,现代网页很少有不用AJAX动态加载的,也就是说对一个url发请求不会得到想要的信息。拿新闻来说,我们选择对新华网上博物馆的新闻进行爬取,在新华网(或者人民网、腾讯新闻等)搜索栏输入“故宫博物院”会跳转到一个网页,这个网页就是动态加载出来的。很多文字教程并没有讲这个,而只是讲怎么爬一个静态的html页面。上图中,是没办法通过对该页面的网址作解析得到所选的<a></a>标签的。事实上,我之前一直认为遇到任何问题都应该到官网找答案,但是在beautifulsoup、scrapy官网耗了很久,虽然会看到一些dynamic load之类的词,但实际上并不清楚,同样的事视频里10分钟就讲完了,这不比看官方文档快?下图中,切换到network标签,选择XMR,刷新页面,就可以看到抓到一个包,在header中可以看到实际上我们要发请求的网址,以及需要哪些参数。
第三,对于动态请求的页面的处理,简单来说就是加载这个页面的时候,我们并不从这个页面的html获取数据,实际上是向某个地址(人民网用的是elasticsearch)发送了带有一些参数的请求,我们就是要获取这个地址,并根据需要更换参数。在这次作业中,博物馆的名字就是一个参数,搜索的博物馆的新闻一页放不下的时候,页码也是一个参数。页面结构、获取所谓的请求地址都是用浏览器的“开发者工具"的。要获取返回的数据,你可以在上图中切换到与header同级的response标签,你会看到一个JSON格式的字符串,格式化以后看得清楚一点(如下图)。这一条所说的你只要在那些教程里看上一两个视频就都会了。
第四,动态请求的页面上有我们所需的各条新闻的链接,当我们获取这些动态加载出来的链接之后,就可以按照解析静态网页的方式解析这些网址了。新华网、人民网都是分主站新闻、地方频道的,别的细分种类还包括视频新闻等,他们的结构是不同的。在本次任务中,我们选择了先只爬主站新闻,后面再加入了地方频道(我们只需要往数据库里存文本和图片地址,不考虑视频),事实上,新华网的地方频道也是由两种主要结构和其他一些乱七八糟的结构(这些就不爬了)组成的。这都是在爬取的过程中发现的,没有什么捷径。
第五,发送请求的是requests包,解析返回来的数据的是beautifulsoup包,本次任务并没有用到scrapy(一个框架,而不只是包)和selenium(最复杂也最强大)。主要就是用find,find_all,select等方法找标签,具体学习上beautifulsoup官网或别的文字教程即可(这个可以有)。
第六,考虑到网页动态返回的数据格式(一般是json),处理数据、保存文件、传数据库的统一性,最好都使用json格式存储数据。
第七,编码相关的问题遇到多次,这里充分体现了面向百度/谷歌编程的重要性。
with open(f"新华网_{keyword}.json", 'w', encoding="utf-8") as f:
json.dump(all_news, f, ensure_ascii=False)
response = requests.get(url, headers=headers)
之后要加response.encoding = response.apparent_encoding
,我试过不加,或指定为utf-8和gbk,都会乱码,只能用apparent_encoding,方法是网上找的,原理不清楚。# -*- coding:utf-8 -*-
。data = resp_obj["Data"].replace("true", "True")
data = ast.literal_eval(data)
p_text = p.text.replace("\u3000", "").replace('\n', "").replace('\r', "").replace('\t', "").replace('\xa0', "")
第八,文本情感倾向分析直接调用阿里云的服务,相关操作按文档来就行(别的也找不到什么资料)。百度、腾讯、华为等其他公司也有这种开放的AI能力提供,也可以选择。
第九,为避免数据库里出现重复记录,要实现增量更新。实现思路是:每次爬取前都会先扫描当前目录下的json文件(如果有news_inserted.json也要读入),读取它们的内容,获取标题(后面会用到,因为某个标题可能对应多个报道同一个新闻的不同页面,对这些新闻得去重,靠的是标题),统计新闻条数,然后写入news_inserted.json,将其他json移入当前目录下的子文件夹backup。每次爬完后都可能在当前目录下产生“新华网_XXX.json”文件,json2mysql.py会把这些文件中的数据导入数据库,而不会把news_inserted.json中的数据导入数据库。所以爬完就要紧跟着导入,不然数据就会被归档。
(1)爬虫
# -*- coding:utf-8 -*- # 加入情感倾向分析(v5) # 1、调用阿里云NLP服务实现情感倾向分析 # 2、解决编码问题,在首行加# -*- coding:utf-8 -*-。注意是有井号的,而且一定是全文的首行,即,包括文首的注释,都得在这句之后 # (1)短文本不用加也行,长文本就会报Non-UTF-8 code starting with的错误,原因我不知道 # 针对更新的需求进行调整(v4) # 1、实现地方频道的爬取 # 2、在结果json中加入title,省去v3中提取symbol的步骤,改为判断title,相应地,要改导入数据库的文件 # 3、测试说明 # (1)先用少一点的博物馆和少一点的curPage # (2)再加curPage # (3)再加别的博物馆 # 针对更新的需求进行调整(v3) # 1、每次运行这个脚本之前都要对当前目录下已有的json文件进行处理,以实现导入数据库的只有增量部分,同时保留历史版本 # 针对传数据库的要求进行调整(v2) # 1、每条新闻的图片最多1张(可以没有,但是不能多,因为表中目前只给一个位置 # 2、每个每条记录都要完整包含来源、博物馆名字,而不是上一个版本那样一个博物馆一个json # 3、使用navicat导入json的功能 import requests import json import csv import os import ast from bs4 import BeautifulSoup import shutil from aliyunsdkalinlp.request.v20200629 import GetSaChGeneralRequest from aliyunsdkcore.client import AcsClient from aliyunsdkcore.acs_exception.exceptions import ClientException from aliyunsdkcore.acs_exception.exceptions import ServerException def get_url_list(origin_url): url = origin_url # 用开发者工具查看ajax包时,在请求头里发现请求的Content-Type的格式和编码是application/json;charset=UTF-8。不加会有乱码。 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) \ Chrome/67.0.3396.99 Safari/537.36", 'Content-Type': 'application/json;charset=UTF-8' } # 存放详情页地址(静态,实际要爬的网页)的列表 url_list = [] try: # 这里的实际请求url中就带了参数,用的是get方法 # 有的网页实际请求url中不带参数,用的是post方法,这时要把参数以data参数给出(详见视频教程) json_urls = requests.get(url=url, headers=headers).json() # 观察返回的json数据,发现所要的url在如下位置 results = json_urls["content"]["results"] if results is None: return for result in results: url_list.append(result["url"]) print(f"正在获取{keyword}的第{curPage}页新闻。。。") return url_list except Exception as e: print(e) # 地方频道 # eg. http://www.js.xinhuanet.com/2021-02/07/c_1127073849.htm # http://www.zj.xinhuanet.com/2020-06/13/c_1126110863.htm def crawl_news_A(keyword, url, article, soup, titles): # 找标题 h1 = article.find_next("h1", id="title") if h1 is None: return # 把莫名奇妙的字符去掉 title = h1.text.replace("\n", "").replace('\r', "").replace('\u3000', ",").strip().replace(' ', ",") # 判断是不是重复,避免传进数据库很多相似度很高的新闻 if title in titles: print("重复的新闻!") return # 在实际爬取过程中发现,搜索结果有时候是模糊匹配,比如搜“上海博物馆”,返回的有“上海自然博物馆”的新闻。报错中提供url,以供确认 if keyword not in title: print(f"由于某些原因,{url} 似乎不是 {keyword} 相关的新闻 :(") return titles.append(title) # 获取日期(三种页面获取时间方式也不同) time = soup.find("span", class_="time").text.replace("\n", "").replace(" ", "").replace('\r', "") year = time.split('年')[0] month = time.split('年')[1].split('月')[0] day = time.split('年')[1].split('月')[1].split('日')[0] date = year + "." + month + "." + day # 在这个页面结构中可以获得来源(不一定是新华网),其他两个页面似乎不行,所以我都默认是新华网了 source = soup.find("em", id="source").text.replace("\n", "").replace(" ", "").replace('\r', "") content = [] img = [] ps = article.find("div", class_="article").find_all("p") for p in ps: p_text = p.text.replace("\u3000", "").replace('\n', "").replace('\r', "").replace('\t', "").replace('\xa0', "") # XXX摄影,图片来自新华在线。。。这种信息是无用的。如果是单独成段,就删掉,否则保留。 if "摄" in p_text or "图片来源" in p_text: if len(p_text) < 50 and p_text.find("记者", -15): continue content.append(p_text) # 如果有图片的话就获取图片链接。观察得知页面中给出的是图片的相对地址,要和页面地址中的一部分作拼接才是完整地址 url_page = '/'.join(url.split('/')[:-1]) p_imgs = p.find_all("img") for p_img in p_imgs: img.append(url_page + '/' + p_img.get("src")) content = ''.join(content) # 代码不完善,有时会爬到错误的东西,导致内容过长。而且我们数据库对新闻限制10000字 if len(content) > 9900: print(f"出错了!文章内容过长,请确认 {url}") return # 有时又会出现把“XXX摄影”之类的删掉以后新闻内容部分为空的情况,这种也不要 if len(content) == 0: print(f"文章长度为0? {url}") return # 阿里云NLP服务限制文本长度不超过1000 content_to_test = content if len(content) < 900 else content[:900] news = {} news["museum"] = keyword news["time"] = date news["type"] = get_sentiment(content_to_test) news["content"] = content news["photo"] = img[0] if (len(img) > 0) else '' news["source"] = source if (source is not None) else "新华网" news["title"] = title return news # 正文频道 # eg. http://www.xinhuanet.com/2020-09/02/c_1126445038.htm # http://www.xinhuanet.com/expo/2019-11/18/c_1210358665.htm def crawl_news_B(keyword, url, p_detail, soup, titles): h_title = soup.find("div", class_="h-title") if h_title is None: return title = h_title.text.replace("\n", "").replace('\r', "").replace('\u3000', ",").strip().replace(' ', ",") if title in titles: print("重复的新闻!") return if keyword not in title: print(f"由于某些原因,{url} 似乎不是 {keyword} 相关的新闻,其标题是《{title}》") return titles.append(title) time = soup.find("span", class_="h-time").text.replace("\n", "").replace(" ", "").replace('\r', "")[:10] date = time.split('-')[0] + "." + time.split('-')[1] + "." + time.split('-')[2] content = [] img = [] ps = p_detail.find_all("p") for p in ps: if p.find("a"): continue p_text = p.text.replace("\u3000", "").replace('\n', "").replace('\r', "").replace('\t', "").replace('\xa0', "") if "摄" in p_text or "图片来源" in p_text: if len(p_text) < 50 and p_text.find("记者", -15): continue content.append(p_text) url_page = '/'.join(url.split('/')[:-1]) p_imgs = p.find_all("img") for p_img in p_imgs: img.append(url_page + '/' + p_img.get("src")) content = ''.join(content) if len(content) > 9900: print(f"出错了!文章内容过长,请确认 {url}") return if len(content) == 0: print(f"文章长度为0? {url}") return content_to_test = content if len(content) < 900 else content[:900] news = {} news["museum"] = keyword news["time"] = date news["type"] = get_sentiment(content_to_test) news["content"] = content news["photo"] = img[0] if (len(img) > 0) else '' news["source"] = "新华网" news["title"] = title return news # 主站 # eg. http://m.xinhuanet.com/2021-04/30/c_1127399187.htm # http://m.xinhuanet.com/2021-04/30/c_1127398081.htm def crawl_news_main(keyword, url, head_line, soup, titles): h1 = head_line.find_next("h1") if h1 is None: print("h1 is None") return title = h1.find_next("span").text.replace("\n", "").replace('\r', "").replace('\u3000', ",").strip().replace(' ', ",") if title in titles: print("重复的新闻!") return if keyword not in title: print(f"由于某些原因,{url} 似乎不是 {keyword} 相关的新闻 :(") return titles.append(title) header_time = soup.find("div", class_="header-time") year = header_time.find("span", class_="year").find("em").text.replace("\n", "").replace(" ", "").replace('\u3000', "") day = header_time.find("span", class_="day").text.replace(" ", "").split('/') date = year + "." + day[0] + "." + day[1] content = [] img = [] ps = soup.find("div", id="detail").find_all("p") for p in ps: p_text = p.text.replace("\u3000", "").replace('\n', "").replace('\r', "").replace('\t', "").replace('\xa0', "") if "摄" in p_text or "图片来源" in p_text: if len(p_text) < 50 and p_text.find("记者", -15): continue content.append(p_text) url_page = '/'.join(url.split('/')[:-1]) p_imgs = p.find_all("img") for p_img in p_imgs: img.append(url_page + '/' + p_img.get("src")) content = ''.join(content) if len(content) > 9900: print(f"出错了!文章内容过长,请确认 {url}") return if len(content) == 0: print(f"文章长度为0? {url}") return content_to_test = content if len(content) < 900 else content[:900] news = {} news["museum"] = keyword news["time"] = date news["type"] = get_sentiment(content_to_test) news["content"] = content news["photo"] = img[0] if (len(img) > 0) else '' news["source"] = "新华网" news["title"] = title return news def crawl_news(url_list, keyword, titles): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) \ Chrome/67.0.3396.99 Safari/537.36" } all_news = {} all_news["data"] = [] for url in url_list: try: response = requests.get(url, headers=headers) response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, "lxml") head_line = soup.find("div", class_="head-line") if head_line is None: article = soup.find("div", id="article") p_detail = soup.find("div", id="p-detail") if article is not None: # 地方频道 news = crawl_news_A(keyword, url, article, soup, titles) if news is None: error = "地方频道爬取失败" elif p_detail is not None: # 正文频道 news = crawl_news_B(keyword, url, p_detail, soup, titles) if news is None: error = "正文频道爬取失败" else: news = None error = "不符合主站、正文、地方频道三种页面布局" else: # 主站 news = crawl_news_main(keyword, url, head_line, soup, titles) if news is None: error = "主站爬取失败" if news is not None: all_news["data"].append(news) else: print(f"爬取 {url} 对应的新闻出现错误:{error}") except Exception as e: print(e) # 写入这家博物馆在新华网的新闻到json,注意编码 if len(all_news["data"]) > 0: with open(f"新华网_{keyword}.json", 'w', encoding="utf-8") as f: json.dump(all_news, f, ensure_ascii=False) def update_check(json_path): # 将当前json文件都合到一起形成news_inserted.json # "data"键对应的值(列表)是一条条新闻,是通过其他json文件中的"data"键获取的,包括上一次运行产生的news_inserted.json news_inserted = {} news_inserted["data"] = [] titles = [] json_list = os.listdir(json_path) for filename in json_list: if not filename.endswith(".json"): continue with open(json_path + filename, 'r', encoding='utf-8') as f: data = json.load(f)["data"] news_inserted["data"].extend(data) for news in data: titles.append(news["title"]) shutil.move(json_path + filename, json_path + "backup/" + filename) news_inserted["num"] = len(titles) if news_inserted["num"] > 0: with open("news_inserted.json", 'w', encoding="utf-8") as f: json.dump(news_inserted, f, ensure_ascii=False) print(f"已有{len(titles)}条新闻") return titles def get_sentiment(Text): with open("../user.csv", "r") as f: reader = csv.reader(f) next(reader) for x in reader: AccessKeyId = x[1] AccessKeySecret = x[2] client = AcsClient(AccessKeyId, AccessKeySecret, "cn-hangzhou") request = GetSaChGeneralRequest.GetSaChGeneralRequest() request.set_Text(Text) request.set_ServiceCode("alinlp") response = client.do_action_with_exception(request) resp_obj = json.loads(response) data = resp_obj["Data"].replace("true", "True") data = ast.literal_eval(data) return data["result"]["sentiment"] if __name__ == "__main__": # 想加什么博物馆往字典里加就行 keywords = {"故宫博物院": [], "首都博物馆": [], "中国科学技术馆": [], "中国地质博物馆": [], "中国人民革命军事博物馆": [], "中国航空博物馆": [], "中国国家博物馆": [], "北京自然博物馆": [], "上海博物馆": [], "南京博物院": [], "北京鲁迅博物馆": [], "浙江省博物馆": [], "中国茶叶博物馆": []} JSON_PATH = "./" # 默认所有操作均在当前目录 titles = update_check(JSON_PATH) for keyword in keywords.keys(): for curPage in range(1, 11): origin_url = f"http://so.news.cn/getNews?keyword={keyword}&curPage={curPage}&sortField=0&searchFields=1&lang=cn" url_list = get_url_list(origin_url) if url_list is not None: keywords[keyword].extend(url_list) if len(keywords[keyword]) == 0: print(f"没有找到{keyword}的新闻 :(") else: print(f"共找到{len(keywords[keyword])}条关于{keyword}的新闻 :)") crawl_news(keywords[keyword], keyword, titles)
(2)导入数据库
import os import json import mysql.connector # 读取json文件 def load_json(json_path): # 合并各个json文件的内容后,要存入数据库的列表,忽略news_inserted.json news_to_insert = [] json_list = os.listdir(json_path) for filename in json_list: if not filename.endswith(".json"): continue if filename == "news_inserted.json": with open(json_path + filename, 'r', encoding='utf-8') as f: num = json.load(f)["num"] print(f"已有{num}条数据") continue with open(json_path + filename, 'r', encoding='utf-8') as f: data = json.load(f)["data"] if data is None: continue for news in data: news_to_insert.append(tuple(news.values())[:-1]) return news_to_insert def insert_mysql(news_to_insert): print(f"预计插入{len(news_to_insert)}条数据。。。") if len(news_to_insert) == 0: print("没有新的数据要插入") return DATABASE = "MUSEUM" TABLE = "news" mydb = mysql.connector.connect( host = "localhost", user = "root", passwd = "123456", database = DATABASE ) mycursor = mydb.cursor() sql = f"INSERT INTO {TABLE} (museum, time, type, content, photo, source) VALUES (%s, %s, %s, %s, %s, %s)" val = news_to_insert mycursor.executemany(sql, val) mydb.commit() print(f"{mycursor.rowcount}条记录插入成功。。。") mycursor.close() if __name__ == "__main__": json_path = "./" news_to_insert = load_json(json_path) insert_mysql(news_to_insert)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。