本文将介绍我是如何在 python 爬虫里面一步一步踩坑, 然后慢慢走出来的, 期间碰到的所有问题我都会详细说明, 让大家以后碰到这些问题时能够快速确定问题的来源, 后面的代码只是贴出了核心代码, 更详细的代码暂时没有贴出来.
流程一览
首先我是想爬某个网站上面的所有文章内容, 但是由于之前没有做过爬虫 (也不知道到底那个语言最方便), 所以这里想到了是用 python 来做一个爬虫 (毕竟人家的名字都带有爬虫的含义), 我这边是打算先将所有从网站上爬下来的数据放到 Elasticsearch 里面, 选择 Elasticsearch 的原因是速度快, 里面分词插件, 倒排索引, 需要数据的时候查询效率会非常好 (毕竟爬的东西比较多), 然后我会将所有的数据在 Elasticsearch 的老婆 kibana 里面将数据进行可视化出来, 并且分析这些文章内容, 可以先看一下预期可视化的效果 (上图了), 这个效果图是 kibana6.4 系统给予的帮助效果图 (就是说你可以弄成这样, 我也想弄成这样). 后面我会发一个 dockerfile 上来 (现在还没弄).
image
- Jdk (Elasticsearch 需要)
- Elasticsearch (用来存储数据)
- Kinaba (用来操作 Elasticsearch 和数据可视化)
- Python (编写爬虫)
- Redis (数据排重)
- liaochengdeMacBook-Pro:scrapy liaocheng$ scrapy startproject scrapyDemo
- New Scrapy project 'scrapyDemo', using template directory '/usr/local/lib/python3.7/site-packages/scrapy/templates/project', created in:
- /Users/liaocheng/script/scrapy/scrapyDemo
- You can start your first spider with:
- cd scrapyDemo
- scrapy genspider example example.com
- liaochengdeMacBook-Pro:scrapy liaocheng$
- liaochengdeMacBook-Pro:scrapy liaocheng$ scrapy genspider demo juejin.im
- Created spider 'demo' using template 'basic'
- liaochengdeMacBook-Pro:scrapy liaocheng$
- # -*- coding: utf-8 -*-
- import scrapy
- class DemoSpider(scrapy.Spider):
- name = 'demo' ## 爬虫的名字
- allowed_domains = ['juejin.im'] ## 需要过滤的域名, 也就是只爬这个网址下面的内容
- start_urls = ['https://juejin.im/post/5c790b4b51882545194f84f0'] ## 初始 url 链接
- def parse(self, response): ## 如果新建的 spider 必须实现这个方法
- pass
- # -*- coding: utf-8 -*-
- import scrapy
- class DemoSpider(scrapy.Spider):
- name = 'demo' ## 爬虫的名字
- allowed_domains = ['juejin.im'] ## 需要过滤的域名, 也就是只爬这个网址下面的内容
- def start_requests(self):
- start_urls = ['http://juejin.im/'] ## 初始 url 链接
- for url in start_urls:
- # 调用 parse
- yield scrapy.Request(url=url, callback=self.parse)
- def parse(self, response): ## 如果新建的 spider 必须实现这个方法
- pass
- import scrapy
- class ArticleItem(scrapy.Item): ## 需要实现 scrapy.Item 文件
- # 文章 id
- id = scrapy.Field()
- # 文章标题
- title = scrapy.Field()
- # 文章内容
- content = scrapy.Field()
- # 作者
- author = scrapy.Field()
- # 发布时间
- createTime = scrapy.Field()
- # 阅读量
- readNum = scrapy.Field()
- # 点赞数
- praise = scrapy.Field()
- # 头像
- photo = scrapy.Field()
- # 评论数
- commentNum = scrapy.Field()
- # 文章链接
- link = scrapy.Field()
- def parse(self, response):
- # 获取页面上所有的 url
- nextPage = response.CSS("a::attr(href)").extract()
- # 遍历页面上所有的 url 链接, 时间复杂度为 O(n)
- for i in nextPage:
- if nextPage is not None:
- # 将链接拼起来
- url = response.urljoin(i)
- # 必须是掘金的链接才进入
- if "juejin.im" in str(url):
- # 存入 Redis, 如果能存进去, 就是一个没有爬过的链接
- if self.insertRedis(url) == True:
- # dont_filter 作用是是否过滤相同 url true 是不过滤, false 为过滤, 我们这里只爬一个页面就行了, 不用全站爬, 全站爬对对掘金不是很友好, 我么这里只是用来测试的
- yield scrapy.Request(url=url, callback=self.parse,headers=self.headers,dont_filter=False)
- # 我们只分析文章, 其他的内容都不管
- if "/post/" in response.url and "#comment" not in response.url:
- # 创建我们刚才的 ArticleItem
- article = ArticleItem()
- # 文章 id 作为 id
- article['id'] = str(response.url).split("/")[-1]
- # 标题
- article['title'] = response.CSS("#juejin> div.view-container> main> div> div.main-area.article-area.shadow> article> h1::text").extract_first()
- # 内容
- parameter = response.CSS("#juejin> div.view-container> main> div> div.main-area.article-area.shadow> article> div.article-content").extract_first()
- article['content'] = self.parseToMarkdown(parameter)
- # 作者
- article['author'] = response.CSS("#juejin> div.view-container> main> div> div.main-area.article-area.shadow> article> div:nth-child(6)> meta:nth-child(1)::attr(content)").extract_first()
- # 创建时间
- createTime = response.CSS("#juejin> div.view-container> main> div> div.main-area.article-area.shadow> article> div.author-info-block> div> div> time::text").extract_first()
- createTime = str(createTime).replace("年", "-").replace("月", "-").replace("日","")
- article['createTime'] = createTime
- # 阅读量
- article['readNum'] = int(str(response.CSS("#juejin> div.view-container> main> div> div.main-area.article-area.shadow> article> div.author-info-block> div> div> span::text").extract_first()).split(" ")[1])
- # 点赞数
- article['badge'] = response.CSS("#juejin> div.view-container> main> div> div.article-suspended-panel.article-suspended-panel> div.like-btn.panel-btn.like-adjust.with-badge::attr(badge)").extract_first()
- # 评论数
- article['commentNum'] = response.CSS("#juejin> div.view-container> main> div> div.article-suspended-panel.article-suspended-panel> div.comment-btn.panel-btn.comment-adjust.with-badge::attr(badge)").extract_first()
- # 文章链接
- article['link'] = response.url
- # 这个方法和很重要 (坑), 之前就是由于执行 yield article, pipeline 就一直不能获取数据
- yield article
- # 将内容转换成 Markdown
- def parseToMarkdown(self, param):
- return tomd.Tomd(str(param)).Markdown
- # url 存入 Redis, 如果能存那么就没有该链接, 如果不能存, 那么就存在该链接
- def insertRedis(self, url):
- if self.Redis != None:
- return self.Redis.sadd("articleUrlList", url) == 1
- else:
- self.Redis = self.redisConnection.getClient()
- self.insertRedis(url)
- from Elasticsearch import Elasticsearch
- class ArticlePipelines(object):
- # 初始化
- def __init__(self):
- # Elasticsearch 的 index
- self.index = "article"
- # Elasticsearch 的 type
- self.type = "type"
- # Elasticsearch 的 ip 加端口
- self.es = Elasticsearch(hosts="localhost:9200")
- # 必须实现的方法, 用来处理 yield 返回的数据
- def process_item(self, item, spider):
- # 这里是判断, 如果是 demo 这个爬虫的数据才处理
- if spider.name != "demo":
- return item
- result = self.checkDocumentExists(item)
- if result == False:
- self.createDocument(item)
- else:
- self.updateDocument(item)
- # 添加文档
- def createDocument(self, item):
- body = {
- "title": item['title'],
- "content": item['content'],
- "author": item['author'],
- "createTime": item['createTime'],
- "readNum": item['readNum'],
- "praise": item['praise'],
- "link": item['link'],
- "commentNum": item['commentNum']
- }
- try:
- self.es.create(index=self.index, doc_type=self.type, id=item["id"], body=body)
- except:
- pass
- # 更新文档
- def updateDocument(self, item):
- parm = {
- "doc" : {
- "readNum" : item['readNum'],
- "praise" : item['praise']
- }
- }
- try:
- self.es.update(index=self.index, doc_type=self.type, id=item["id"], body=parm)
- except:
- pass
- # 检查文档是否存在
- def checkDocumentExists(self, item):
- try:
- self.es.get(self.index, self.type, item["id"])
- return True
- except:
- return False
- liaochengdeMacBook-Pro:scrapyDemo liaocheng$ scrapy list
- demo
- liaochengdeMacBook-Pro:scrapyDemo liaocheng$
- GET /article/_search
- {
- "query": {
- "match_all": {}
- }
- }
- {
- "took": 7,
- "timed_out": false,
- "_shards": {
- "total": 5,
- "successful": 5,
- "skipped": 0,
- "failed": 0
- },
- "hits": {
- "total": 1,
- "max_score": 1,
- "hits": [
- {
- "_index": "article2",
- "_type": "type",
- "_id": "5c790b4b51882545194f84f0",
- "_score": 1,
- "_source": {}
- }
- ]
- }
- }
来源: http://www.jianshu.com/p/a0b64dbab60a