Все те, кто много работает в имперской столице, знают, что снять подходящий дом действительно непросто. Агент будет взимать арендную плату за один месяц в качестве агентского вознаграждения. Более того, многих черных посредников обманывают и похищают под видом аренды жилья. Чтобы отыскать на просторах постов настоящего помещика, это все равно, что искать иголку в стоге сена, и при этом нужно бороться смекалкой и храбростью с разными черными посредниками. Далее я расскажу историю моей кровавой битвы.
Итак, с чего начать?Давайте сначала выберем позицию. Можно сказать, что на долю посредников приходится большинство веб-сайтов, таких как Ganji 58. Местность крутая, ее легко защищать и трудно атаковать. Что же касается холостых рыб, то ресурсов слишком мало, а ловить их особого значения не имеет, поэтому они тоже сдаются. Я нацелился на Дубана. Большинство детской обуви в имперской столице знают, что в группе Douban много групп по аренде, большинство из них молодые люди, и многие из них сдают в субаренду, но большая часть - это контракт, подписанный с арендодателем, который экономит агентское вознаграждение. Грубо пролистал.В основном объем обновления за один день может достигать 90 страниц, по 25 штук данных на странице.Конечно, часть старых доливала. Этот объем данных уже довольно большой.Хотя здесь также смешано большое количество посредников, это относительно намного лучше, чем в других местах.
Торжественно заявляем: Вы должны контролировать частоту сканирования данных, чтобы не мешать нормальному доступу к сайту! А если частота слишком высока, то будет убит Дубан, а лезть и лелеять! Также внимательно читайте комментарии!
Давайте сначала проанализируем структуру страницы, которую нужно просканировать. известныйГруппа по аренде в ПекинеНапример.
Сначала нажимаем набольше групповых обсужденийПереключитесь на страницу списка, чтобы можно было проанализировать логику страницы. Перелистывая несколько страниц назад и вперед, нетрудно обнаружить, что Douban использует параметры, стоящие за URL-адресом, для осуществления пейджинга. Например, адрес первой страницыhttps://www.douban.com/group/beijingzufang/discussion?start=0
, вторая страницаhttps://www.douban.com/group/beijingzufang/discussion?start=25
, 25 штук данных на странице, очень понятно, да?
В настоящее время нам нужно только получить данные каждой страницы отдельно, а затем выполнить некоторую фильтрацию, что может значительно сократить время фильтрации. В качестве объектов сканирования выбираем первые 20 страниц, с одной стороны, это не повлияет на работу сайта, а с другой стороны, мы также следим за тем, чтобы данные были максимально актуальными.
Хорошо, вот в чем дело: в качестве интерфейса я использую node для сканирования и сначала ввожу некоторые необходимые зависимости.
import fs from 'fs' // node的文件模块,用于将筛选后的数据输出为html
import path from 'path' // node的路径模块,用于处理文件的路径
// 以下模块非node.js自带模块,需要使用npm安装
// 客户端请求代理模块
import superagent from "superagent"
// node端操作dom的利器,可以理解成node版jQuery,语法与jQuery几乎一样
import cheerio from "cheerio"
// 通过事件来决定执行顺序的工具,下面用到时作详解
import eventproxy from 'eventproxy'
// async是一个第三方node模块,mapLimit用于控制访问频率
import mapLimit from "async/mapLimit"
Затем мы можем организовать страницы, которые мы хотим сканировать, в массив
let ep = new eventproxy() // 实例化eventproxy
let baseUrl = 'https://www.douban.com/group/beijingzufang/discussion?start=';
let pageUrls = [] // 要抓取的页面数组
let page = 20 // 抓取页面数量
let perPageQuantity = 25 // 每页数据条数
for (let i = 0; i < page; i++) {
pageUrls.push({
url: baseUrl + i * perPageQuantity
});
}
Простой анализ домовой структуры страницы. Действительные данные на странице все вtable
, первыйtr
это заголовок, то каждыйtr
соответствует фрагменту данных. затем каждыйtr
4 подtd
. Сохраните название, автора, количество ответов и время последней модификации соответственно.
Сначала пишем функцию входа, посещаем все страницы для обхода и сохраняем нужные нам данные. Другими словами, я уже давно не писал jQuery.
function start() {
// 遍历爬取页面
const getPageInfo = (pageItem, callback) => {
// 设置访问间隔
let delay = parseInt((Math.random() * 30000000) % 1000, 10)
pageUrls.forEach(pageUrl => {
superagent.get(pageUrl.url)
// 模拟浏览器
.set('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36')
// 如果你不乖乖少量爬数据的话,很可能被豆瓣kill掉,这时候需要模拟登录状态才能访问
// .set('Cookie','')
.end((err, pres) => {
let $ = cheerio.load(pres.text) // 将页面数据用cheerio处理,生成一个类jQuery对象
let itemList = $('.olt tbody').children().slice(1, 26) // 取出table中的每行数据,并过滤掉表格标题
// 遍历页面中的每条数据
for (let i = 0; i < itemList.length; i++) {
let item = itemList.eq(i).children()
let title = item.eq(0).children('a').text() || '' // 获取标题
let url = item.eq(0).children('a').attr('href') || '' // 获取详情页链接
// let author = item.eq(1).children('a').attr('href').replace('https://www.douban.com/people', '').replace(/\//g, '') || '' // 获取作者id
let author = item.eq(1).children('a').text() || '' // 这里改为使用作者昵称而不是id的原因是发现有些中介注册了好多账号,打一枪换个地方。虽然同名也有,但是这么小的数据量下,概率低到忽略不计
let markSum = item.eq(2).text() // 获取回应数量
let lastModify = item.eq(3).text() // 获取最后修改时间
let data = {
title,
url,
author,
markSum,
lastModify
}
// ep.emit('事件名称', 数据内容)
ep.emit('preparePage', data) // 每处理完一条数据,便把这条数据通过preparePage事件发送出去,这里主要是起计数的作用
}
setTimeout(() => {
callback(null, pageItem.url);
}, delay);
})
})
}
}
Мы используем mapLimit для управления частотой доступа.Подробнее о mapLimit см. в официальной документации.портал
mapLimit(pageUrls, 2, function (item, callback) {
getPageInfo(item, callback);
}, function (err) {
if (err) {
console.log(err)
}
console.log('抓取完毕')
});
Кратко расскажем о стратегии фильтрации: во-первых, в заголовке отфильтровать неуместные локации и наиболее часто употребляемые слова посредников. Вы также можете добавить нужные ключевые слова и целенаправленно фильтровать их. Затем подсчитайте количество сообщений каждого автора.Условие оценки здесь состоит в том, что если количество сообщений каждого человека появляется более 5 раз на просканированной странице, он считается посредником. Если количество ответов на определенный пост огромно, либо за старый пост проголосовали, либо есть вероятность, что кто-то постоянно очищает рейтинг.Порог, который я установил здесь, равен 100. Вы только представьте себе, что нормальный арендодатель не был бы так сумашедшим в своем существовании, потому что хороший дом не заботится о том, чтобы сдать его в аренду.Вполне вероятно, что посредник каждый день чистит старые столбы. Даже если все смотрят, потому что дом лучше, вероятность того, что вы его арендуете, очень мала, поэтому он отфильтровывается напрямую.
// 我们设置三个全局变量来保存一些数据
let result = [] // 存放最终筛选结果
let authorMap = {} // 我们以对象属性的方式,来统计每个的发帖数
let intermediary = [] // 中介id列表,你也可以把这部分数据保存起来,以后抓取的时候直接过滤掉!
// 还记得之前的ep.emit()吗,它的每次emit都被这里捕获。ep.after('事件名称',数量,事件达到指定数量后的callback())。
// 也就是说,总共有20*25(页面数*每页数据量)个事件都被捕获到以后,才会执行这里的回调函数
ep.after('preparePage', pageUrls.length * page, function (data) {
// 这里我们传入不想要出现的关键词,用'|'隔开 。比如排除一些位置,排除中介常用短语
let filterWords = /押一付一|短租|月付|蛋壳|有房出租|6号线|六号线/
// 这里我们传入需要筛选的关键词,如没有,可设置为空格
let keyWords = /西二旗/
// 我们先统计每个人的发帖数,并以对象的属性保存。这里利用对象属性名不能重复的特性实现计数。
data.forEach(item => {
authorMap[item.author] = authorMap[item.author] ? ++authorMap[item.author] : 1
if (authorMap[item.author] > 4) {
intermediary.push(item.author) // 如果发现某个人的发帖数超过5条,直接打入冷宫。
}
})
// 数组去重,Set去重了解一下,可以查阅Set这种数据结构
intermediary = [...new Set(intermediary)]
// 再次遍历抓取到的数据
data.forEach(item => {
// 这里if的顺序可是有讲究的,合理的排序可以提升程序的效率
if (item.markSum > 100) {
console.log('评论过多,丢弃')
return
}
if (filterWords.test(item.title)) {
console.log('标题带有不希望出现的词语')
return
}
if(intermediary.includes(item.author)){
console.log('发帖数过多,丢弃')
return
}
// 只有通过了上面的层层检测,才会来到最后一步,这里如果你没有设期望的关键词,筛选结果会被统统加到结果列表中
if (keyWords.test(item.title)) {
result.push(item)
}
})
// .......
});
На данный момент у нас есть ожидаемый список результатов, но распечатать его напрямую не так просто, поэтому мы генерируем из него html. Нам просто нужно просто собрать html
// 设置html模板
let top = '<!DOCTYPE html>' +
'<html lang="en">' +
'<head>' +
'<meta charset="UTF-8">' +
'<style>' +
'.listItem{ display:block;margin-top:10px;text-decoration:none;}' +
'.markSum{ color:red;}' +
'.lastModify{ color:"#aaaaaa"}' +
'</style>' +
'<title>筛选结果</title>' +
'</head>' +
'<body>' +
'<div>'
let bottom = '</div> </body> </html>'
// 拼装有效数据html
let content = ''
result.forEach(function (item) {
content += `<a class="listItem" href="${item.url}" target="_blank">${item.title}_____<span class="markSum">${item.markSum}</span>____<span class="lastModify">${item.lastModify}</span>`
})
let final = top + content + bottom
// 最后把生成的html输出到指定的文件目录下
fs.writeFile(path.join(__dirname, '../tmp/result.html'), final, function (err) {
if (err) {
return console.error(err);
}
console.log('success')
});
Наконец, нам просто нужно выставить функцию входа
export default {
start
}
Поскольку мы используем синтаксис ES6, нам нужно использоватьbabel-node
. Установить первымbabel-cli
, вы можете выбрать глобальную или локальную установку,npm i babel-cli -g
. При этом не забудьте про установку трех зависимостей в начале статьи.
Наконец, мы вносим приведенный выше скрипт в файл index.js и выполняем егоbabel-node index.js
. Мы видели впечатляющий успех.
// index.js
import douban from './src/douban.js'
douban.start()
Наконец, давайте откроем HTML, чтобы увидеть эффект. Количество ответов отмечено красным. Щелкните заголовок, чтобы перейти непосредственно на страницу, соответствующую Douban. В то же время, используя эффект изменения цвета после нажатия на тег a, мы легко можем судить о том, видели ли мы эти данные.
Я просто установил некоторые условия фильтрации, и данные упали с 500 до 138, что значительно сократило время проверки. Если я добавлю определенные ключевые слова фильтра, результаты поиска будут более точными!
Что ж, уже поздно, и на сегодняшнем обмене все. Если вы думаете, что найти дом сложнее, вам все равно придется найти Ляньцзя.Я люблю таких крупных посредников, как моя семья, которая более надежна и беззаботна. Наконец-то желаю всем найти теплое гнездышко!