技巧:Python 爬虫实战:爬取新闻网站的 10 条经验分享
优采云 发布时间: 2022-12-06 00:20技巧:Python 爬虫实战:爬取新闻网站的 10 条经验分享
大家好,我是聪明鹤。
前段时间完成了一个爬虫项目,完成了国内13条主流新闻网站的内容采集(根据关键词进行爬取)。包括
中国日报、中国新闻网、人民网、光明网、国际在线、中央电视台、中央电视台、中华网、凤凰网、网易新闻、新浪新闻、中国青年网、中国青年在线
新闻网站虽然是一个比较简单的文本爬虫,但是在爬取过程中还是遇到了很多坑,项目完成后也有不少收获。
现将自己的经验整理记录分享,希望对大家有所帮助。
0、目录体内容页面格式不统一。自动识别网页代码获取总页数的几种常用方法。增加爬虫的健壮性 大文件的批量读取参数可以放在配置文件中 1. 文本内容的页面格式不统一
您可能遇到过这种情况。根据关键词搜索结果爬取新闻时,新闻正文页面格式不统一。
这些新闻网页要么来自不同的站点,要么来自不同的新闻版块,要么之前经历过网站修改。各种因素导致网页格式不一致,导致爬虫无法使用统一的解析函数进行解析。工作带来很大的麻烦。
例如,在《人民网》中,春节为关键词的搜索结果中,有不同格式的新闻页面:
《李焕之与春节序曲》中,正文内容在div标签下,属性为rm_txt_con。
《如何在平台春节发红包实现双赢》中,正文内容在属性为artDet的div标签下。
在“跨年电影市场大放异彩”中,正文内容在属性为show_text的div标签下。
在解析网页时,各种接口必须兼容和适配。否则,可能会漏掉一批新闻网页,或者引发异常,甚至导致程序崩溃。
针对以上情况,如果您有更好的解决方案,欢迎与我交流。
我将简要谈谈我的解决方案。
首先,我们可以用最简单的方法if...else...来判断,示例代码如下:
cont1 = bsObj.find("div", attrs={"class": "rm_txt_con"})<br />if cont1:<br /> # parse content 1<br />else:<br /> cont2 = bsObj.find("div", attrs={"class": "artDet"})<br /> if cont2:<br /> # parse content 2<br /> else:<br /> cont3 = bsObj.find("div", attrs={"class": "show_text"})<br /> if cont3:<br /> # parse content 3<br /> else:<br /> print("parse failed")<br />
就是先获取一个标签,如果获取到则根据相应的规则解析文本,如果没有获取则继续寻找下一个……直到所有已知的标签都被检索出来,如果还没有'还没搞定,那就输出get fail。
这种方法逻辑简单,实现方便,确实可以解决问题。
但是,不够优雅!
当页面格式种类较多时,代码会显得非常臃肿,尤其是python代码需要严格缩进时,代码会变得特别不美观,不方便维护。
所以我们可以使用配置的方式来优化上面的代码。
# 走配置的方法 <br />confilter = [<br /> {"tag": "div", "type": "class", "value": "rm_txt_con"},<br /> {"tag": "div", "type": "class", "value": "box_con"},<br /> {"tag": "div", "type": "class", "value": "box_text"},<br /> {"tag": "div", "type": "class", "value": "show_text"},<br /> {"tag": "div", "type": "id", "value": "p_content"},<br /> {"tag": "div", "type": "class", "value": "artDet"},<br />]<br /><br />for f in confilter:<br /> con = bsObj.find(f["tag"], attrs={f["type"]: f["value"]})<br /> if con:<br /> # parse content<br /> break<br />
这样,每次添加网页类型时,只需要在配置中添加一行即可,非常方便。
2、自动识别网页代码
不同的网页使用不同的字符编码,最常用的是utf-8和GB2312。
如果解析网页内容时字符编码设置不匹配,抓取到的文字会出现乱码。
如何自动识别网页的编码,可以试试chardet库,它可以根据网页内容自动推断出最有可能的编码格式和对应的置信度。
import chardet<br />import requests<br /><br />r = requests.get("https://www.xxxxxxxxx.com")<br /># 推测网页内容的编码格式<br />data =chardet.detect(r.content)<br /># 结果是 json 格式,<br /># data["encoding"] 为编码格式,data["confidence"] 为置信度<br />if data["confidence"] > 0.9:<br /> r.encoding = data["encoding"]<br />else:<br /> r.encoding = "utf-8"<br />print(r.text)<br />
当然,还有一个更简洁的方法apparent_encoding。
import chardet<br />import requests<br /><br />r = requests.get("https://www.xxxxxxxxx.com")<br />r.encoding = r.apparent_encoding<br />print(r.text)<br />
两者的识别效果差不多,后者书写更简洁,使用更方便;前者可以查看更详细的代码识别信息。使用哪种方法取决于实际情况。
但是在使用过程中,我发现这两种识别网页编码的方法并不是100%准确的。有些新闻网页(我猜是网页中混合了多种编码格式的内容)会被识别为错误的编码,导致解析出现乱码。
对于这种情况,我还没有想到合适的解决方案。我目前的解决方案是,如果代码识别结果的置信度低于90%,则视为识别失败。这个时候根据具体情况给它一个默认的编码格式,比如utf-8或者GB2312。
3、获取总页数的几种常用方法
我们在循环爬取新闻列表的时候,会遇到一个很重要的问题,就是程序需要循环多少次。
翻译是新闻列表中有多少页。
关键词搜索到的搜索结果,不同的网站有不同的显示方式,对应不同的获取总页数的方式。
3.1 返回结果json收录总页数
有的网站使用Ajax动态加载数据,也就是说服务器会把每个页面的新闻数据以json的形式发送出去。一般情况下,这个请求会收录数据项总数和页面总数的信息。
以凤凰网为例,关键词搜索结果的请求响应消息中收录total和totalPage两个字段,分别表示搜索结果的条目总数和总页数。
这种情况下,我们可以直接解析json,提取总页数。
示例代码如下:
page = jsonObj["data"]["totalPage"]<br />print(int(page))<br />
当然,为了防止以后消息协议发生变化,如果在解析json的时候找不到key,报错crash,可以在解析前加一个判断(判断key是否存在)来增加健壮性程序。
if "data" in jsonObj and "totalPage" in jsonObj["data"]:<br /> page = jsonObj["data"]["totalPage"]<br /> print(int(page))<br />
3.2 解析最后一页按钮的链接
在带有翻页按钮的网站中,如果有末页、尾页、尾页按钮,通过分析按钮的跳转链接,可以知道搜索结果的总页数。
以中国新闻网为例,查看最后一个页面按钮的点击事件,会发现点击时会调用ongetKey()的一个JavaScript方法。经过观察测试,发现传入的参数98是点击后跳转的参数。页码。
因此,我们只需要获取最后一个翻页按钮的点击响应事件,提取其参数,即可获取总页数。
示例代码如下:
# 获取尾页按钮<br />bsObj = BeautifulSoup(html, "html.parser")<br />pagediv = bsObj.find("div", attrs={"id": "pagediv"})<br />lastPage = pagediv.find_all("a")[-1]<br /># 从尾页按钮的 href 中提取总页码<br />total = re.findall(r"\d+", lastPage["href"])<br />print(int(total[0]))<br />
让我们改进代码,增加它的健壮性,并封装它。
def getTotal_ZGXWW(html):<br /> bsObj = BeautifulSoup(html, "html.parser")<br /> pagediv = bsObj.find("div", attrs={"id": "pagediv"})<br /> if not pagediv:<br /> return 0<br /> lastPage = pagediv.find_all("a")<br /> if len(lastPage) > 0 and lastPage[-1] and "href" in lastPage[-1]:<br /> total = re.findall(r"\d+", lastPage[-1]["href"])<br /> if len(total) > 0:<br /> return int(total[0])<br /> return 0<br />
3.3 搜索结果总数除以每页展示次数
搜索结果页一般显示本次搜索的条目总数,用总数除以每页条目数,四舍五入得到总页数。
以央视为例,在页面顶部
在标签中,有此搜索结果中显示的项目总数。
通常,每页显示的条目数是固定的。我们只需要将条目总数除以每页的条目数,并将结果四舍五入即可得到总页数。
示例代码如下:
bsObj = BeautifulSoup(html, "html.parser")<br /># 获取标签<br />lmdhd = bsObj.find("div", attrs={"class": "lmdhd"})<br /># 正则提取总条数<br />total = re.findall(r"\d+", lmdhd.text)<br /># 计算总页数(每页 10 条)<br />totalPage = Math.ceil(int(total[0]) / 10)<br />print(totalPage)<br />
让我们改进代码,增加它的健壮性,并封装它。
def getTotal_YSW(html):<br /> bsObj = BeautifulSoup(html, "html.parser")<br /> lmdhd = bsObj.find("div", attrs={"class": "lmdhd"})<br /> if not lmdhd:<br /> return 0<br /> total = re.findall(r"\d+", lmdhd.text)<br /> if len(total) > 0:<br /> totalPage = Math.ceil(int(total[0]) / 10)<br /> return totalPage<br /> return 0<br />
不过这种方法不一定准确,因为网站的很多搜索结果都没有完整显示,只显示前几页的数据。
这样会导致一些问题,比如爬取大量重复的数据;抓取过程中出现空数据甚至报错,所以需要做好去重和异常捕获。
3.4 循环爬行直到终止条件
对于一些瀑布流展示数据的网站,页码的划分不是很明显,我们没有办法直接知道总页数。
这种情况下,我们可以在while(True)循环中加入终止条件的判断,比如返回数据为空,释放时间不符合要求等条件。
示例代码(伪代码)如下:
while(True):<br /> # 爬取数据,以及下一页的链接<br /> data, url = getData_And_NextUrl(url)<br /> # 保存数据<br /> saveData(data)<br /> # 当下一页链接为空时退出<br /> if not url:<br /> break;<br />
while(True):<br /> # 爬取数据,以及下一页的链接<br /> data, url = getData_And_NextUrl(url)<br /> # 当数据为空时退出<br /> if not data:<br /> break;<br /> # 保存数据<br /> saveData(data)<br />
4.如何实现断点续传
爬虫难免会报错,崩溃退出。对于一个爬取大量数据的爬虫来说,每次崩溃都从头开始爬取无疑是浪费时间和挫败感。
所以加入了断点续存的功能,非常人性化。
在访问新闻详情页之前,先搜索本地是否有对应保存的新闻文件,有则跳过,没有则开始爬取。
示例代码如下:
# fetchNewsUrlList 函数用来获取搜索结果中某一页的全部新闻链接<br /># keyword 是搜索的关键词,page 是页码<br />newsList = fetchNewsUrlList(keyword, page)<br />for url in newsList:<br /> # getFilenameByUrl 函数用来根据 url 获取保存该网页新闻的文件名<br /> filename = getFilenameByUrl(url)<br /> # path 是文件保存的路径<br /> # 如果该文件存在,则跳过<br /> if os.path.exists(path + filename):<br /> continue<br /> # 若没有该文件,则爬取该网页并保存新闻内容<br /> content = getNewsContent(url)<br /> saveData(content)<br />
通过这种机制,我们可以快速跳过之前爬取的数据,直接从上次中断的地方继续爬取,不仅节省了大量的时间和网络资源,也在一定程度上降低了对目标的影响网站引起的负载。
另外,这种断点续传机制对于一些需要周期性增量爬取数据的项目也是很有必要的。
5.去除文件名中的特殊字符
我们知道.txt文件的文件名中不允许出现一些特殊字符。
文件名不能收录以下任何字符:\ / : * ? " |
如果我们使用新闻标题作为保存的文件名,标题中的一些特殊字符可能会导致文件保存失败,甚至出错导致死机。
所以,如果我们使用新闻标题作为保存的文件名,我们需要对文件名做一些处理,去除或替换特殊字符。
<p># 使用正则表达式剔除特殊字符<br />import re<br /><br />def fixFilename(filename):<br /> intab = r'[?*/\\|.:>