предисловие
Поскольку рекомендательная система на основе машинного обучения была исследована недавно, для обучения моделей ИИ требуется большой объем данных, но в процессе тестирования и проверки моделей из-за отсутствия китайских наборов данных (или их люди проделали большую работу в этом отношении) очень плохо), может использовать только наборы данных зарубежных общедоступных рекомендательных систем, есть известныеНабор данных о рейтингах фильмов MovieLensиDel.icio.us Набор данных рекомендаций по ссылкам, хотя плюсы и минусы рекомендательной модели можно примерно оценить, рассчитав функцию потерь для проведения соответствующих оптимизаций, но из-за различий в локали, культуре и т. д. определенный разрыв между оценками иностранцев того или иного фильма все же существует и наши. , при выводе результата рекомендации, даже если определенному фильму или определенной ссылке на веб-сайт дается высокое сходство, я все еще не уверен, действительно ли результат рекомендации так точен, как вычисляет функция потерь. Поэтому, чтобы иметь китайский набор данных, который можно использовать для обучения, в этой статье записан процесс захвата обзоров фильмов Douban.
анализ сайта
Прежде всего, нам нужно проанализировать сайт, который нужно просканировать.Дубан фильм, в основном с помощью средств отладки поисковой системы или браузера, чтобы увидеть, есть ли API, которые можно использовать, а затем начать анализировать структуру страниц веб-сайта и найти информацию, которую можно извлечь, исходя из предположения, что API не найдены.
Через поисковик нашелПлатформа разработчиков Douban,существуетДубан фильмВ документации есть подробные интерфейсы для получения фильмов, получения рецензий на фильмы и т. д. Как раз когда я думал, что очередной сбор данных станет очень простым, меня все же успокоила следующая картинка.
Если вы зарегистрировались в качестве разработчика Douban до 2015 года, поздравляем, вы можете получать любые данные через API или SDK, предоставляемые Douban.
Получить информацию о фильме
Хотя невозможно получить APIKey, через документацию я все же обнаружил, что некоторые GET-запросы не требуют аутентификации AUTH, то есть наличие или отсутствие APIKey не влияет на использование. Среди них, что нам полезно, так это получитьTOP250电影列表
Интерфейс:
http://api.douban.com/v2/movie/top250
Формат, возвращаемый интерфейсом, примерно следующий:
Он содержит некоторую подробную информацию о фильме, которой достаточно для рекомендательной системы.
Кроме того, используя инструменты отладки Chrome,Дубан Фильмы - КатегорияНа этой странице я нашел используемый ими интерфейс JQuery, который также является запросом GET и не требует AUTH.
Подробный интерфейс выглядит следующим образом: вы можете итеративно получить все записи фильмов, изменив start
https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=&start=20
Этот интерфейс может получить все фильмы, включенные в Douban.После моего теста, когда начало изменено на 10000, возвращаемые данные уже пусты.
Получить информацию о обзоре фильма
Теперь, когда есть метод получения информации о фильме, следующим шагом будет анализ того, как получить информацию о просмотре фильма.
На странице сведений о каждом фильме, напримерТрумэн, мы нашли следующие страницы с подробными сведениями, на которых показаны обзоры фильмов и краткие обзоры для этого фильма соответственно.
https://movie.douban.com/subject/1292064/reviews ##影评页面
https://movie.douban.com/subject/1292064/comments ##短评页面
Как обычно, после использования инструмента отладки, чтобы увидеть, есть ли какой-либо доступный API-интерфейс, я обнаружил, что на этот раз мне не так повезло.Эта страница обзора фильма отображается сервером.
Без интерфейса проанализируем страницу, все же через средство отладки:
Каждый комментарий находится в блоке div элемента обзора, а все комментарии находятся в блоке div списка обзора, мы можем легко найти подробную информацию о каждом комментарии с помощью синтаксиса xpath, ниже приведен оператор xpath всей информации. на него извлекать содержимое, когда пишем краулер
评论列表: "//div[contains(@class,'review-list')]//div[contains(@class,'review-item')]"
评论ID: ".//div[@class='main-bd']//div[@class='review-short']/@data-rid"
作者头像: "./header[@class='main-hd']//a[@class='avator']//img/@src "
作者昵称: ".//header//a[@class='name']/text()"
推荐程度(评分): ".//header//span[contains(@class,'main-title-rating')]/@title"
影评标题: ".//div[@class='main-bd']//h2//a/text()"
影评摘要: ".//div[@class='main-bd']//div[@class='short-content']/text()"
影评详情页链接: ".//div[@class='main-bd']//h2//a/@href"
Среди них мы не можем получить конкретную оценку напрямую, а можем получить только конкретное текстовое описание.После моей проверки конкретные соответствующие отношения выглядят следующим образом:
'力荐': 5,
'推荐': 4,
'还行': 3,
'较差': 2,
'很差': 1,
В последующем процессе написания кода мы преобразуем его в соответствующую рейтинговую информацию в соответствии с этим соответствием.
внедрить обходчик
Теперь, когда анализ почти завершен и необходимая нам информация в основном доступна, мы приступим к реализации конкретного краулера.Мы используем Scrapy, фреймворк краулера Python, чтобы упростить процесс разработки краулера. Установка Scrapy и построение среды VirtualEnv подробно обсуждаться не будут, и они не входят в рамки данной статьи.Адрес документа Scrapy на китайском языке
Создать проект проекта
##创建DoubanSpider工程
scrapy startproject Douban
Созданный каталог проекта выглядит примерно так:
в:
spiders: 爬虫文件夹,存放具体的爬虫代码,我们待会要编写的两个爬虫(电影信息和影评信息)就需要放在这个文件夹下
items.py: 模型类,所有需要结构化的数据都要预先在此文件中定义
middlewares.py: 中间件类,scrapy的核心之一,我们会用到其中的downloadMiddleware,
pipelines.py: 管道类,数据的输出管理,是存数据库还是存文件在这里决定
settings.py: 设置类,一些全局的爬虫设置,如果每个爬虫需要有自定义的地方,可以在爬虫中直接设置custom_settings属性
Сканер информации о фильмах
Поскольку интерфейс API можно использовать для получения информации о фильме, странице здесь не нужно использовать поисковый робот для обработки данных.
Создайте новый файл movie.py в пауках, чтобы определить наш поисковый робот.
Поскольку мы сканируем фильмы через API-интерфейс, нам не нужен последующий анализ, поэтому наш краулер может напрямую наследовать Spider.
определить сканер
class MovieSpider(Spider):
name = 'movie' #爬虫名称
allow_dominas = ["douban.com"] #允许的域名
#自定义的爬虫设置,会覆盖全局setting中的设置
custom_settings = {
"ITEM_PIPELINES": {
'Douban.pipelines.MoviePipeline': 300
},
"DEFAULT_REQUEST_HEADERS": {
'accept': 'application/json, text/javascript, */*; q=0.01',
'accept-encoding': 'gzip, deflate',
'accept-language': 'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4',
'referer': 'https://mm.taobao.com/search_tstar_model.htm?spm=719.1001036.1998606017.2.KDdsmP',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36',
'x-requested-with': 'XMLHttpRequest',
},
"ROBOTSTXT_OBEY":False #需要忽略ROBOTS.TXT文件
}
пользовательская настройка,ITEM_PIPELINES
Указывает интерфейс канала, используемый для вывода данных после получения данных.
DEFAULT_REQUEST_HEADERS
Это позволяет нашему Пауку маскироваться под браузер, чтобы предотвратить его перехват Douban.
иROBOTSTXT_OBEY
Это сделано для того, чтобы наш поисковый робот проигнорировал предупреждение файла ROBOTS.txt.
Далее черезstart_request
Сообщите сканеру, какую ссылку сканировать:
def start_requests(self):
url = '''https://movie.douban.com/j/new_search_subjects?sort=T&range=0,10&tags=&start={start}'''
requests = []
for i in range(500):
request = Request(url.format(start=i*20), callback=self.parse_movie)
requests.append(request)
return requests
Поскольку мы анализировали веб-сайт ранее, когда параметр запуска достигает 10000, мы не можем получить данные, поэтому здесь мы напрямую используем это число, чтобы получить все ссылки в цикле.
Затем проанализируйте содержимое, возвращаемое каждым интерфейсом:
def parse_movie(self, response):
jsonBody = json.loads(response.body)
subjects = jsonBody['data']
movieItems = []
for subject in subjects:
item = MovieItem()
item['id'] = int(subject['id'])
item['title'] = subject['title']
item['rating'] = float(subject['rate'])
item['alt'] = subject['url']
item['image'] = subject['cover']
movieItems.append(item)
return movieItems
В запросе мы указываем метод parse_movie для анализа возвращаемого содержимого. Здесь нам нужно использовать элемент, определенный в items.py. Конкретный элемент выглядит следующим образом:
Определить элемент
#定义你需要获取的数据
class MovieItem(scrapy.Item):
id = scrapy.Field()
title = scrapy.Field()
rating = scrapy.Field()
genres = scrapy.Field()
original_title = scrapy.Field()
alt = scrapy.Field()
image = scrapy.Field()
year = scrapy.Field()
После того, как элементы будут возвращены в Scrapy, Scrapy вызовет тот, который мы указали в custom_setting передDouban.pipelines.MoviePipeline
Для обработки полученного элемента MoviePipeline определяется в файле pipe.py, детали следующие:
Определить конвейеры
class MoviePipeline(object):
movieInsert = '''insert into movies(id,title,rating,genres,original_title,alt,image,year) values ('{id}','{title}','{rating}','{genres}','{original_title}','{alt}','{image}','{year}')'''
def process_item(self, item, spider):
id = item['id']
sql = 'select * from movies where id=%s'% id
self.cursor.execute(sql)
results = self.cursor.fetchall()
if len(results) > 0:
rating = item['rating']
sql = 'update movies set rating=%f' % rating
self.cursor.execute(sql)
else:
sqlinsert = self.movieInsert.format(
id=item['id'],
title=pymysql.escape_string(item['title']),
rating=item['rating'],
genres=item.get('genres'),
original_title=item.get('original_title'),
alt=pymysql.escape_string(item.get('alt')),
image=pymysql.escape_string(item.get('image')),
year=item.get('year')
)
self.cursor.execute(sqlinsert)
return item
def open_spider(self, spider):
self.connect = pymysql.connect('localhost','root','******','douban', charset='utf8', use_unicode=True)
self.cursor = self.connect.cursor()
self.connect.autocommit(True)
def close_spider(self, spider):
self.cursor.close()
self.connect.close()
В этом конвейере мы вставляем полученный элемент каждый раз в определенную таблицу данных, подключаясь к базе данных mysql.
запустить сканер
Введите в командной строке:
scrapy crawl movie
сканер обзоров фильмов
Со сканером обзоров фильмов намного сложнее, потому что мы получаем информацию о фильмах непосредственно через интерфейс.Формат данных, возвращаемых этим интерфейсом, является единым, и в основном нет ненормальной ситуации, и количество фильмов ограничено, и его можно проползли за короткое время.После этого антикраулер Дубана не сработает, и они будут сталкиваться в процессе написания краулера рецензии на фильм.
логика сканера
class ReviewSpider(Spider):
name = "review"
allow_domain = ['douban.com']
custom_settings = {
"ITEM_PIPELINES": {
'Douban.pipelines.ReviewPipeline': 300
},
"DEFAULT_REQUEST_HEADERS": {
'connection':'keep-alive',
'Upgrade-Insecure-Requests':'1',
'DNT':1,
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding':'gzip, deflate, br',
'Accept-Language':'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7',
'Cookie':'bid=wpnjOBND4DA; ll="118159"; __utmc=30149280;', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/67.0.3396.87 Safari/537.36',
},
"ROBOTSTXT_OBEY": False,
# "DOWNLOAD_DELAY": 1,
"RETRY_TIMES": 9,
"DOWNLOAD_TIMEOUT": 10
}
По сравнению со сканером, который получает информацию о фильме, в custom_setting есть несколько дополнительных настроек:
RETRY_TIMES
: используется для управления максимальным количеством повторных попыток, потому что у Douban есть механизм защиты от сканирования, когда IP-адрес имеет слишком много посещений, он ограничивает доступ к IP-адресу, поэтому, чтобы обойти этот механизм, мы используем IP-адрес прокси для обхода соответствующая страница, каждое сканирование Если вы берете страницу, измените IP один раз, но из-за неравномерного качества IP-адреса прокси, заряд может быть лучше, но он все равно будет существовать.Во избежание возникновения игнорирования страницы поскольку прокси не может подключиться, мы устанавливаем это значение. , когда количество повторных попыток больше установленного значения, а страница все еще не получена, соединение будет пропущено. Если качество IP-адреса вашего прокси плохое, увеличьте количество раз здесь.
DOWNLOAD_TIMEOUT
: время ожидания загрузки, по умолчанию составляет 60 секунд, здесь изменено на 10 секунд, чтобы ускорить общую скорость сканирования, из-за RETRY_TIMES время оценки RETRY составляет 1 минуту, если таких проблемных страниц много, тогда все сканирование процесс будет очень долгим.
DOWNLOAD_DELAY
: задержка загрузки, если у вас все еще есть доступ к 403 после использования прокси-IP, установите это значение, потому что определенный IP-адрес, который слишком часто посещает страницу, активирует механизм предотвращения сканирования Douban.
def start_requests(self):
#从数据库中找到所有的moviesId
self.connect = pymysql.connect('localhost','root','******','douban', charset='utf8', use_unicode=True)
self.cursor = self.connect.cursor()
self.connect.autocommit(True)
sql = "select id,current_page,total_page from movies"
self.cursor.execute(sql)
results = self.cursor.fetchall()
url_format = '''https://movie.douban.com/subject/{movieId}/reviews?start={offset}'''
for row in results:
movieId = row[0]
current_page = row[1]
total_page = row[2]
if current_page != total_page: ##说明评论没有爬完
url = url_format.format(movieId=movieId, offset=current_page*20)
request = Request(url, callback=self.parse_review, meta={'movieId': movieId}, dont_filter=True)
yield request
Как обычно, мы сообщаем Scrapy начальную ссылку URL для сканирования в start_request, Согласно нашему предыдущему анализу, формат адреса страницы обзора фильма:
https://movie.douban.com/subject/{movieId}/reviews?start={offset}
Для movieId наш предыдущий поисковый робот уже собрал информацию обо всех фильмах, поэтому мы сначала получаем всю захваченную информацию о фильмах, запрашивая базу данных, получаем из нее movieId, а затем создаем ссылку на страницу.
url = url_format.format(movieId=movieId, offset=current_page*20)
Поскольку процесс сканирования обзоров фильмов Douban очень долгий, в середине будут возникать различные проблемы, которые приведут к неожиданному завершению работы сканера, поэтому нам нужен механизм, позволяющий сканеру продолжить сканирование с того места, где он остановился в прошлый раз. Текущая_страница и общая_страница для этой цели.Да, в последующем процессе анализа данных, каждый раз, когда страница анализируется, количество страниц текущей страницы будет сохранено, чтобы предотвратить непредвиденные ситуации.
def parse_review(self, response):
movieId = response.request.meta['movieId']
review_list = response.xpath("//div[contains(@class,'review-list')]//div[contains(@class,'review-item')]")
for review in review_list:
item = ReviewItem()
item['id'] = review.xpath(".//div[@class='main-bd']//div[@class='review-short']/@data-rid").extract()[0]
avator = review.xpath(".//header//a[@class='avator']/@href").extract()[0]
item['username'] = avator.split('/')[-2]
item['avatar'] = review.xpath("./header[@class='main-hd']//a[@class='avator']//img/@src").extract()[0]
item['nickname'] = review.xpath(".//header//a[@class='name']/text()").extract()[0]
item['movieId'] = movieId
rate = review.xpath(".//header//span[contains(@class,'main-title-rating')]/@title").extract()
if len(rate)>0:
rate = rate[0]
item['rating'] = RATING_DICT.get(rate)
item['create_time'] = review.xpath(".//header//span[@class='main-meta']/text()").extract()[0]
item['title'] = review.xpath(".//div[@class='main-bd']//h2//a/text()").extract()[0]
item['alt'] = review.xpath(".//div[@class='main-bd']//h2//a/@href").extract()[0]
summary = review.xpath(".//div[@class='main-bd']//div[@class='short-content']/text()").extract()[0]
item['summary'] = summary.strip().replace('\n', '').replace('\xa0(','')
yield item
current_page = response.xpath("//span[@class='thispage']/text()").extract()
total_page = response.xpath("//span[@class='thispage']/@data-total-page").extract()
paginator = response.xpath("//div[@class='paginator']").extract()
if len(paginator) == 0 and len(review_list): ##不存在导航条,但是评论列表存在,说明评论只有一页
sql = "update movies set current_page = 1, total_page=1 where id='%s'" % movieId
self.cursor.execute(sql)
elif len(paginator) and len(review_list):
current_page = int(current_page[0])
total_page = int(total_page[0])
sql = "update movies set current_page = %d, total_page=%d where id='%s'" % (current_page, total_page, movieId)
self.cursor.execute(sql)
if current_page != total_page:
url_format = '''https://movie.douban.com/subject/{movieId}/reviews?start={offset}'''
next_request = Request(url_format.format(movieId=movieId, offset=current_page*20),
callback=self.parse_review,
dont_filter=True, meta={'movieId': movieId})
yield next_request
else:
yield response.request
Затем проанализируйте функцию анализа, сбор данных DoubanItem не будет дополнительно введен, а конкретное содержимое можно легко определить с помощью оператора xpath, использованного в предыдущем анализе.
MovieId передается в стартовой ссылке через мета-атрибут запроса.Конечно, вы можете найти место, содержащее movieId, проанализировав веб-страницу.
current_page = response.xpath("//span[@class='thispage']/text()").extract()
total_page = response.xpath("//span[@class='thispage']/@data-total-page").extract()
paginator = response.xpath("//div[@class='paginator']").extract()
Функция приведенного выше базового кода в основном состоит в том, чтобы получить нижнюю панель навигации на странице обзора фильма.
Однако есть две ситуации, в которых эта панель навигации не может быть получена:
1. 当某个电影的评论不足20条时,也就是只有一页评论。
2. 当触发了豆瓣的反爬虫的机制时,返回的页面并不是评论页面,而是一个验证页面,自然也找不到导航条
Итак, в следующем коде я использую эти переменные для оценки описанных выше ситуаций:
1. 情况1时,不需要继续爬取剩下的评论,直接将current_page和total_page设置为1保存到movie表即可
2. 情况2时,由于此时触发了反爬虫机制,返回的页面没有我们的数据,如果我们直接忽略掉的话,会损失大量的数据(这种情况很常见),所以我们就干脆再试一次,返回request,让Scrapy重新爬取这个页面,因为每次重新爬取都会换一个新的代理IP,所以我们有很大概率下次抓取就是正常的。此处有一点需要注意:因为Scrapy默认会过滤掉重复请求,所以我们需要在构造Request的时候讲dont_filter参数设置为True,让其不要过滤重复链接。
3. 正常情况时,通过xpath语法获取的下一页评论的链接地址然后构造一个request交给Scrapy继续爬取
Промежуточное ПО для загрузки обзоров фильмов
Как упоминалось выше, при сканировании страниц обзора фильмов вам необходимо использовать IP-адрес прокси-сервера, чтобы обойти механизм предотвращения сканирования Douban.Конкретные настройки прокси-сервера должны быть установлены в DownloadMiddleware.
class DoubanDownloaderMiddleware(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
ip_list = None
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
if self.ip_list is None or len(self.ip_list) == 0:
response = requests.request('get','http://api3.xiguadaili.com/ip/?tid=555688914990728&num=10&protocol=https').text
self.ip_list = response.split('\r\n')
ip = random.choice(self.ip_list)
request.meta['proxy'] = "https://"+ip
print("当前proxy:%s" % ip)
self.ip_list.remove(ip)
return None
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# # - or raise IgnoreRequest
if response.status == 403:
res = parse.urlparse(request.url)
res = parse.parse_qs(res.query)
url = res.get('r')
if url and len(url) > 0 :
request = request.replace(url=res['r'][0])
return request
return response
В основном необходимо реализовать две функции: process_request и process_response.Первая вызывается Scrapy перед сканированием каждой страницы, а вторая вызывается после сканирования каждой страницы.
В первом методе мы получаем интерфейс, вызывая IP-адрес онлайн-прокси, получаем IP-адрес прокси, а затем устанавливаем атрибут прокси запроса для достижения функции замены прокси.Конечно, вы также можете прочитать IP-адрес прокси через файл.
В последнем методе мы оцениваем код состояния как 403, потому что этот код состояния указывает на то, что текущий запрос запрещен к обнаружению и запрещен антикраулерами, и все, что нам нужно сделать, это переупаковать адрес запрещенного запроса и освободить его. в очередь сканирования Scrapy.
Обзор фильма Пункт
class ReviewItem(scrapy.Item):
id = scrapy.Field()
username = scrapy.Field()
nickname = scrapy.Field()
avatar = scrapy.Field()
movieId = scrapy.Field()
rating = scrapy.Field()
create_time = scrapy.Field()
title = scrapy.Field()
summary = scrapy.Field()
alt = scrapy.Field()
Нечего сказать, просто напишите, что вы хотите сохранить
Обзор фильмов
class ReviewPipeline(object):
reviewInsert = '''insert into reviews(id,username,nickname,avatar,summary,title,movieId,rating,create_time,alt) values ("{id}","{username}", "{nickname}","{avatar}", "{summary}","{title}","{movieId}","{rating}","{create_time}","{alt}")'''
def process_item(self, item, spider):
sql_insert = self.reviewInsert.format(
id=item['id'],
username=pymysql.escape_string(item['username']),
nickname=pymysql.escape_string(item['nickname']),
avatar=pymysql.escape_string(item['avatar']),
summary=pymysql.escape_string(item['summary']),
title=pymysql.escape_string(item['title']),
rating=item['rating'],
movieId=item['movieId'],
create_time=pymysql.escape_string(item['create_time']),
alt=pymysql.escape_string(item['alt'])
)
print("SQL:", sql_insert)
self.cursor.execute(sql_insert)
return item
def open_spider(self, spider):
self.connect = pymysql.connect('localhost','root','******','douban', charset='utf8', use_unicode=True)
self.cursor = self.connect.cursor()
self.connect.autocommit(True)
def close_spider(self, spider):
self.cursor.close()
self.connect.close()
和之前的电影的pipeline类似,就是基本的数据库写操作。
запустить сканер
scrapy crawl review
Когда я закончил писать эту статью, сканеры обзоров фильмов все еще ползали:
Глядя на базу данных, уже есть 97W данных:
Если вы считаете, что моя статья полезна для вас, пожалуйста, поддержите кубок☕️