Scrapy - Clé étrangère SQLalchemy non créée dans SQLite

Aug 22 2020

J'ai essayé d'exécuter Scrapy en utilisant itemLoader pour collecter toutes les données et les mettre dans SQLite 3. J'ai réussi à rassembler toutes les informations que je voulais mais je ne peux pas obtenir les clés étrangères à générer dans mes tables ThreadInfo et PostInfo en utilisant back_populatesune clé étrangère. J'ai essayé back_refmais cela n'a pas fonctionné non plus. Toutes les autres informations ont été insérées dans la base de données SQLite après la fin de mon Scrapy.

Mon objectif est d'avoir quatre tables, boardInfo, threadInfo, postInfo et authorInfo liées les unes aux autres.

  • boardInfo aura une relation un-à-plusieurs avec threadInfo
  • threadInfo aura une relation un-à-plusieurs avec postInfo
  • authorInfo aura une relation un-à-plusieurs avec threadInfo et
    postInfo.

J'ai utilisé DB Browser pour SQLite et j'ai trouvé que les valeurs de mes clés étrangères sont Null. J'ai essayé une requête pour la valeur (threadInfo.boardInfos_id), et elle s'est affichée None. J'essaie de résoudre ce problème pendant plusieurs jours et de lire le document, mais je ne peux pas résoudre le problème.

Comment puis-je faire générer les clés foriegn dans mes tables threadInfo et postInfo?

Merci pour tous les conseils et commentaires.

Voici mes models.py

from sqlalchemy import create_engine, Column, Table, ForeignKey, MetaData
from sqlalchemy import Integer, String, Date, DateTime, Float, Boolean, Text
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from scrapy.utils.project import get_project_settings

Base = declarative_base()

def db_connect():
    '''
    Performs database connection using database settings from settings.py.
    Returns sqlalchemy engine instance
    '''
    return create_engine(get_project_settings().get('CONNECTION_STRING'))

def create_table(engine):
    Base.metadata.create_all(engine)

class BoardInfo(Base): 
    __tablename__ = 'boardInfos'
    id = Column(Integer, primary_key=True)
    boardName = Column('boardName', String(100)) 
    threadInfosLink = relationship('ThreadInfo', back_populates='boardInfosLink') # One-to-Many with threadInfo

class ThreadInfo(Base):
    __tablename__ = 'threadInfos'
    id = Column(Integer, primary_key=True)
    threadTitle = Column('threadTitle', String())
    threadLink = Column('threadLink', String())
    threadAuthor = Column('threadAuthor', String())
    threadPost = Column('threadPost', Text())
    replyCount = Column('replyCount', Integer)
    readCount = Column('readCount', Integer)

    boardInfos_id = Column(Integer, ForeignKey('boardInfos.id')) # Many-to-One with boardInfo
    boardInfosLink = relationship('BoardInfo', back_populates='threadInfosLink') # Many-to-One with boardInfo

    postInfosLink = relationship('PostInfo', back_populates='threadInfosLink') # One-to-Many with postInfo
    
    authorInfos_id = Column(Integer, ForeignKey('authorInfos.id')) # Many-to-One with authorInfo
    authorInfosLink = relationship('AuthorInfo', back_populates='threadInfosLink') # Many-to-One with authorInfo

class PostInfo(Base):
    __tablename__ = 'postInfos'
    id = Column(Integer, primary_key=True)
    postOrder = Column('postOrder', Integer, nullable=True)
    postAuthor = Column('postAuthor', Text(), nullable=True)
    postContent = Column('postContent', Text(), nullable=True)
    postTimestamp = Column('postTimestamp', Text(), nullable=True)

    threadInfos_id = Column(Integer, ForeignKey('threadInfos.id')) # Many-to-One with threadInfo 
    threadInfosLink = relationship('ThreadInfo', back_populates='postInfosLink') # Many-to-One with threadInfo 
    
    authorInfos_id = Column(Integer, ForeignKey('authorInfos.id')) # Many-to-One with authorInfo
    authorInfosLink = relationship('AuthorInfo', back_populates='postInfosLink') # Many-to-One with authorInfo

class AuthorInfo(Base):
    __tablename__ = 'authorInfos'
    id = Column(Integer, primary_key=True)
    threadAuthor = Column('threadAuthor', String())

    postInfosLink = relationship('PostInfo', back_populates='authorInfosLink') # One-to-Many with postInfo
    threadInfosLink = relationship('ThreadInfo', back_populates='authorInfosLink') # One-to-Many with threadInfo

Voici mes pipelines.py

from sqlalchemy import exists, event
from sqlalchemy.orm import sessionmaker
from scrapy.exceptions import DropItem
from .models import db_connect, create_table, BoardInfo, ThreadInfo, PostInfo, AuthorInfo
from sqlalchemy.engine import Engine
from sqlite3 import Connection as SQLite3Connection
import logging

@event.listens_for(Engine, "connect")
def _set_sqlite_pragma(dbapi_connection, connection_record):
    if isinstance(dbapi_connection, SQLite3Connection):
        cursor = dbapi_connection.cursor()
        cursor.execute("PRAGMA foreign_keys=ON;")
        # print("@@@@@@@ PRAGMA prog is running!! @@@@@@")
        cursor.close()

class DuplicatesPipeline(object):

    def __init__(self):
        '''
        Initializes database connection and sessionmaker.
        Creates tables.
        '''
        engine = db_connect()
        create_table(engine)
        self.Session = sessionmaker(bind=engine)
        logging.info('****DuplicatesPipeline: database connected****')

    def process_item(self, item, spider):

        session = self.Session()
        
        exist_threadLink = session.query(exists().where(ThreadInfo.threadLink == item['threadLink'])).scalar()
        exist_thread_replyCount = session.query(ThreadInfo.replyCount).filter_by(threadLink = item['threadLink']).scalar()
        if exist_threadLink is True: # threadLink is in DB
            if exist_thread_replyCount < item['replyCount']: # check if replyCount is more?
                return item
                session.close()
            else:
                raise DropItem('Duplicated item found and replyCount is not changed')
                session.close()
        else: # New threadLink to be added to BoardPipeline
            return item
            session.close()

class BoardPipeline(object):
    def __init__(self):
        '''
        Initializes database connection and sessionmaker
        Creates tables
        '''
        engine = db_connect()
        create_table(engine)
        self.Session = sessionmaker(bind=engine)

    def process_item(self, item, spider):
        '''
        Save scraped info in the database
        This method is called for every item pipeline component
        '''

        session = self.Session()

        # Input info to boardInfos
        boardInfo = BoardInfo()
        boardInfo.boardName = item['boardName']
        
        # Input info to threadInfos
        threadInfo = ThreadInfo()
        threadInfo.threadTitle = item['threadTitle']
        threadInfo.threadLink = item['threadLink']
        threadInfo.threadAuthor = item['threadAuthor']
        threadInfo.threadPost = item['threadPost']
        threadInfo.replyCount = item['replyCount']
        threadInfo.readCount = item['readCount']

        # Input info to postInfos
        # Due to info is in list, so we have to loop and add it.
        for num in range(len(item['postOrder'])):
            postInfoNum = 'postInfo' + str(num)
            postInfoNum = PostInfo()
            postInfoNum.postOrder = item['postOrder'][num]
            postInfoNum.postAuthor = item['postAuthor'][num]
            postInfoNum.postContent = item['postContent'][num]
            postInfoNum.postTimestamp = item['postTimestamp'][num]
            session.add(postInfoNum)
        
        # Input info to authorInfo
        authorInfo = AuthorInfo()
        authorInfo.threadAuthor = item['threadAuthor'] 

        # check whether the boardName exists
        exist_boardName = session.query(exists().where(BoardInfo.boardName == item['boardName'])).scalar()
        if exist_boardName is False:  # the current boardName does not exists
            session.add(boardInfo)

        # check whether the threadAuthor exists
        exist_threadAuthor = session.query(exists().where(AuthorInfo.threadAuthor == item['threadAuthor'])).scalar()
        if exist_threadAuthor is False:  # the current threadAuthor does not exists
            session.add(authorInfo)

        try:
            session.add(threadInfo)
            session.commit()

        except:
            session.rollback()
            raise

        finally:
            session.close()

        return item

Réponses

kerasbaz Aug 22 2020 at 08:43

D'après le code que je peux voir, il ne me semble pas que vous définissiez ThreadInfo.authorInfosLinkou ThreadInfo.authorInfos_idnulle part (il en va de même pour tous vos FK / relations).

Pour que les objets associés soient attachés à une instance ThreadInfo, vous devez les créer, puis les attacher quelque chose comme:

        # Input info to authorInfo
        authorInfo = AuthorInfo()
        authorInfo.threadAuthor = item['threadAuthor'] 
        
        threadInfo.authorInfosLink = authorInfo

Vous ne souhaitez probablement pas session.add () chaque objet s'il est lié via FK. Vous voudrez:

  1. instancier un BoardInfoobjetbi
  2. puis instanciez attachez votre ThreadInfoobjet associéti
  3. attachez votre objet associé, par exemple bi.threadInfosLink = ti
  4. À la fin de toutes vos relations chaînées, vous pouvez simplement ajouter bià la session en utilisant session.add(bi)- tous les objets liés seront ajoutés via leurs relations et les FK seront corrects.
kerasbaz Aug 22 2020 at 10:27

D'après la discussion dans les commentaires de mon autre réponse, voici comment je rationaliserais vos modèles pour qu'ils aient plus de sens pour moi.

Remarquer:

  1. J'ai supprimé partout les "Info" inutiles
  2. J'ai supprimé les noms de colonnes explicites de vos définitions de modèle et je vais plutôt me fier à la capacité de SQLAlchemy à les déduire pour moi en fonction de mes noms d'attributs
  3. Dans un objet "Post", je ne nomme pas l'attribut PostContent, cela implique que le contenu se rapporte à la publication car c'est ainsi que nous y accédons - appelez simplement l'attribut "Post"
  4. J'ai supprimé toute la terminologie "Lien" - dans les endroits où je pense que vous voulez une référence à une collection d'objets liés, j'ai fourni un attribut pluriel de cet objet comme relation.
  5. J'ai laissé une ligne dans le modèle de publication que vous pouvez supprimer. Comme vous pouvez le voir, vous n'avez pas besoin d '"auteur" deux fois - une fois en tant qu'objet lié et une fois sur le poste, ce qui va à l'encontre de l'objectif des FK.

Avec ces modifications, lorsque vous essayez d'utiliser ces modèles à partir de votre autre code, il devient évident où vous devez utiliser .append () et où vous affectez simplement l'objet associé. Pour un objet Board donné, vous savez que 'threads' est une collection basée uniquement sur le nom de l'attribut, vous allez donc faire quelque chose commeb.threads.append(thread)

from sqlalchemy import create_engine, Column, Table, ForeignKey, MetaData
from sqlalchemy import Integer, String, Date, DateTime, Float, Boolean, Text
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

class Board(Base): 
    __tablename__ = 'board'
    id = Column(Integer, primary_key=True)
    name = Column(String(100)) 
    threads = relationship(back_populates='board')

class Thread(Base):
    __tablename__ = 'thread'
    id = Column(Integer, primary_key=True)
    title = Column(String())
    link = Column(String())
    author = Column(String())
    post = Column(Text())
    reply_count = Column(Integer)
    read_count = Column(Integer)

    board_id = Column(Integer, ForeignKey('Board.id'))
    board = relationship('Board', back_populates='threads')

    posts = relationship('Post', back_populates='threads')
    
    author_id = Column(Integer, ForeignKey('Author.id'))
    author = relationship('Author', back_populates='threads')

class Post(Base):
    __tablename__ = 'post'
    id = Column(Integer, primary_key=True)
    order = Column(Integer, nullable=True)
    author = Column(Text(), nullable=True)    # remove this line and instead use the relationship below
    content = Column(Text(), nullable=True)
    timestamp = Column(Text(), nullable=True)

    thread_id = Column(Integer, ForeignKey('Thread.id'))
    thread = relationship('Thread', back_populates='posts')
    
    author_id = Column(Integer, ForeignKey('Author.id')) 
    author = relationship('Author', back_populates='posts')

class AuthorInfo(Base):
    __tablename__ = 'author'
    id = Column(Integer, primary_key=True)
    name = Column(String())

    posts = relationship('Post', back_populates='author') 
    threads = relationship('Thread', back_populates='author')