В настоящее время я работаю над созданием веб-скребка для веб-сайта под названием Mountain Project и столкнулся с проблемой при вставке элементов в базу данных MySQL (MariaDB).
Базовый c поток моего сканера таков:
- Получить ответ от экстрактора ссылок (я подклассифицирую CrawlSpider)
- Извлечь данные из ответа и отправить извлеченные элементы по элементу конвейер
- Элементы выбираются SqlPipeline и вставляются в базу данных
Важное замечание о шаге 2 заключается в том, что сканер отправляет несколько элементов вниз по трубопровод. Первый элемент будет основным ресурсом (либо маршрут, либо область), а затем любые другие элементы после него будут другими важными данными об этом ресурсе. Для областей эти элементы будут данными о различных погодных условиях, для маршрутов эти элементы будут иметь разные оценки, назначенные этим маршрутам.
Мои таблицы областей и маршрутов выглядят следующим образом:
CREATE TABLE `area` (
`area_id` INT(10) UNSIGNED NOT NULL,
`parent_id` INT(10) UNSIGNED NULL DEFAULT NULL,
`name` VARCHAR(200) NOT NULL DEFAULT '',
`latitude` FLOAT(12) NULL DEFAULT -1,
`longitude` FLOAT(12) NULL DEFAULT -1,
`elevation` INT(11) NULL DEFAULT '0',
`link` VARCHAR(300) NOT NULL,
PRIMARY KEY (`area_id`) USING BTREE
)
CREATE TABLE `route` (
`route_id` INT(10) UNSIGNED NOT NULL,
`parent_id` INT(10) UNSIGNED NOT NULL,
`name` VARCHAR(200) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
`link` VARCHAR(300) NOT NULL COLLATE 'utf8_general_ci',
`rating` FLOAT(12) NULL DEFAULT '0',
`types` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
`pitches` INT(3) NULL DEFAULT '0',
`height` INT(5) NULL DEFAULT '0',
`length` VARCHAR(5) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`route_id`) USING BTREE,
INDEX `fk_parent_id` (`parent_id`) USING BTREE,
CONSTRAINT `fk_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `mountainproject`.`area` (`area_id`) ON UPDATE CASCADE ON DELETE CASCADE
)
И вот пример одной из моих таблиц условий:
CREATE TABLE `temp_avg` (
`month` INT(2) UNSIGNED NOT NULL,
`area_id` INT(10) UNSIGNED NOT NULL,
`avg_high` INT(3) NOT NULL,
`avg_low` INT(3) NOT NULL,
PRIMARY KEY (`month`, `area_id`) USING BTREE,
INDEX `fk_area_id` (`area_id`) USING BTREE,
CONSTRAINT `fk_area_id` FOREIGN KEY (`area_id`) REFERENCES `mountainproject`.`area` (`area_id`) ON UPDATE CASCADE ON DELETE CASCADE
)
Вот где вещи начинают становиться неприятными. Если я запускаю свой сканер и просто извлекаю области , все работает нормально. Область вставляется в базу данных, и все данные условий вставляются без проблем. Однако, когда я пытаюсь извлечь области и маршрутов, я получаю ошибки ограничения внешнего ключа при попытке вставить маршруты, потому что область, которой принадлежит маршрут (parent_id), не существует. В настоящее время, чтобы обойти это, я дважды запускаю свой гусеничный ход. Один раз, чтобы извлечь данные области, и один раз, чтобы извлечь данные маршрута. Если я это сделаю, все пойдет гладко.
Мое лучшее предположение относительно того, почему это не работает в настоящее время, состоит в том, что вставляемые области не были зафиксированы, и поэтому, когда я пытаюсь добавить маршрут, который принадлежит к незафиксированной области, она не может найти родительскую область. Однако эта теория быстро разваливается, потому что я могу вставить данные условия в тот же прогон, в который вставляю область, к которой принадлежат данные.
Мой код вставки выглядит следующим образом:
def insert_item(self, table_name, item):
encoded_vals = [self.sql_encode(val) for val in item.values()]
sql = "INSERT INTO %s (%s) VALUES (%s)" % (
table_name,
", ".join(item.keys()),
", ".join(encoded_vals)
)
logging.debug(sql)
self.cursor.execute(sql)
# EDIT: As suggested by @tadman I have moved to using the built in sql value
# encoding. I'm leaving this here because it doesn't affect the issue
def sql_encode(self, value):
"""Encode provided value and return a valid SQL value
Arguments:
value {Any} -- Value to encode
Returns:
str -- SQL encode value as a str
"""
encoded_val = None
is_empty = False
if isinstance(value, str):
is_empty = len(value) == 0
encoded_val = "NULL" if is_empty or value is None else value
if isinstance(encoded_val, str) and encoded_val is not "NULL":
encode_val = encoded_val.replace("\"", "\\\"")
encoded_val = "\"%s\"" % encoded_val
return str(encoded_val)
Остальная часть проекта находится в репозитории GitHub , если требуется больше кода / контекста